This commit is contained in:
Marwan Alwali 2025-08-13 19:31:08 +03:00
parent 1992c3359d
commit 6b85b05882
28 changed files with 32075 additions and 1087 deletions

View File

@ -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'

Binary file not shown.

View File

@ -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 = {

View File

@ -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')),

View File

@ -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(

View File

@ -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', {

View File

@ -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,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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
View 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

Binary file not shown.

View File

@ -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>

View File

@ -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') {

View File

@ -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">&#xea;</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">&#xea;</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>

View File

@ -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) {

Binary file not shown.

View File

@ -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">

View File

@ -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>

View 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">&laquo; 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 &raquo;</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 %}