update
This commit is contained in:
parent
1992c3359d
commit
6b85b05882
Binary file not shown.
@ -4,7 +4,7 @@ URL configuration for accounts app.
|
||||
|
||||
from django.urls import path
|
||||
from . import views
|
||||
from allauth.account.views import SignupView, LoginView, LogoutView
|
||||
# from allauth.account.views import SignupView, LoginView, LogoutView
|
||||
|
||||
app_name = 'accounts'
|
||||
|
||||
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -28,7 +28,7 @@ SECRET_KEY = 'django-insecure-#w(wlw2r86+q03%d(=mp_cbx+atrrot_aorpa))i0!s26@y9&&
|
||||
DEBUG = True
|
||||
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '10.10.1.109']
|
||||
|
||||
# Application definition
|
||||
DJANGO_APPS = [
|
||||
@ -45,6 +45,7 @@ THIRD_PARTY_APPS = [
|
||||
'corsheaders',
|
||||
'django_extensions',
|
||||
'allauth',
|
||||
# 'allauth.socialaccount',
|
||||
]
|
||||
|
||||
LOCAL_APPS = [
|
||||
@ -171,17 +172,17 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
AUTH_USER_MODEL = 'accounts.User'
|
||||
|
||||
# Account login
|
||||
ACCOUNT_LOGIN_METHODS = {'email'}
|
||||
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
|
||||
ACCOUNT_UNIQUE_EMAIL = True
|
||||
|
||||
# ACCOUNT_LOGIN_METHODS = {'email'}
|
||||
# ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
|
||||
# ACCOUNT_UNIQUE_EMAIL = True
|
||||
#
|
||||
LOGOUT_REDIRECT_URL = 'home'
|
||||
ACCOUNT_SIGNUP_REDIRECT_URL = '/'
|
||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = 'email'
|
||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
ACCOUNT_CONFIRM_EMAIL_ON_GET = True
|
||||
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_MINUTES = 5
|
||||
LOGIN_REDIRECT_URL = 'home'
|
||||
# ACCOUNT_SIGNUP_REDIRECT_URL = '/'
|
||||
# ACCOUNT_USER_MODEL_USERNAME_FIELD = 'email'
|
||||
# ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
# ACCOUNT_CONFIRM_EMAIL_ON_GET = True
|
||||
# ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_MINUTES = 5
|
||||
# LOGIN_REDIRECT_URL = 'home'
|
||||
|
||||
# REST Framework Configuration
|
||||
REST_FRAMEWORK = {
|
||||
|
||||
@ -36,7 +36,7 @@ urlpatterns += i18n_patterns(
|
||||
path('accounts/', include('allauth.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
path('', include('core.urls')),
|
||||
# path('accounts/', include('accounts.urls')),
|
||||
path('accounts/', include('accounts.urls')),
|
||||
path('patients/', include('patients.urls')),
|
||||
path('appointments/', include('appointments.urls')),
|
||||
path('inpatients/', include('inpatients.urls')),
|
||||
|
||||
Binary file not shown.
@ -45,15 +45,15 @@ class IntegrationDashboardView(LoginRequiredMixin, TemplateView):
|
||||
# Basic statistics
|
||||
context.update({
|
||||
'total_systems': ExternalSystem.objects.filter(tenant=self.request.user.tenant).count(),
|
||||
'total_endpoints': IntegrationEndpoint.objects.filter(tenant=self.request.user.tenant).count(),
|
||||
'total_mappings': DataMapping.objects.filter(tenant=self.request.user.tenant).count(),
|
||||
'total_webhooks': WebhookEndpoint.objects.filter(tenant=self.request.user.tenant).count(),
|
||||
'total_endpoints': IntegrationEndpoint.objects.filter(external_system__tenant=self.request.user.tenant).count(),
|
||||
'total_mappings': DataMapping.objects.filter(endpoint__external_system__tenant=self.request.user.tenant).count(),
|
||||
'total_webhooks': WebhookEndpoint.objects.filter(external_system__tenant=self.request.user.tenant).count(),
|
||||
})
|
||||
|
||||
# Recent activity
|
||||
context.update({
|
||||
'recent_executions': IntegrationExecution.objects.filter(
|
||||
tenant=self.request.user.tenant
|
||||
endpoint__tenant=self.request.user.tenant
|
||||
).order_by('-execution_time')[:10],
|
||||
|
||||
'recent_webhook_executions': WebhookExecution.objects.filter(
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -121,7 +121,7 @@ class InventoryItemAdmin(admin.ModelAdmin):
|
||||
}),
|
||||
('Reorder Information', {
|
||||
'fields': [
|
||||
'reorder_point', 'reorder_quantity', 'max_stock_level'
|
||||
'reorder_point', 'reorder_quantity', 'min_stock_level', 'max_stock_level'
|
||||
]
|
||||
}),
|
||||
('Supplier Information', {
|
||||
|
||||
@ -72,12 +72,12 @@ class InventoryDashboardView(LoginRequiredMixin, TemplateView):
|
||||
context['expiring_soon_items'] = expiring_soon_items
|
||||
|
||||
# Financial statistics
|
||||
# total_inventory_value = InventoryStock.objects.filter(
|
||||
# inventory_item__tenant=user.tenant
|
||||
# ).aggregate(
|
||||
# total_value=Sum(F('quantity') * F('unit_cost'))
|
||||
# )['total_value'] or 0
|
||||
# context['total_inventory_value'] = total_inventory_value
|
||||
total_inventory_value = InventoryStock.objects.filter(
|
||||
inventory_item__tenant=user.tenant
|
||||
).aggregate(
|
||||
total_value=Sum(F('quantity_available') * F('unit_cost'))
|
||||
)['total_value'] or 0
|
||||
context['total_inventory_value'] = total_inventory_value
|
||||
|
||||
# Recent activity
|
||||
context['recent_orders'] = PurchaseOrder.objects.filter(
|
||||
@ -618,21 +618,21 @@ class InventoryStockListView(LoginRequiredMixin, ListView):
|
||||
List all inventory stock with filtering and search.
|
||||
"""
|
||||
model = InventoryStock
|
||||
template_name = 'inventory/stock_list.html'
|
||||
template_name = 'inventory/stock/stock_list.html'
|
||||
context_object_name = 'stock_items'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = InventoryStock.objects.filter(
|
||||
tenant=self.request.user.tenant
|
||||
).select_related('item', 'location')
|
||||
inventory_item__tenant=self.request.user.tenant
|
||||
).select_related('inventory_item', 'location')
|
||||
|
||||
# Search functionality
|
||||
search = self.request.GET.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(item__item_name__icontains=search) |
|
||||
Q(item__item_code__icontains=search) |
|
||||
Q(inventory_item__item_name__icontains=search) |
|
||||
Q(inventory_item__item_code__icontains=search) |
|
||||
Q(location__location_name__icontains=search)
|
||||
)
|
||||
|
||||
@ -644,7 +644,7 @@ class InventoryStockListView(LoginRequiredMixin, ListView):
|
||||
# Filter by stock status
|
||||
stock_status = self.request.GET.get('stock_status')
|
||||
if stock_status == 'low':
|
||||
queryset = queryset.filter(quantity__lte=F('minimum_stock_level'))
|
||||
queryset = queryset.filter(quantity_available__lte=F('quantity_available'))
|
||||
elif stock_status == 'out':
|
||||
queryset = queryset.filter(quantity=0)
|
||||
elif stock_status == 'expired':
|
||||
@ -655,13 +655,13 @@ class InventoryStockListView(LoginRequiredMixin, ListView):
|
||||
expiry_date__gt=timezone.now().date()
|
||||
)
|
||||
|
||||
return queryset.order_by('item__item_name', 'location__location_name')
|
||||
return queryset.order_by('inventory_item__item_name', 'location__name')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['locations'] = InventoryLocation.objects.filter(
|
||||
tenant=self.request.user.tenant
|
||||
).order_by('location_name')
|
||||
).order_by('name')
|
||||
return context
|
||||
|
||||
|
||||
@ -670,13 +670,13 @@ class InventoryStockDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about inventory stock.
|
||||
"""
|
||||
model = InventoryStock
|
||||
template_name = 'inventory/stock_detail.html'
|
||||
template_name = 'inventory/stock/stock_detail.html'
|
||||
context_object_name = 'stock'
|
||||
|
||||
def get_queryset(self):
|
||||
return InventoryStock.objects.filter(
|
||||
tenant=self.request.user.tenant
|
||||
).select_related('item', 'location')
|
||||
inventory_item__tenant=self.request.user.tenant
|
||||
).select_related('inventory_item', 'location')
|
||||
|
||||
|
||||
class InventoryStockCreateView(LoginRequiredMixin, CreateView):
|
||||
@ -717,7 +717,7 @@ class InventoryStockUpdateView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
model = InventoryStock
|
||||
form_class = InventoryStockForm
|
||||
template_name = 'inventory/stock_form.html'
|
||||
template_name = 'inventory/stock/stock_form.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return InventoryStock.objects.filter(tenant=self.request.user.tenant)
|
||||
@ -887,12 +887,12 @@ def inventory_stats(request):
|
||||
stats = {
|
||||
'total_items': InventoryItem.objects.filter(tenant=user.tenant).count(),
|
||||
'low_stock_items': InventoryStock.objects.filter(
|
||||
tenant=user.tenant,
|
||||
quantity__lte=F('minimum_stock_level')
|
||||
inventory_item__tenant=user.tenant,
|
||||
quantity_available__lte=F('quantity_available')
|
||||
).count(),
|
||||
'expired_items': InventoryStock.objects.filter(
|
||||
tenant=user.tenant,
|
||||
expiry_date__lte=timezone.now().date()
|
||||
inventory_item__tenant=user.tenant,
|
||||
expiration_date__lte=timezone.now().date()
|
||||
).count(),
|
||||
'active_orders': PurchaseOrder.objects.filter(
|
||||
tenant=user.tenant,
|
||||
|
||||
1499
inventory_data.py
1499
inventory_data.py
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -155,7 +155,7 @@ class ReportTemplateDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about a report template.
|
||||
"""
|
||||
model = ReportTemplate
|
||||
template_name = 'radiology/report_template_detail.html'
|
||||
template_name = 'radiology/templates/report_template_detail.html'
|
||||
context_object_name = 'report_template'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -180,7 +180,7 @@ class ReportTemplateCreateView(LoginRequiredMixin, PermissionRequiredMixin, Crea
|
||||
"""
|
||||
model = ReportTemplate
|
||||
form_class = ReportTemplateForm
|
||||
template_name = 'radiology/report_template_form.html'
|
||||
template_name = 'radiology/templates/report_template_form.html'
|
||||
permission_required = 'radiology.add_reporttemplate'
|
||||
success_url = reverse_lazy('radiology:report_template_list')
|
||||
|
||||
@ -211,7 +211,7 @@ class ReportTemplateUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Upda
|
||||
"""
|
||||
model = ReportTemplate
|
||||
form_class = ReportTemplateForm
|
||||
template_name = 'radiology/report_template_form.html'
|
||||
template_name = 'radiology/templates/report_template_form.html'
|
||||
permission_required = 'radiology.change_reporttemplate'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -244,7 +244,7 @@ class ReportTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Dele
|
||||
Delete a report template (soft delete by deactivating).
|
||||
"""
|
||||
model = ReportTemplate
|
||||
template_name = 'radiology/report_template_confirm_delete.html'
|
||||
template_name = 'radiology/templates/report_template_confirm_delete.html'
|
||||
permission_required = 'radiology.delete_reporttemplate'
|
||||
success_url = reverse_lazy('radiology:report_template_list')
|
||||
|
||||
@ -264,10 +264,10 @@ class ReportTemplateDeleteView(LoginRequiredMixin, PermissionRequiredMixin, Dele
|
||||
action='REPORT_TEMPLATE_DEACTIVATED',
|
||||
model='ReportTemplate',
|
||||
object_id=str(self.object.id),
|
||||
details={'template_name': self.object.template_name}
|
||||
details={'template_name': self.object.name}
|
||||
)
|
||||
|
||||
messages.success(request, f'Report template "{self.object.template_name}" deactivated successfully.')
|
||||
messages.success(request, f'Report template "{self.object.name}" deactivated successfully.')
|
||||
return redirect(self.success_url)
|
||||
|
||||
|
||||
@ -280,7 +280,7 @@ class ImagingOrderListView(LoginRequiredMixin, ListView):
|
||||
List all imaging orders with filtering and search.
|
||||
"""
|
||||
model = ImagingOrder
|
||||
template_name = 'radiology/imaging_order_list.html'
|
||||
template_name = 'radiology/orders/imaging_order_list.html'
|
||||
context_object_name = 'imaging_orders'
|
||||
paginate_by = 25
|
||||
|
||||
@ -341,24 +341,36 @@ class ImagingOrderDetailView(LoginRequiredMixin, DetailView):
|
||||
model = ImagingOrder
|
||||
template_name = 'radiology/orders/imaging_order_detail.html'
|
||||
context_object_name = 'imaging_order'
|
||||
|
||||
# def get_queryset(self):
|
||||
# return ImagingOrder.objects.filter(tenant=self.request.user.tenant)
|
||||
#
|
||||
# def get_context_data(self, **kwargs):
|
||||
# context = super().get_context_data(**kwargs)
|
||||
# imaging_order = self.object
|
||||
#
|
||||
# # Get studies for this order
|
||||
# context['studies'] = imaging_order.studies.all().order_by('-study_datetime')
|
||||
#
|
||||
# # Get reports for this order
|
||||
# context['reports'] = RadiologyReport.objects.filter(
|
||||
# study__order=imaging_order,
|
||||
# tenant=self.request.user.tenant
|
||||
# ).order_by('-created_at')
|
||||
#
|
||||
# return context
|
||||
|
||||
def get_queryset(self):
|
||||
return ImagingOrder.objects.filter(tenant=self.request.user.tenant)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
imaging_order = self.object
|
||||
|
||||
# Get studies for this order with prefetched reports
|
||||
studies = imaging_order.studies.all().order_by('-study_datetime')
|
||||
context['studies'] = studies
|
||||
|
||||
# Create a dictionary mapping each study to its reports
|
||||
# reports = {}
|
||||
# for study in studies:
|
||||
study_reports = RadiologyReport.objects.filter(
|
||||
study=studies.first,
|
||||
# tenant=self.request.user.tenant
|
||||
).order_by('-created_at')
|
||||
# reports[study.id] = study_reports
|
||||
|
||||
context['reports'] = study_reports
|
||||
|
||||
# Get all reports for this order (for backward compatibility)
|
||||
# context['reports'] = RadiologyReport.objects.filter(
|
||||
# study__order=imaging_order,
|
||||
# tenant=self.request.user.tenant
|
||||
# ).order_by('-created_at')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ImagingOrderCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||
@ -438,7 +450,7 @@ class ImagingStudyListView(LoginRequiredMixin, ListView):
|
||||
List all imaging studies with filtering and search.
|
||||
"""
|
||||
model = ImagingStudy
|
||||
template_name = 'radiology/imaging_study_list.html'
|
||||
template_name = 'radiology/studies/imaging_study_list.html'
|
||||
context_object_name = 'imaging_studies'
|
||||
paginate_by = 25
|
||||
|
||||
@ -481,7 +493,7 @@ class ImagingStudyDetailView(LoginRequiredMixin, DetailView):
|
||||
Display detailed information about an imaging study.
|
||||
"""
|
||||
model = ImagingStudy
|
||||
template_name = 'radiology/imaging_study_detail.html'
|
||||
template_name = 'radiology/studies/imaging_study_detail.html'
|
||||
context_object_name = 'imaging_study'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -512,7 +524,7 @@ class ImagingStudyCreateView(LoginRequiredMixin, PermissionRequiredMixin, Create
|
||||
"""
|
||||
model = ImagingStudy
|
||||
form_class = ImagingStudyForm
|
||||
template_name = 'radiology/imaging_study_form.html'
|
||||
template_name = 'radiology/studies/imaging_study_form.html'
|
||||
permission_required = 'radiology.add_imagingstudy'
|
||||
success_url = reverse_lazy('radiology:imaging_study_list')
|
||||
|
||||
@ -543,7 +555,7 @@ class ImagingStudyUpdateView(LoginRequiredMixin, PermissionRequiredMixin, Update
|
||||
"""
|
||||
model = ImagingStudy
|
||||
fields = ['status', 'technical_notes'] # Restricted fields
|
||||
template_name = 'radiology/imaging_study_update_form.html'
|
||||
template_name = 'radiology/studies/imaging_study_form.html'
|
||||
permission_required = 'radiology.change_imagingstudy'
|
||||
|
||||
def get_queryset(self):
|
||||
@ -580,37 +592,37 @@ class ImagingSeriesListView(LoginRequiredMixin, ListView):
|
||||
List all imaging series with filtering and search.
|
||||
"""
|
||||
model = ImagingSeries
|
||||
template_name = 'radiology/imaging_series_list.html'
|
||||
template_name = 'radiology/series/imaging_series_list.html'
|
||||
context_object_name = 'imaging_series'
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = ImagingSeries.objects.filter(tenant=self.request.user.tenant)
|
||||
|
||||
# Filter by study
|
||||
study_id = self.request.GET.get('study')
|
||||
if study_id:
|
||||
queryset = queryset.filter(study_id=study_id)
|
||||
|
||||
# Search functionality
|
||||
search = self.request.GET.get('search')
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(series_description__icontains=search) |
|
||||
Q(study__order__patient__first_name__icontains=search) |
|
||||
Q(study__order__patient__last_name__icontains=search)
|
||||
)
|
||||
|
||||
return queryset.select_related('study__order__patient').order_by('-created_at')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update({
|
||||
'studies': ImagingStudy.objects.filter(
|
||||
tenant=self.request.user.tenant
|
||||
).select_related('order__patient').order_by('-study_datetime')[:50],
|
||||
})
|
||||
return context
|
||||
# def get_queryset(self):
|
||||
# queryset = ImagingSeries.objects.filter(tenant=self.request.user.tenant)
|
||||
#
|
||||
# # Filter by study
|
||||
# study_id = self.request.GET.get('study')
|
||||
# if study_id:
|
||||
# queryset = queryset.filter(study_id=study_id)
|
||||
#
|
||||
# # Search functionality
|
||||
# search = self.request.GET.get('search')
|
||||
# if search:
|
||||
# queryset = queryset.filter(
|
||||
# Q(series_description__icontains=search) |
|
||||
# Q(study__order__patient__first_name__icontains=search) |
|
||||
# Q(study__order__patient__last_name__icontains=search)
|
||||
# )
|
||||
#
|
||||
# return queryset.select_related('study__imaging_order__patient').order_by('-created_at')
|
||||
#
|
||||
# def get_context_data(self, **kwargs):
|
||||
# context = super().get_context_data(**kwargs)
|
||||
# context.update({
|
||||
# 'studies': ImagingStudy.objects.filter(
|
||||
# tenant=self.request.user.tenant
|
||||
# ).select_related('imaging_order__patient').order_by('-study_datetime')[:50],
|
||||
# })
|
||||
# return context
|
||||
|
||||
|
||||
class ImagingSeriesDetailView(LoginRequiredMixin, DetailView):
|
||||
@ -841,11 +853,11 @@ def radiology_stats(request):
|
||||
status='IN_PROGRESS'
|
||||
).count(),
|
||||
'reports_pending': RadiologyReport.objects.filter(
|
||||
tenant=tenant,
|
||||
study__tenant=tenant,
|
||||
status='DRAFT'
|
||||
).count(),
|
||||
'critical_findings': RadiologyReport.objects.filter(
|
||||
tenant=tenant,
|
||||
study__tenant=tenant,
|
||||
has_critical_findings=True,
|
||||
status='SIGNED',
|
||||
signed_datetime__date=today
|
||||
|
||||
342
temp.txt
Normal file
342
temp.txt
Normal file
@ -0,0 +1,342 @@
|
||||
# import random
|
||||
# import uuid
|
||||
# from datetime import datetime, timedelta
|
||||
# from decimal import Decimal
|
||||
# from django.utils import timezone as django_timezone
|
||||
# from django.contrib.auth import get_user_model
|
||||
#
|
||||
# from core.models import Tenant
|
||||
# from inventory.models import InventoryItem, InventoryStock, InventoryLocation, PurchaseOrder, PurchaseOrderItem, \
|
||||
# Supplier
|
||||
#
|
||||
# User = get_user_model()
|
||||
#
|
||||
# # Saudi Arabian Inventory Data
|
||||
# SAUDI_MEDICAL_CATEGORIES = [
|
||||
# 'Pharmaceuticals',
|
||||
# 'Medical Devices',
|
||||
# 'Surgical Instruments',
|
||||
# 'Laboratory Supplies',
|
||||
# 'PPE & Safety',
|
||||
# 'IV Therapy',
|
||||
# 'Emergency Supplies'
|
||||
# ]
|
||||
#
|
||||
# SAUDI_SUPPLIERS = [
|
||||
# 'Saudi Medical Supply Co.',
|
||||
# 'Gulf Medical Equipment',
|
||||
# 'Arabian Healthcare Supplies',
|
||||
# 'Riyadh Medical Trading',
|
||||
# 'Al-Dawaa Medical',
|
||||
# 'Nahdi Medical Company',
|
||||
# 'United Pharmaceuticals'
|
||||
# ]
|
||||
#
|
||||
# SAUDI_CITIES = ['Riyadh', 'Jeddah', 'Dammam', 'Medina', 'Taif', 'Khobar']
|
||||
#
|
||||
# MEDICAL_ITEMS = [
|
||||
# {'name': 'Paracetamol 500mg', 'category': 'Pharmaceuticals', 'unit': 'TAB'},
|
||||
# {'name': 'Disposable Syringe 5ml', 'category': 'Medical Devices', 'unit': 'PCS'},
|
||||
# {'name': 'Surgical Gloves Size M', 'category': 'PPE & Safety', 'unit': 'PAIR'},
|
||||
# {'name': 'Blood Collection Tube', 'category': 'Laboratory Supplies', 'unit': 'PCS'},
|
||||
# {'name': 'IV Bag Normal Saline', 'category': 'IV Therapy', 'unit': 'BAG'},
|
||||
# {'name': 'Emergency Oxygen Mask', 'category': 'Emergency Supplies', 'unit': 'PCS'}
|
||||
# ]
|
||||
#
|
||||
#
|
||||
# def create_saudi_suppliers(tenants):
|
||||
# """Create Saudi suppliers"""
|
||||
# suppliers = []
|
||||
#
|
||||
# for tenant in tenants:
|
||||
# print(f"Creating suppliers for {tenant.name}...")
|
||||
#
|
||||
# for i, supplier_name in enumerate(SAUDI_SUPPLIERS):
|
||||
# supplier_code = f"SUP-{tenant.id}-{i + 1:03d}"
|
||||
#
|
||||
# try:
|
||||
# supplier = Supplier.objects.create(
|
||||
# tenant=tenant,
|
||||
# supplier_code=supplier_code,
|
||||
# name=supplier_name,
|
||||
# supplier_type='DISTRIBUTOR',
|
||||
# city=random.choice(SAUDI_CITIES),
|
||||
# country='Saudi Arabia',
|
||||
# is_active=True
|
||||
# )
|
||||
# suppliers.append(supplier)
|
||||
# print(f" ✓ Created supplier: {supplier_name}")
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f" ✗ Error creating supplier {supplier_name}: {e}")
|
||||
# continue
|
||||
#
|
||||
# print(f"Created {len(suppliers)} suppliers")
|
||||
# return suppliers
|
||||
#
|
||||
#
|
||||
# def create_saudi_inventory_locations(tenants):
|
||||
# """Create Saudi inventory locations"""
|
||||
# locations = []
|
||||
#
|
||||
# storage_rooms = ['Pharmacy', 'Central Supply', 'OR Storage', 'ICU Supply', 'Ward Storage']
|
||||
#
|
||||
# for tenant in tenants:
|
||||
# print(f"Creating locations for {tenant.name}...")
|
||||
#
|
||||
# for i, room in enumerate(storage_rooms):
|
||||
# location_code = f"LOC-{tenant.id}-{i + 1:03d}"
|
||||
#
|
||||
# try:
|
||||
# location = InventoryLocation.objects.create(
|
||||
# tenant=tenant,
|
||||
# location_code=location_code,
|
||||
# name=f"{room} - {tenant.city}",
|
||||
# description=f"Storage location in {room}",
|
||||
# location_type='WAREHOUSE',
|
||||
# building='Main Hospital',
|
||||
# floor='Ground Floor',
|
||||
# room=room,
|
||||
# is_active=True
|
||||
# )
|
||||
# locations.append(location)
|
||||
# print(f" ✓ Created location: {location.name}")
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f" ✗ Error creating location {room}: {e}")
|
||||
# continue
|
||||
#
|
||||
# print(f"Created {len(locations)} locations")
|
||||
# return locations
|
||||
#
|
||||
#
|
||||
# def create_saudi_inventory_items(tenants):
|
||||
# """Create Saudi inventory items"""
|
||||
# items = []
|
||||
#
|
||||
# for tenant in tenants:
|
||||
# print(f"Creating items for {tenant.name}...")
|
||||
#
|
||||
# for i, item_data in enumerate(MEDICAL_ITEMS):
|
||||
# item_code = f"ITM-{tenant.id}-{i + 1:03d}"
|
||||
#
|
||||
# try:
|
||||
# item = InventoryItem.objects.create(
|
||||
# tenant=tenant,
|
||||
# item_code=item_code,
|
||||
# item_name=item_data['name'],
|
||||
# description=f"Medical item: {item_data['name']}",
|
||||
# category=item_data['category'],
|
||||
# subcategory=item_data['category'],
|
||||
# item_type='STOCK',
|
||||
# manufacturer='Saudi Medical Industries',
|
||||
# unit_of_measure=item_data['unit'],
|
||||
# package_size=1,
|
||||
# unit_cost=Decimal(str(random.uniform(10, 100))),
|
||||
# list_price=Decimal(str(random.uniform(15, 150))),
|
||||
# has_expiration=item_data['category'] == 'Pharmaceuticals',
|
||||
# is_active=True,
|
||||
# is_tracked=True,
|
||||
# reorder_point=random.randint(10, 50),
|
||||
# reorder_quantity=random.randint(100, 500),
|
||||
# max_stock_level=random.randint(500, 1000)
|
||||
# )
|
||||
# items.append(item)
|
||||
# print(f" ✓ Created item: {item.item_name}")
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f" ✗ Error creating item {item_data['name']}: {e}")
|
||||
# continue
|
||||
#
|
||||
# print(f"Created {len(items)} items")
|
||||
# return items
|
||||
#
|
||||
#
|
||||
# def create_saudi_inventory_stock(items, locations):
|
||||
# """Create Saudi inventory stock entries"""
|
||||
# stocks = []
|
||||
#
|
||||
# for item in items:
|
||||
# print(f"Creating stock for {item.item_name}...")
|
||||
#
|
||||
# # Get locations for this tenant
|
||||
# tenant_locations = [loc for loc in locations if loc.tenant == item.tenant]
|
||||
# if not tenant_locations:
|
||||
# continue
|
||||
#
|
||||
# location = random.choice(tenant_locations)
|
||||
#
|
||||
# try:
|
||||
# stock = InventoryStock.objects.create(
|
||||
# inventory_item=item,
|
||||
# location=location,
|
||||
# quantity_on_hand=random.randint(50, 500),
|
||||
# quantity_reserved=random.randint(0, 20),
|
||||
# received_date=django_timezone.now().date() - timedelta(days=random.randint(1, 90)),
|
||||
# expiration_date=django_timezone.now().date() + timedelta(days=365) if item.has_expiration else None,
|
||||
# unit_cost=item.unit_cost,
|
||||
# quality_status='AVAILABLE'
|
||||
# )
|
||||
# stocks.append(stock)
|
||||
# print(f" ✓ Created stock for: {item.item_name}")
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f" ✗ Error creating stock for {item.item_name}: {e}")
|
||||
# continue
|
||||
#
|
||||
# print(f"Created {len(stocks)} stock entries")
|
||||
# return stocks
|
||||
#
|
||||
#
|
||||
# def create_saudi_purchase_orders(tenants, suppliers):
|
||||
# """Create Saudi purchase orders"""
|
||||
# orders = []
|
||||
#
|
||||
# for tenant in tenants:
|
||||
# print(f"Creating purchase orders for {tenant.name}...")
|
||||
#
|
||||
# # Get suppliers for this tenant
|
||||
# tenant_suppliers = [supplier for supplier in suppliers if supplier.tenant == tenant]
|
||||
# if not tenant_suppliers:
|
||||
# print(f" No suppliers found for {tenant.name}, skipping...")
|
||||
# continue
|
||||
#
|
||||
# # Get delivery locations
|
||||
# try:
|
||||
# locations = InventoryLocation.objects.filter(tenant=tenant)
|
||||
# delivery_location = locations.first() if locations.exists() else None
|
||||
# except:
|
||||
# delivery_location = None
|
||||
#
|
||||
# for i in range(3): # Create 3 orders per tenant
|
||||
# po_number = f"PO-{tenant.id}-{django_timezone.now().year}-{i + 1:04d}"
|
||||
# supplier = random.choice(tenant_suppliers)
|
||||
#
|
||||
# try:
|
||||
# order = PurchaseOrder.objects.create(
|
||||
# tenant=tenant,
|
||||
# po_number=po_number,
|
||||
# supplier=supplier,
|
||||
# order_date=django_timezone.now().date() - timedelta(days=random.randint(1, 30)),
|
||||
# requested_delivery_date=django_timezone.now().date() + timedelta(days=random.randint(7, 30)),
|
||||
# order_type='STANDARD',
|
||||
# priority='NORMAL',
|
||||
# subtotal=Decimal(str(random.uniform(1000, 10000))),
|
||||
# tax_amount=Decimal('0.00'),
|
||||
# shipping_amount=Decimal('0.00'),
|
||||
# total_amount=Decimal(str(random.uniform(1000, 10000))),
|
||||
# status='DRAFT',
|
||||
# delivery_location=delivery_location,
|
||||
# payment_terms='NET_30'
|
||||
# )
|
||||
# orders.append(order)
|
||||
# print(f" ✓ Created PO: {po_number}")
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f" ✗ Error creating PO {po_number}: {e}")
|
||||
# continue
|
||||
#
|
||||
# print(f"Created {len(orders)} purchase orders")
|
||||
# return orders
|
||||
#
|
||||
#
|
||||
# def create_saudi_purchase_order_items(orders, items):
|
||||
# """Create Saudi purchase order items"""
|
||||
# po_items = []
|
||||
#
|
||||
# for order in orders:
|
||||
# print(f"Creating items for PO {order.po_number}...")
|
||||
#
|
||||
# # Get items for this tenant
|
||||
# tenant_items = [item for item in items if item.tenant == order.tenant]
|
||||
# if not tenant_items:
|
||||
# continue
|
||||
#
|
||||
# # Create 2-3 items per order
|
||||
# num_items = min(3, len(tenant_items))
|
||||
# selected_items = random.sample(tenant_items, num_items)
|
||||
#
|
||||
# for line_num, item in enumerate(selected_items, 1):
|
||||
# quantity_ordered = random.randint(10, 100)
|
||||
# unit_price = item.unit_cost * Decimal(str(random.uniform(0.9, 1.1)))
|
||||
# total_price = unit_price * quantity_ordered
|
||||
#
|
||||
# try:
|
||||
# po_item = PurchaseOrderItem.objects.create(
|
||||
# purchase_order=order,
|
||||
# line_number=line_num,
|
||||
# inventory_item=item,
|
||||
# quantity_ordered=quantity_ordered,
|
||||
# quantity_received=0,
|
||||
# unit_price=unit_price,
|
||||
# total_price=total_price,
|
||||
# requested_delivery_date=order.requested_delivery_date,
|
||||
# status='PENDING'
|
||||
# )
|
||||
# po_items.append(po_item)
|
||||
# print(f" ✓ Created PO item: {item.item_name}")
|
||||
#
|
||||
# except Exception as e:
|
||||
# print(f" ✗ Error creating PO item for {item.item_name}: {e}")
|
||||
# continue
|
||||
#
|
||||
# print(f"Created {len(po_items)} purchase order items")
|
||||
# return po_items
|
||||
#
|
||||
#
|
||||
# def main():
|
||||
# """Main function to create all Saudi inventory data"""
|
||||
# print("🏥 Starting Saudi Inventory Data Generation...")
|
||||
#
|
||||
# # Get tenants
|
||||
# try:
|
||||
# tenants = list(Tenant.objects.filter(is_active=True)[:5]) # Limit to first 5 tenants
|
||||
# if not tenants:
|
||||
# print("❌ No active tenants found. Please run core_data.py first.")
|
||||
# return
|
||||
#
|
||||
# print(f"📋 Found {len(tenants)} active tenants")
|
||||
# except Exception as e:
|
||||
# print(f"❌ Error getting tenants: {e}")
|
||||
# return
|
||||
#
|
||||
# # Create data step by step
|
||||
# print("\n1️⃣ Creating Suppliers...")
|
||||
# suppliers = create_saudi_suppliers(tenants)
|
||||
# if not suppliers:
|
||||
# print("❌ No suppliers created. Stopping.")
|
||||
# return
|
||||
#
|
||||
# print("\n2️⃣ Creating Locations...")
|
||||
# locations = create_saudi_inventory_locations(tenants)
|
||||
# if not locations:
|
||||
# print("❌ No locations created. Stopping.")
|
||||
# return
|
||||
#
|
||||
# print("\n3️⃣ Creating Items...")
|
||||
# items = create_saudi_inventory_items(tenants)
|
||||
# if not items:
|
||||
# print("❌ No items created. Stopping.")
|
||||
# return
|
||||
#
|
||||
# print("\n4️⃣ Creating Stock...")
|
||||
# stocks = create_saudi_inventory_stock(items, locations)
|
||||
#
|
||||
# print("\n5️⃣ Creating Purchase Orders...")
|
||||
# orders = create_saudi_purchase_orders(tenants, suppliers)
|
||||
#
|
||||
# print("\n6️⃣ Creating Purchase Order Items...")
|
||||
# po_items = create_saudi_purchase_order_items(orders, items)
|
||||
#
|
||||
# print("\n🎉 Saudi Inventory Data Generation Complete!")
|
||||
# print(f"📊 Summary:")
|
||||
# print(f" - Suppliers: {len(suppliers)}")
|
||||
# print(f" - Locations: {len(locations)}")
|
||||
# print(f" - Items: {len(items)}")
|
||||
# print(f" - Stock Entries: {len(stocks)}")
|
||||
# print(f" - Purchase Orders: {len(orders)}")
|
||||
# print(f" - PO Items: {len(po_items)}")
|
||||
#
|
||||
#
|
||||
# if __name__ == "__main__":
|
||||
# main()
|
||||
BIN
templates/.DS_Store
vendored
BIN
templates/.DS_Store
vendored
Binary file not shown.
@ -109,14 +109,14 @@
|
||||
{% endif %}
|
||||
<div class="navbar-item navbar-user dropdown">
|
||||
<a href="#" class="navbar-link dropdown-toggle d-flex align-items-center" data-bs-toggle="dropdown">
|
||||
<img src="{% static '/img/user/user-13.jpg' %}" alt="" />
|
||||
<img src="{% static '/img/user/user-4.jpg' %}" alt="" />
|
||||
<span>
|
||||
<span class="d-none d-md-inline fw-bold">{{ request.user.get_full_name }}</span>
|
||||
<b class="caret"></b>
|
||||
</span>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-end me-1">
|
||||
<a href="/extra/profile" class="dropdown-item">Edit Profile</a>
|
||||
<a href="#" class="dropdown-item">Edit Profile</a>
|
||||
<a href="/email/inbox" class="dropdown-item d-flex align-items-center">
|
||||
Inbox
|
||||
<span class="badge bg-danger rounded-pill ms-auto pb-4px">2</span>
|
||||
|
||||
@ -534,20 +534,20 @@ function initializeCharts() {
|
||||
{# .catch(error => {#}
|
||||
{# console.error('Error updating charts:', error);#}
|
||||
{# });#}
|
||||
{# } #}
|
||||
{# }#}
|
||||
|
||||
// Real-time notifications for critical alerts
|
||||
{#function checkCriticalAlerts() {#}
|
||||
{# if ({{ expired_items }} > 0 || {{ low_stock_items }} > 5) {#}
|
||||
{# // Show notification#}
|
||||
{# if ('Notification' in window && Notification.permission === 'granted') {#}
|
||||
{# new Notification('Inventory Alert', {#}
|
||||
{# body: `${{{ expired_items }}} expired items, ${{{ low_stock_items }}} low stock items`,#}
|
||||
{# icon: '/static/img/inventory-icon.png'#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
{# }#}
|
||||
function checkCriticalAlerts() {
|
||||
if ({{ expired_items }} > 0 || {{ low_stock_items }} > 5) {
|
||||
// Show notification
|
||||
if ('Notification' in window && Notification.permission === 'granted') {
|
||||
new Notification('Inventory Alert', {
|
||||
body: `{{ expired_items }} expired items, {{ low_stock_items }} low stock items`,
|
||||
icon: '/static/img/inventory-icon.png'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request notification permission
|
||||
if ('Notification' in window && Notification.permission === 'default') {
|
||||
|
||||
@ -9,13 +9,13 @@
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'inventory:dashboard' %}">Inventory</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'inventory:stock_list' %}">Stock</a></li>
|
||||
<li class="breadcrumb-item active">{{ object.item.name }}</li>
|
||||
<li class="breadcrumb-item active">{{ object.inventory_item.item_name }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">Stock Details - {{ object.item.name }}</h1>
|
||||
<h1 class="page-header mb-0">Stock Details - {{ object.inventory_item.item_name }}</h1>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'inventory:stock_form' object.pk %}" class="btn btn-primary">
|
||||
<a href="{% url 'inventory:stock_update' object.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-edit me-2"></i>Update Stock
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||
@ -35,7 +35,7 @@
|
||||
<li><a class="dropdown-item" href="#" onclick="generateReport()">
|
||||
<i class="fas fa-chart-line me-2"></i>Generate Report
|
||||
</a></li>
|
||||
<li><a class="dropdown-item text-danger" href="{% url 'inventory:stock_confirm_delete' object.pk %}">
|
||||
<li><a class="dropdown-item text-danger" href="">
|
||||
<i class="fas fa-trash me-2"></i>Delete Stock Record
|
||||
</a></li>
|
||||
</ul>
|
||||
@ -71,18 +71,18 @@
|
||||
<tr>
|
||||
<td class="fw-bold">Item:</td>
|
||||
<td>
|
||||
<a href="{% url 'inventory:item_detail' object.item.pk %}">
|
||||
{{ object.item.name }}
|
||||
<a href="{% url 'inventory:item_detail' object.inventory_item.pk %}">
|
||||
{{ object.inventory_item.item_name }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">SKU:</td>
|
||||
<td>{{ object.item.sku }}</td>
|
||||
<td>{{ object.inventory_item.item_code }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Category:</td>
|
||||
<td>{{ object.item.category.name }}</td>
|
||||
<td>{{ object.inventory_item.get_category_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Location:</td>
|
||||
@ -91,10 +91,10 @@
|
||||
<tr>
|
||||
<td class="fw-bold">Current Quantity:</td>
|
||||
<td>
|
||||
<span class="fs-4 fw-bold {% if object.current_quantity <= object.minimum_quantity %}text-danger{% elif object.current_quantity <= object.reorder_level %}text-warning{% else %}text-success{% endif %}">
|
||||
{{ object.current_quantity }}
|
||||
<span class="fs-4 fw-bold {% if object.quantity_available <= object.inventory_item.min_stock_level %}text-danger{% elif object.quantity_available <= object.inventory_item.reorder_point %}text-warning{% else %}text-success{% endif %}">
|
||||
{{ object.quantity_available }}
|
||||
</span>
|
||||
{{ object.item.unit_of_measure }}
|
||||
{{ object.inventory_item.unit_of_measure }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -103,25 +103,25 @@
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<td class="fw-bold">Minimum Quantity:</td>
|
||||
<td>{{ object.minimum_quantity }} {{ object.item.unit_of_measure }}</td>
|
||||
<td>{{ object.inventory_item.min_stock_level }} {{ object.inventory_item.unit_of_measure }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Reorder Level:</td>
|
||||
<td>{{ object.reorder_level }} {{ object.item.unit_of_measure }}</td>
|
||||
<td>{{ object.inventory_item.reorder_point }} {{ object.inventory_item.unit_of_measure }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Maximum Quantity:</td>
|
||||
<td>{{ object.maximum_quantity|default:"Not set" }} {{ object.item.unit_of_measure }}</td>
|
||||
<td>{{ object.inventory_item.max_stock_level|default:"Not set" }} {{ object.item.unit_of_measure }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Unit Cost:</td>
|
||||
<td>${{ object.unit_cost|floatformat:2 }}</td>
|
||||
<td><span class="symbol">ê</span>{{ object.unit_cost|floatformat:'2g' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Total Value:</td>
|
||||
<td>
|
||||
<span class="fs-5 fw-bold text-primary">
|
||||
${{ object.total_value|floatformat:2 }}
|
||||
<span class="symbol">ê</span>{{ object.total_cost|floatformat:'2g' }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
@ -133,22 +133,22 @@
|
||||
<div class="mt-3">
|
||||
<div class="d-flex justify-content-between mb-1">
|
||||
<span>Stock Level</span>
|
||||
<span>{{ object.current_quantity }} / {{ object.maximum_quantity|default:"∞" }} {{ object.item.unit_of_measure }}</span>
|
||||
<span>{{ object.quantity_available }} / {{ object.inventory_item.min_stock_level }} {{ object.inventory_item.unit_of_measure }}</span>
|
||||
</div>
|
||||
<div class="progress">
|
||||
{% if object.maximum_quantity %}
|
||||
{% widthratio object.current_quantity object.maximum_quantity 100 as percentage %}
|
||||
{% if object.inventory_item.max_stock_level %}
|
||||
{% widthratio object.quantity_available object.inventory_item.max_stock_level 100 as percentage %}
|
||||
{% else %}
|
||||
{% widthratio object.current_quantity object.reorder_level 100 as percentage %}
|
||||
{% widthratio object.quantity_available object.inventory_item.min_stock_level 100 as percentage %}
|
||||
{% endif %}
|
||||
<div class="progress-bar bg-{% if object.current_quantity <= object.minimum_quantity %}danger{% elif object.current_quantity <= object.reorder_level %}warning{% else %}success{% endif %}"
|
||||
<div class="progress-bar bg-{% if object.quantity_available <= object.inventory_item.min_stock_level %}danger{% elif object.quantity_available <= object.inventory_item.reorder_point %}warning{% else %}success{% endif %}"
|
||||
style="width: {{ percentage|default:0 }}%"></div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between mt-1">
|
||||
<small class="text-danger">Min: {{ object.minimum_quantity }}</small>
|
||||
<small class="text-warning">Reorder: {{ object.reorder_level }}</small>
|
||||
<small class="text-danger">Min: {{ object.inventory_item.min_stock_level }}</small>
|
||||
<small class="text-warning">Reorder: {{ object.inventory_item.reorder_point }}</small>
|
||||
{% if object.maximum_quantity %}
|
||||
<small class="text-success">Max: {{ object.maximum_quantity }}</small>
|
||||
<small class="text-success">Max: {{ object.inventory_item.max_stock_level }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
{% block title %}Inventory Stock - Inventory Management{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'assets/plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -29,10 +29,10 @@
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Stock Management</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="{% url 'inventory:stock_adjustment_create' %}" class="btn btn-xs btn-warning me-2">
|
||||
<a href="{% url 'inventory:stock_create' %}" class="btn btn-xs btn-warning me-2">
|
||||
<i class="fa fa-edit"></i> Stock Adjustment
|
||||
</a>
|
||||
<a href="{% url 'inventory:stock_count_create' %}" class="btn btn-xs btn-info me-2">
|
||||
<a href="{% url 'inventory:stock_create' %}" class="btn btn-xs btn-info me-2">
|
||||
<i class="fa fa-clipboard-list"></i> Physical Count
|
||||
</a>
|
||||
<button class="btn btn-xs btn-primary me-2" data-bs-toggle="modal" data-bs-target="#stock-alerts-modal">
|
||||
@ -156,7 +156,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for stock in object_list %}
|
||||
<tr data-stock-status="{{ stock.stock_status }}" data-location="{{ stock.location.id }}" data-category="{{ stock.item.category }}">
|
||||
<tr data-stock-status="{{ stock.stock_status }}" data-location="{{ stock.location.id }}" data-category="{{ stock.inventory_item.category }}">
|
||||
<td>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input row-checkbox" type="checkbox" value="{{ stock.id }}">
|
||||
@ -164,16 +164,16 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
{% if stock.item.image %}
|
||||
<img src="{{ stock.item.image.url }}" alt="{{ stock.item.name }}" class="rounded me-2" width="40" height="40">
|
||||
{% if stock.inventory_item.image %}
|
||||
<img src="{{ stock.inventory_item.image.url }}" alt="{{ stock.inventory_item.name }}" class="rounded me-2" width="40" height="40">
|
||||
{% else %}
|
||||
<div class="bg-light rounded me-2 d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||
<i class="fa fa-box text-muted"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div class="fw-bold">{{ stock.item.name }}</div>
|
||||
<div class="small text-muted">{{ stock.item.code }}</div>
|
||||
<div class="fw-bold">{{ stock.inventory_item.name }}</div>
|
||||
<div class="small text-muted">{{ stock.inventory_item.code }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -186,7 +186,7 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">{{ stock.quantity_on_hand }}</div>
|
||||
<div class="small text-muted">{{ stock.item.unit_of_measure }}</div>
|
||||
<div class="small text-muted">{{ stock.inventory_item.unit_of_measure }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-bold">{{ stock.available_quantity }}</div>
|
||||
@ -282,7 +282,7 @@
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% include 'inventory/partials/stock_alerts.html' %}
|
||||
{% include 'inventory/stock_alerts.html' %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -346,12 +346,14 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'assets/plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive/js/dataTables.responsive.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/datatables.net-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/dropzone/src/options.js' %}"></script>
|
||||
|
||||
<script>
|
||||
|
||||
$(document).ready(function() {
|
||||
// Initialize DataTable
|
||||
var table = $('#stock-table').DataTable({
|
||||
@ -402,7 +404,7 @@ $(document).ready(function() {
|
||||
e.preventDefault();
|
||||
|
||||
$.ajax({
|
||||
url: '{% url "inventory:stock_quick_adjust" %}',
|
||||
url: '{% url "inventory:stock_create" %}',
|
||||
method: 'POST',
|
||||
data: $(this).serialize(),
|
||||
success: function(response) {
|
||||
@ -480,10 +482,10 @@ function quickAdjust(stockId) {
|
||||
$('#quick-adjust-modal').modal('show');
|
||||
}
|
||||
|
||||
function moveStock(stockId) {
|
||||
// Implement stock movement functionality
|
||||
window.location.href = '{% url "inventory:stock_move" 0 %}'.replace('0', stockId);
|
||||
}
|
||||
{#function moveStock(stockId) {#}
|
||||
{# // Implement stock movement functionality#}
|
||||
{# window.location.href = '{% url "inventory:stock_move" 0 %}'.replace('0', stockId);#}
|
||||
{# }#}
|
||||
|
||||
function bulkAdjust() {
|
||||
var selectedIds = $('.row-checkbox:checked').map(function() {
|
||||
@ -496,7 +498,7 @@ function bulkAdjust() {
|
||||
}
|
||||
|
||||
// Redirect to bulk adjustment page
|
||||
window.location.href = '{% url "inventory:stock_bulk_adjust" %}?ids=' + selectedIds.join(',');
|
||||
window.location.href = '?ids=' + selectedIds.join(',');
|
||||
}
|
||||
|
||||
function bulkMove() {
|
||||
@ -510,7 +512,7 @@ function bulkMove() {
|
||||
}
|
||||
|
||||
// Redirect to bulk move page
|
||||
window.location.href = '{% url "inventory:stock_bulk_move" %}?ids=' + selectedIds.join(',');
|
||||
window.location.href = '?ids=' + selectedIds.join(',');
|
||||
}
|
||||
|
||||
function bulkReorder() {
|
||||
@ -532,9 +534,9 @@ function clearSelection() {
|
||||
$('#bulk-actions').hide();
|
||||
}
|
||||
|
||||
function exportStock() {
|
||||
window.open('{% url "inventory:stock_export" %}');
|
||||
}
|
||||
{#function exportStock() {#}
|
||||
{# window.open('{% url "inventory:stock_export" %}');#}
|
||||
{# }#}
|
||||
|
||||
// Search on enter key
|
||||
$('#search-input').on('keypress', function(e) {
|
||||
|
||||
BIN
templates/radiology/.DS_Store
vendored
BIN
templates/radiology/.DS_Store
vendored
Binary file not shown.
@ -68,7 +68,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Study Type:</td>
|
||||
<td>{{ object.study_type }}</td>
|
||||
<td>{{ object.study_description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Body Part:</td>
|
||||
@ -83,27 +83,31 @@
|
||||
<tr>
|
||||
<td class="fw-bold">Priority:</td>
|
||||
<td>
|
||||
{% if object.priority == 'stat' %}
|
||||
<span class="badge bg-danger">STAT</span>
|
||||
{% elif object.priority == 'urgent' %}
|
||||
<span class="badge bg-warning text-dark">Urgent</span>
|
||||
{% elif object.priority == 'routine' %}
|
||||
{% if object.priority == 'STAT' %}
|
||||
<span class="badge bg-red">STAT</span>
|
||||
{% elif object.priority == 'URGENT' %}
|
||||
<span class="badge bg-warning">Urgent</span>
|
||||
{% elif object.priority == 'ROUTINE' %}
|
||||
<span class="badge bg-success">Routine</span>
|
||||
{% elif object.priority == 'EMERGENCY' %}
|
||||
<span class="badge bg-danger">Emergency</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="fw-bold">Status:</td>
|
||||
<td>
|
||||
{% if object.status == 'pending' %}
|
||||
{% if object.status == 'PENDING' %}
|
||||
<span class="badge bg-secondary">Pending</span>
|
||||
{% elif object.status == 'scheduled' %}
|
||||
{% elif object.status == 'SCHEDULED' %}
|
||||
<span class="badge bg-info">Scheduled</span>
|
||||
{% elif object.status == 'in_progress' %}
|
||||
{% elif object.status == 'IN_PROGRESS' %}
|
||||
<span class="badge bg-warning text-dark">In Progress</span>
|
||||
{% elif object.status == 'completed' %}
|
||||
{% elif object.status == 'ON_HOLD' %}
|
||||
<span class="badge bg-danger">On Hold</span>
|
||||
{% elif object.status == 'COMPLETED' %}
|
||||
<span class="badge bg-success">Completed</span>
|
||||
{% elif object.status == 'cancelled' %}
|
||||
{% elif object.status == 'CANCELLED' %}
|
||||
<span class="badge bg-danger">Cancelled</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
@ -317,17 +321,17 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Reports -->
|
||||
{% if object.radiology_reports.exists %}
|
||||
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Radiology Reports</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% for report in object.radiology_reports.all %}
|
||||
{% for report in reports %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h6 class="mb-0">Report #{{ report.report_number }}</h6>
|
||||
<h6 class="mb-0">Report #{{ report }}</h6>
|
||||
<small class="text-muted">{{ report.created_at|date:"M d, Y g:i A" }}</small>
|
||||
</div>
|
||||
<div>
|
||||
@ -350,7 +354,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<a href="{% url 'radiology:radiology_report_detail' report.pk %}"
|
||||
<a href=""
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fa fa-eye me-1"></i>View Full Report
|
||||
</a>
|
||||
@ -365,7 +369,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-xl-4">
|
||||
|
||||
@ -106,15 +106,15 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info">{{ order.modality }}</span>
|
||||
<span class="badge bg-info">{{ order.modality }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-{% if order.priority == 'URGENT' %}danger{% elif order.priority == 'HIGH' %}warning{% else %}secondary{% endif %}">
|
||||
<span class="badge bg-{% if order.priority == 'URGENT' %}danger{% elif order.priority == 'HIGH' %}warning{% else %}secondary{% endif %}">
|
||||
{{ order.get_priority_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-{% if order.status == 'PENDING' %}warning{% elif order.status == 'SCHEDULED' %}info{% elif order.status == 'IN_PROGRESS' %}primary{% elif order.status == 'COMPLETED' %}success{% else %}secondary{% endif %}">
|
||||
<span class="badge bg-{% if order.status == 'PENDING' %}warning{% elif order.status == 'SCHEDULED' %}info{% elif order.status == 'IN_PROGRESS' %}primary{% elif order.status == 'COMPLETED' %}success{% else %}secondary{% endif %}">
|
||||
{{ order.get_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
634
templates/radiology/series/imaging_series_list.html
Normal file
634
templates/radiology/series/imaging_series_list.html
Normal file
@ -0,0 +1,634 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Imaging Series - {{ study.study_description|default:"Study" }} - Hospital Management{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="content">
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h4>Imaging Series</h4>
|
||||
<h6>Study: {{ study.study_description|default:"Imaging Study" }}</h6>
|
||||
</div>
|
||||
<div class="page-btn">
|
||||
<a href="{% url 'radiology:imaging_study_detail' study.pk %}" class="btn btn-secondary me-2">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Study
|
||||
</a>
|
||||
<a href="{% url 'radiology:imaging_series_create' study.pk %}" class="btn btn-added">
|
||||
<i class="fas fa-plus me-1"></i>Add Series
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Study Information -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-info-circle me-2"></i>Study Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="info-group">
|
||||
<label class="form-label">Patient:</label>
|
||||
<p class="fw-bold">{{ study.patient.get_full_name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="info-group">
|
||||
<label class="form-label">Accession Number:</label>
|
||||
<p>{{ study.accession_number }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="info-group">
|
||||
<label class="form-label">Study Date:</label>
|
||||
<p>{{ study.study_date|date:"M d, Y" }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="info-group">
|
||||
<label class="form-label">Modality:</label>
|
||||
<p><span class="badge bg-primary">{{ study.get_modality_display }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Series Statistics -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="dash-widget">
|
||||
<div class="dash-widgetimg">
|
||||
<span><i class="fas fa-layer-group"></i></span>
|
||||
</div>
|
||||
<div class="dash-widgetcontent">
|
||||
<h5 id="total-series">{{ series.count }}</h5>
|
||||
<h6>Total Series</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="dash-widget">
|
||||
<div class="dash-widgetimg">
|
||||
<span><i class="fas fa-images"></i></span>
|
||||
</div>
|
||||
<div class="dash-widgetcontent">
|
||||
<h5 id="total-images">{{ total_images|default:0 }}</h5>
|
||||
<h6>Total Images</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="dash-widget">
|
||||
<div class="dash-widgetimg">
|
||||
<span><i class="fas fa-check-circle"></i></span>
|
||||
</div>
|
||||
<div class="dash-widgetcontent">
|
||||
<h5 id="completed-series">{{ completed_series|default:0 }}</h5>
|
||||
<h6>Completed Series</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="dash-widget">
|
||||
<div class="dash-widgetimg">
|
||||
<span><i class="fas fa-clock"></i></span>
|
||||
</div>
|
||||
<div class="dash-widgetcontent">
|
||||
<h5 id="processing-series">{{ processing_series|default:0 }}</h5>
|
||||
<h6>Processing</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters and Search -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-end">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Search Series:</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="search-input"
|
||||
placeholder="Search by description, protocol...">
|
||||
<button class="btn btn-outline-secondary" type="button" id="search-btn">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Modality:</label>
|
||||
<select class="form-select" id="modality-filter">
|
||||
<option value="">All Modalities</option>
|
||||
<option value="CT">CT</option>
|
||||
<option value="MR">MR</option>
|
||||
<option value="US">US</option>
|
||||
<option value="DX">DX</option>
|
||||
<option value="CR">CR</option>
|
||||
<option value="XA">XA</option>
|
||||
<option value="RF">RF</option>
|
||||
<option value="MG">MG</option>
|
||||
<option value="NM">NM</option>
|
||||
<option value="PT">PT</option>
|
||||
<option value="OT">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Status:</label>
|
||||
<select class="form-select" id="status-filter">
|
||||
<option value="">All Status</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="processing">Processing</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Date Range:</label>
|
||||
<input type="date" class="form-control" id="date-filter">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-group">
|
||||
<button class="btn btn-outline-secondary me-2" onclick="clearFilters()">
|
||||
<i class="fas fa-times me-1"></i>Clear
|
||||
</button>
|
||||
<button class="btn btn-primary" onclick="applyFilters()">
|
||||
<i class="fas fa-filter me-1"></i>Filter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Series List -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-layer-group me-2"></i>Imaging Series
|
||||
</h5>
|
||||
<div class="card-tools">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="toggleView('grid')" id="grid-view-btn">
|
||||
<i class="fas fa-th"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm active" onclick="toggleView('list')" id="list-view-btn">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Grid View -->
|
||||
<div id="grid-view" class="d-none">
|
||||
<div class="row" id="series-grid">
|
||||
{% for series in series %}
|
||||
<div class="col-lg-4 col-md-6 col-12 mb-4 series-card"
|
||||
data-modality="{{ series.modality }}"
|
||||
data-status="{{ series.status|default:'completed' }}"
|
||||
data-date="{{ series.series_date|date:'Y-m-d' }}">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="card-title mb-0">
|
||||
Series {{ series.series_number }}
|
||||
</h6>
|
||||
<span class="badge bg-{{ series.get_status_color|default:'success' }}">
|
||||
{{ series.get_status_display|default:'Completed' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="series-info">
|
||||
<div class="info-row">
|
||||
<span class="label">Description:</span>
|
||||
<span class="value">{{ series.series_description|default:"No description" }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Modality:</span>
|
||||
<span class="value">
|
||||
<span class="badge bg-primary">{{ series.get_modality_display }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Protocol:</span>
|
||||
<span class="value">{{ series.protocol_name|default:"N/A" }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Images:</span>
|
||||
<span class="value">{{ series.number_of_images|default:0 }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Date/Time:</span>
|
||||
<span class="value">{{ series.series_datetime|date:"M d, Y H:i" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="btn-group w-100">
|
||||
<a href="{% url 'radiology:imaging_series_detail' series.pk %}"
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-eye me-1"></i>View
|
||||
</a>
|
||||
<a href="{% url 'radiology:imaging_series_edit' series.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button class="btn btn-outline-info btn-sm"
|
||||
onclick="viewImages('{{ series.pk }}')">
|
||||
<i class="fas fa-images me-1"></i>Images
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-layer-group fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No Series Found</h5>
|
||||
<p class="text-muted">No imaging series found for this study.</p>
|
||||
<a href="{% url 'radiology:imaging_series_create' study.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add First Series
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List View -->
|
||||
<div id="list-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover" id="series-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Series #</th>
|
||||
<th>Description</th>
|
||||
<th>Modality</th>
|
||||
<th>Protocol</th>
|
||||
<th>Images</th>
|
||||
<th>Date/Time</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for series in series %}
|
||||
<tr class="series-row"
|
||||
data-modality="{{ series.modality }}"
|
||||
data-status="{{ series.status|default:'completed' }}"
|
||||
data-date="{{ series.series_date|date:'Y-m-d' }}">
|
||||
<td>
|
||||
<span class="fw-bold">{{ series.series_number }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="series-desc">
|
||||
<div class="fw-bold">{{ series.series_description|default:"No description" }}</div>
|
||||
<small class="text-muted">{{ series.series_instance_uid|truncatechars:30 }}</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">{{ series.get_modality_display }}</span>
|
||||
</td>
|
||||
<td>{{ series.protocol_name|default:"N/A" }}</td>
|
||||
<td>
|
||||
<span class="fw-bold">{{ series.number_of_images|default:0 }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ series.series_date|date:"M d, Y" }}</div>
|
||||
<small class="text-muted">{{ series.series_time|time:"H:i" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-{{ series.get_status_color|default:'success' }}">
|
||||
{{ series.get_status_display|default:'Completed' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'radiology:imaging_series_detail' series.pk %}"
|
||||
class="btn btn-outline-primary btn-sm" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'radiology:imaging_series_edit' series.pk %}"
|
||||
class="btn btn-outline-secondary btn-sm" title="Edit Series">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button class="btn btn-outline-info btn-sm"
|
||||
onclick="viewImages('{{ series.pk }}')" title="View Images">
|
||||
<i class="fas fa-images"></i>
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline-secondary btn-sm dropdown-toggle"
|
||||
data-bs-toggle="dropdown">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{% url 'radiology:imaging_series_download' series.pk %}">
|
||||
<i class="fas fa-download me-2"></i>Download DICOM
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="exportSeries('{{ series.pk }}')">
|
||||
<i class="fas fa-file-export me-2"></i>Export
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item text-danger" href="{% url 'radiology:imaging_series_delete' series.pk %}">
|
||||
<i class="fas fa-trash me-2"></i>Delete
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" class="text-center py-5">
|
||||
<i class="fas fa-layer-group fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No Series Found</h5>
|
||||
<p class="text-muted">No imaging series found for this study.</p>
|
||||
<a href="{% url 'radiology:imaging_series_create' study.pk %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add First Series
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<div class="pagination-info">
|
||||
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} series
|
||||
</div>
|
||||
<nav>
|
||||
<ul class="pagination mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1">« First</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active">
|
||||
<span class="page-link">{{ num }}</span>
|
||||
</li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last »</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Initialize view
|
||||
toggleView('list');
|
||||
|
||||
// Search functionality
|
||||
$('#search-btn').on('click', function() {
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
$('#search-input').on('keypress', function(e) {
|
||||
if (e.which === 13) {
|
||||
applyFilters();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function toggleView(viewType) {
|
||||
if (viewType === 'grid') {
|
||||
$('#list-view').addClass('d-none');
|
||||
$('#grid-view').removeClass('d-none');
|
||||
$('#list-view-btn').removeClass('active');
|
||||
$('#grid-view-btn').addClass('active');
|
||||
} else {
|
||||
$('#grid-view').addClass('d-none');
|
||||
$('#list-view').removeClass('d-none');
|
||||
$('#grid-view-btn').removeClass('active');
|
||||
$('#list-view-btn').addClass('active');
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const searchTerm = $('#search-input').val().toLowerCase();
|
||||
const modalityFilter = $('#modality-filter').val();
|
||||
const statusFilter = $('#status-filter').val();
|
||||
const dateFilter = $('#date-filter').val();
|
||||
|
||||
// Filter grid view
|
||||
$('.series-card').each(function() {
|
||||
const card = $(this);
|
||||
const description = card.find('.series-info').text().toLowerCase();
|
||||
const modality = card.data('modality');
|
||||
const status = card.data('status');
|
||||
const date = card.data('date');
|
||||
|
||||
let show = true;
|
||||
|
||||
if (searchTerm && !description.includes(searchTerm)) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
if (modalityFilter && modality !== modalityFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
if (statusFilter && status !== statusFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
if (dateFilter && date !== dateFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
card.toggle(show);
|
||||
});
|
||||
|
||||
// Filter list view
|
||||
$('.series-row').each(function() {
|
||||
const row = $(this);
|
||||
const description = row.find('.series-desc').text().toLowerCase();
|
||||
const modality = row.data('modality');
|
||||
const status = row.data('status');
|
||||
const date = row.data('date');
|
||||
|
||||
let show = true;
|
||||
|
||||
if (searchTerm && !description.includes(searchTerm)) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
if (modalityFilter && modality !== modalityFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
if (statusFilter && status !== statusFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
if (dateFilter && date !== dateFilter) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
row.toggle(show);
|
||||
});
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
$('#search-input').val('');
|
||||
$('#modality-filter').val('');
|
||||
$('#status-filter').val('');
|
||||
$('#date-filter').val('');
|
||||
|
||||
$('.series-card, .series-row').show();
|
||||
}
|
||||
|
||||
function viewImages(seriesId) {
|
||||
// Open image viewer for series
|
||||
window.open(`/radiology/series/${seriesId}/images/`, '_blank');
|
||||
}
|
||||
|
||||
function exportSeries(seriesId) {
|
||||
// Export series functionality
|
||||
if (confirm('Export this imaging series?')) {
|
||||
window.location.href = `/radiology/series/${seriesId}/export/`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.info-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.info-group .form-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-group p {
|
||||
margin-bottom: 0;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.series-info .info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
}
|
||||
|
||||
.series-info .info-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.series-info .label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
flex: 0 0 40%;
|
||||
}
|
||||
|
||||
.series-info .value {
|
||||
color: #6c757d;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.series-desc {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.card-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-group .btn.active {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.series-info .info-row {
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.series-info .value {
|
||||
text-align: left;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user