update
This commit is contained in:
parent
193ee7f34a
commit
25f548825b
0
core/templatetags/__init__.py
Normal file
0
core/templatetags/__init__.py
Normal file
BIN
core/templatetags/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
core/templatetags/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
core/templatetags/__pycache__/custom_filters.cpython-312.pyc
Normal file
BIN
core/templatetags/__pycache__/custom_filters.cpython-312.pyc
Normal file
Binary file not shown.
322
core/templatetags/custom_filters.py
Normal file
322
core/templatetags/custom_filters.py
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
from django import template
|
||||||
|
from django.db.models import Sum, F
|
||||||
|
from decimal import Decimal
|
||||||
|
import operator
|
||||||
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def mul(value, arg):
|
||||||
|
return value * arg
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def div(value, arg):
|
||||||
|
return value / arg if arg != 0 else 0
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="add_class")
|
||||||
|
def add_class(field, css_class):
|
||||||
|
return field.as_widget(attrs={"class": css_class})
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def sum_values(queryset, field_names):
|
||||||
|
"""
|
||||||
|
Calculate the sum of multiplied values from specified fields.
|
||||||
|
|
||||||
|
Usage: {{ category_group.list|sum_values:"current_stock,item.current_cost" }}
|
||||||
|
|
||||||
|
This will multiply current_stock * item.current_cost for each object
|
||||||
|
and return the sum of all results.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queryset: List or queryset of objects
|
||||||
|
field_names: Comma-separated string of field names to multiply
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Sum of all multiplied values
|
||||||
|
"""
|
||||||
|
if not queryset or not field_names:
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
try:
|
||||||
|
fields = [field.strip() for field in field_names.split(',')]
|
||||||
|
total = Decimal('0.00')
|
||||||
|
|
||||||
|
for obj in queryset:
|
||||||
|
values = []
|
||||||
|
for field_path in fields:
|
||||||
|
# Handle nested field access (e.g., "item.current_cost")
|
||||||
|
value = obj
|
||||||
|
for field_part in field_path.split('.'):
|
||||||
|
if hasattr(value, field_part):
|
||||||
|
value = getattr(value, field_part)
|
||||||
|
else:
|
||||||
|
value = 0
|
||||||
|
break
|
||||||
|
|
||||||
|
# Convert to Decimal for precise calculations
|
||||||
|
if value is None:
|
||||||
|
value = 0
|
||||||
|
values.append(Decimal(str(value)))
|
||||||
|
|
||||||
|
# Multiply all values together
|
||||||
|
if values:
|
||||||
|
product = reduce(operator.mul, values, Decimal('1'))
|
||||||
|
total += product
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def multiply(value, arg):
|
||||||
|
"""
|
||||||
|
Multiply two values.
|
||||||
|
|
||||||
|
Usage: {{ inventory.current_stock|multiply:inventory.item.current_cost }}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: First value to multiply
|
||||||
|
arg: Second value to multiply
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Product of the two values
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if value is None or arg is None:
|
||||||
|
return Decimal('0.00')
|
||||||
|
return Decimal(str(value)) * Decimal(str(arg))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def divide(value, arg):
|
||||||
|
"""
|
||||||
|
Divide two values.
|
||||||
|
|
||||||
|
Usage: {{ inventory.current_stock|divide:inventory.item.reorder_point }}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Dividend
|
||||||
|
arg: Divisor
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Result of division, or 0 if divisor is 0
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if value is None or arg is None or Decimal(str(arg)) == 0:
|
||||||
|
return Decimal('0.00')
|
||||||
|
return Decimal(str(value)) / Decimal(str(arg))
|
||||||
|
except (ValueError, TypeError, ZeroDivisionError):
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def percentage(value, total):
|
||||||
|
"""
|
||||||
|
Calculate percentage of value relative to total.
|
||||||
|
|
||||||
|
Usage: {{ current_stock|percentage:max_stock }}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Current value
|
||||||
|
total: Total value (100%)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Percentage (0-100)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if value is None or total is None or Decimal(str(total)) == 0:
|
||||||
|
return Decimal('0.00')
|
||||||
|
return (Decimal(str(value)) / Decimal(str(total))) * 100
|
||||||
|
except (ValueError, TypeError, ZeroDivisionError):
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def currency_format(value, currency='SAR'):
|
||||||
|
"""
|
||||||
|
Format value as currency.
|
||||||
|
|
||||||
|
Usage: {{ total_value|currency_format:"SAR" }}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Numeric value to format
|
||||||
|
currency: Currency symbol/code
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted currency string
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
value = 0
|
||||||
|
formatted_value = "{:,.2f}".format(float(value))
|
||||||
|
return f"{currency} {formatted_value}"
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return f"{currency} 0.00"
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def sum_field(queryset, field_name):
|
||||||
|
"""
|
||||||
|
Sum a specific field from a queryset.
|
||||||
|
|
||||||
|
Usage: {{ inventory_items|sum_field:"current_stock" }}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queryset: List or queryset of objects
|
||||||
|
field_name: Name of the field to sum
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Sum of the field values
|
||||||
|
"""
|
||||||
|
if not queryset or not field_name:
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
try:
|
||||||
|
total = Decimal('0.00')
|
||||||
|
for obj in queryset:
|
||||||
|
# Handle nested field access
|
||||||
|
value = obj
|
||||||
|
for field_part in field_name.split('.'):
|
||||||
|
if hasattr(value, field_part):
|
||||||
|
value = getattr(value, field_part)
|
||||||
|
else:
|
||||||
|
value = 0
|
||||||
|
break
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
total += Decimal(str(value))
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def count_by_condition(queryset, condition):
|
||||||
|
"""
|
||||||
|
Count objects that meet a specific condition.
|
||||||
|
|
||||||
|
Usage: {{ inventory_items|count_by_condition:"current_stock__lte=reorder_point" }}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queryset: List or queryset of objects
|
||||||
|
condition: Condition string to evaluate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Count of objects meeting the condition
|
||||||
|
"""
|
||||||
|
if not queryset:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
count = 0
|
||||||
|
for obj in queryset:
|
||||||
|
# Simple condition checking for stock levels
|
||||||
|
if condition == 'low_stock':
|
||||||
|
if hasattr(obj, 'current_stock') and hasattr(obj, 'item'):
|
||||||
|
if obj.current_stock <= getattr(obj.item, 'reorder_point', 0):
|
||||||
|
count += 1
|
||||||
|
elif condition == 'out_of_stock':
|
||||||
|
if hasattr(obj, 'current_stock'):
|
||||||
|
if obj.current_stock <= 0:
|
||||||
|
count += 1
|
||||||
|
elif condition == 'high_stock':
|
||||||
|
if hasattr(obj, 'current_stock') and hasattr(obj, 'item'):
|
||||||
|
reorder_point = getattr(obj.item, 'reorder_point', 0)
|
||||||
|
if obj.current_stock > reorder_point * 2:
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def get_item(dictionary, key):
|
||||||
|
"""
|
||||||
|
Get item from dictionary by key.
|
||||||
|
|
||||||
|
Usage: {{ my_dict|get_item:key_variable }}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dictionary: Dictionary to access
|
||||||
|
key: Key to look up
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Any: Value from dictionary or None
|
||||||
|
"""
|
||||||
|
if isinstance(dictionary, dict):
|
||||||
|
return dictionary.get(key)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def format_number(value, decimal_places=2):
|
||||||
|
"""
|
||||||
|
Format number with thousand separators.
|
||||||
|
|
||||||
|
Usage: {{ large_number|format_number:0 }}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value: Number to format
|
||||||
|
decimal_places: Number of decimal places
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Formatted number string
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if value is None:
|
||||||
|
return "0"
|
||||||
|
|
||||||
|
if decimal_places == 0:
|
||||||
|
return "{:,}".format(int(float(value)))
|
||||||
|
else:
|
||||||
|
format_str = "{:,.{}f}".format(decimal_places)
|
||||||
|
return format_str.format(float(value))
|
||||||
|
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return "0"
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def calculate_stock_value(inventory_items):
|
||||||
|
"""
|
||||||
|
Calculate total stock value for a list of inventory items.
|
||||||
|
|
||||||
|
Usage: {% calculate_stock_value inventory_items as total_value %}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
inventory_items: List of inventory objects
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Total stock value
|
||||||
|
"""
|
||||||
|
if not inventory_items:
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
try:
|
||||||
|
total = Decimal('0.00')
|
||||||
|
for item in inventory_items:
|
||||||
|
if hasattr(item, 'current_stock') and hasattr(item, 'item'):
|
||||||
|
stock = Decimal(str(item.current_stock or 0))
|
||||||
|
cost = Decimal(str(getattr(item.item, 'current_cost', 0) or 0))
|
||||||
|
total += stock * cost
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
except (ValueError, TypeError, AttributeError):
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
|
||||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -37,6 +37,7 @@ urlpatterns = [
|
|||||||
path('patient/<int:patient_id>/problem/add/', views.add_problem, name='add_problem'),
|
path('patient/<int:patient_id>/problem/add/', views.add_problem, name='add_problem'),
|
||||||
path('encounter/<int:encounter_id>/status/', views.update_encounter_status, name='update_encounter_status'),
|
path('encounter/<int:encounter_id>/status/', views.update_encounter_status, name='update_encounter_status'),
|
||||||
path('note/<int:note_id>/sign/', views.sign_note, name='sign_note'),
|
path('note/<int:note_id>/sign/', views.sign_note, name='sign_note'),
|
||||||
|
path('problem/<int:problem_id>/resolve/', views.resolve_problem, name='resolve_problem'),
|
||||||
|
|
||||||
# API endpoints
|
# API endpoints
|
||||||
# path('api/', include('emr.api.urls')),
|
# path('api/', include('emr.api.urls')),
|
||||||
|
|||||||
30
emr/views.py
30
emr/views.py
@ -459,7 +459,7 @@ class ClinicalNoteDeleteView(
|
|||||||
SuccessMessageMixin, DeleteView
|
SuccessMessageMixin, DeleteView
|
||||||
):
|
):
|
||||||
model = ClinicalNote
|
model = ClinicalNote
|
||||||
template_name = 'emr/clinical_note_confirm_delete.html'
|
template_name = 'emr/clinical_notes/clinical_note_confirm_delete.html'
|
||||||
success_url = reverse_lazy('emr:clinical_note_list')
|
success_url = reverse_lazy('emr:clinical_note_list')
|
||||||
success_message = _('Clinical note deleted successfully.')
|
success_message = _('Clinical note deleted successfully.')
|
||||||
|
|
||||||
@ -1497,20 +1497,20 @@ def get_status_class(status):
|
|||||||
# return redirect('emr:encounter_detail', pk=pk)
|
# return redirect('emr:encounter_detail', pk=pk)
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# @login_required
|
@login_required
|
||||||
# def resolve_problem(request, pk):
|
def resolve_problem(request, pk):
|
||||||
# prob = get_object_or_404(ProblemList, pk=pk, patient__tenant=request.user.tenant)
|
prob = get_object_or_404(ProblemList, pk=pk, patient__tenant=request.user.tenant)
|
||||||
# if prob.status == 'ACTIVE':
|
if prob.status == 'ACTIVE':
|
||||||
# prob.status = 'RESOLVED'; prob.save()
|
prob.status = 'RESOLVED'; prob.save()
|
||||||
# AuditLogEntry.objects.create(
|
AuditLogEntry.objects.create(
|
||||||
# tenant=request.user.tenant, user=request.user,
|
tenant=request.user.tenant, user=request.user,
|
||||||
# action='UPDATE', model_name='ProblemList',
|
action='UPDATE', model_name='ProblemList',
|
||||||
# object_id=str(prob.pk), changes={'status': 'Problem resolved'}
|
object_id=str(prob.pk), changes={'status': 'Problem resolved'}
|
||||||
# )
|
)
|
||||||
# messages.success(request, _('Problem resolved.'))
|
messages.success(request, _('Problem resolved.'))
|
||||||
# else:
|
else:
|
||||||
# messages.error(request, _('Only active problems can be resolved.'))
|
messages.error(request, _('Only active problems can be resolved.'))
|
||||||
# return redirect('emr:problem_detail', pk=pk)
|
return redirect('emr:problem_detail', pk=pk)
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# @login_required
|
# @login_required
|
||||||
|
|||||||
Binary file not shown.
@ -278,9 +278,7 @@ class DepartmentListView(LoginRequiredMixin, ListView):
|
|||||||
Q(description__icontains=search)
|
Q(description__icontains=search)
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset.annotate(
|
return queryset
|
||||||
employee_count=Count('employees')
|
|
||||||
).order_by('name')
|
|
||||||
|
|
||||||
|
|
||||||
class DepartmentDetailView(LoginRequiredMixin, DetailView):
|
class DepartmentDetailView(LoginRequiredMixin, DetailView):
|
||||||
|
|||||||
Binary file not shown.
0
integration/management/__init__.py
Normal file
0
integration/management/__init__.py
Normal file
BIN
integration/management/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
integration/management/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
0
integration/management/commands/__init__.py
Normal file
0
integration/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
319
integration/management/commands/integration_data.py
Normal file
319
integration/management/commands/integration_data.py
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
# integration/management/commands/generate_integration_data.py
|
||||||
|
|
||||||
|
import random
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from faker import Faker
|
||||||
|
|
||||||
|
from integration.models import (
|
||||||
|
ExternalSystem, IntegrationEndpoint,
|
||||||
|
DataMapping, IntegrationExecution,
|
||||||
|
WebhookEndpoint, WebhookExecution,
|
||||||
|
IntegrationLog
|
||||||
|
)
|
||||||
|
from core.models import Tenant
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
fake = Faker('en_US')
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Generate sample data for the integration app"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Utility helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@staticmethod
|
||||||
|
def random_choice(field, exclude=None):
|
||||||
|
"""Return a random value from a Django field’s choices."""
|
||||||
|
choices = field.choices
|
||||||
|
if exclude:
|
||||||
|
choices = [c for c in choices if c[0] not in exclude]
|
||||||
|
return random.choice(choices)[0]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def random_bool(chance=0.5):
|
||||||
|
return random.random() < chance
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Main entry point
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
tenants = Tenant.objects.all()
|
||||||
|
if not tenants:
|
||||||
|
self.stdout.write(self.style.ERROR("No tenants found. Create tenants before generating data."))
|
||||||
|
return
|
||||||
|
|
||||||
|
for tenant in tenants:
|
||||||
|
self.generate_external_systems(tenant)
|
||||||
|
self.generate_webhooks(tenant)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS("Integration data generation complete."))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# External system + endpoints
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def generate_external_systems(self, tenant, count=5):
|
||||||
|
for _ in range(count):
|
||||||
|
system = ExternalSystem.objects.create(
|
||||||
|
tenant=tenant,
|
||||||
|
name=fake.company() + " " + self.random_choice(ExternalSystem.SYSTEM_TYPES,
|
||||||
|
exclude=['other']),
|
||||||
|
description=fake.text(max_nb_chars=200),
|
||||||
|
system_type=self.random_choice(ExternalSystem.SYSTEM_TYPES),
|
||||||
|
vendor=fake.company(),
|
||||||
|
version=f"{random.randint(1, 5)}.{random.randint(0, 9)}",
|
||||||
|
base_url=fake.url(),
|
||||||
|
host=fake.hostname(),
|
||||||
|
port=random.randint(1024, 65535),
|
||||||
|
authentication_type=self.random_choice(ExternalSystem.AUTHENTICATION_TYPES),
|
||||||
|
authentication_config=self._random_auth_config(),
|
||||||
|
configuration=self._random_config(),
|
||||||
|
timeout_seconds=random.randint(10, 60),
|
||||||
|
retry_attempts=random.randint(0, 5),
|
||||||
|
retry_delay_seconds=random.randint(1, 10),
|
||||||
|
is_active=self.random_bool(0.9),
|
||||||
|
is_healthy=self.random_bool(0.7),
|
||||||
|
health_check_interval=random.randint(120, 600),
|
||||||
|
connection_count=random.randint(0, 1000),
|
||||||
|
success_count=random.randint(0, 800),
|
||||||
|
failure_count=random.randint(0, 200),
|
||||||
|
last_used_at=timezone.now() - timedelta(days=random.randint(0, 30)),
|
||||||
|
created_by=User.objects.filter(is_staff=True).first()
|
||||||
|
)
|
||||||
|
self.generate_endpoints(system, count=3)
|
||||||
|
|
||||||
|
def generate_endpoints(self, system, count=3):
|
||||||
|
for _ in range(count):
|
||||||
|
endpoint = IntegrationEndpoint.objects.create(
|
||||||
|
external_system=system,
|
||||||
|
name=fake.word().title() + " API",
|
||||||
|
description=fake.text(max_nb_chars=150),
|
||||||
|
endpoint_type=self.random_choice(IntegrationEndpoint.ENDPOINT_TYPES),
|
||||||
|
path=f"/{fake.word()}/{fake.word()}",
|
||||||
|
method=self.random_choice(IntegrationEndpoint.METHODS),
|
||||||
|
direction=self.random_choice(IntegrationEndpoint.DIRECTIONS),
|
||||||
|
headers=self._random_headers(),
|
||||||
|
parameters=self._random_params(),
|
||||||
|
request_format="json",
|
||||||
|
response_format="json",
|
||||||
|
request_mapping=self._random_mapping(),
|
||||||
|
response_mapping=self._random_mapping(),
|
||||||
|
request_schema=self._random_schema(),
|
||||||
|
response_schema=self._random_schema(),
|
||||||
|
is_active=self.random_bool(0.9),
|
||||||
|
created_by=User.objects.filter(is_staff=True).first()
|
||||||
|
)
|
||||||
|
# Data mappings belong to an endpoint – create a few of them
|
||||||
|
for _ in range(random.randint(0, 2)):
|
||||||
|
DataMapping.objects.create(
|
||||||
|
endpoint=endpoint,
|
||||||
|
name=fake.word().title() + " Mapping",
|
||||||
|
description=fake.text(max_nb_chars=100),
|
||||||
|
mapping_type=self.random_choice(DataMapping.MAPPING_TYPES),
|
||||||
|
source_field=fake.word(),
|
||||||
|
source_format="json",
|
||||||
|
source_validation=self._random_validation(),
|
||||||
|
target_field=fake.word(),
|
||||||
|
target_format="json",
|
||||||
|
target_validation=self._random_validation(),
|
||||||
|
transformation_type=self.random_choice(DataMapping.TRANSFORMATION_TYPES),
|
||||||
|
transformation_config=self._random_config(),
|
||||||
|
is_required=self.random_bool(),
|
||||||
|
validation_rules=self._random_validation(),
|
||||||
|
default_value=fake.word(),
|
||||||
|
is_active=self.random_bool(),
|
||||||
|
created_by=User.objects.filter(is_staff=True).first()
|
||||||
|
)
|
||||||
|
self.generate_executions(endpoint, count=random.randint(0, 5))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Execution history
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def generate_executions(self, endpoint, count=3):
|
||||||
|
for _ in range(count):
|
||||||
|
started = timezone.now() - timedelta(minutes=random.randint(0, 120))
|
||||||
|
completed = started + timedelta(seconds=random.randint(10, 120))
|
||||||
|
if self.random_bool(0.1):
|
||||||
|
completed = None # simulate in‑flight or stuck
|
||||||
|
|
||||||
|
exec = IntegrationExecution.objects.create(
|
||||||
|
endpoint=endpoint,
|
||||||
|
execution_type=self.random_choice(IntegrationExecution.EXECUTION_TYPES),
|
||||||
|
status=self.random_choice(IntegrationExecution.STATUSES),
|
||||||
|
started_at=started,
|
||||||
|
completed_at=completed,
|
||||||
|
request_data=self._random_json(),
|
||||||
|
response_data=self._random_json(),
|
||||||
|
request_size_bytes=random.randint(100, 2000),
|
||||||
|
response_size_bytes=random.randint(100, 2000),
|
||||||
|
processing_time_ms=random.randint(50, 2000),
|
||||||
|
network_time_ms=random.randint(10, 500),
|
||||||
|
error_message="" if self.random_bool(0.9) else fake.sentence(),
|
||||||
|
error_details=self._random_json() if self.random_bool(0.2) else {},
|
||||||
|
retry_count=random.randint(0, 3),
|
||||||
|
external_id=fake.uuid4(),
|
||||||
|
correlation_id=fake.uuid4(),
|
||||||
|
triggered_by=User.objects.filter(is_staff=True).first(),
|
||||||
|
metadata=self._random_json()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Webhook executions (if the endpoint is a webhook)
|
||||||
|
if endpoint.endpoint_type == "webhook":
|
||||||
|
self.generate_webhook_executions(endpoint.external_system, exec)
|
||||||
|
|
||||||
|
# Logs – one per execution
|
||||||
|
self.generate_logs(exec, count=random.randint(1, 4))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Webhook endpoints & executions
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def generate_webhooks(self, tenant, count=2):
|
||||||
|
for _ in range(count):
|
||||||
|
system = ExternalSystem.objects.filter(tenant=tenant).first()
|
||||||
|
if not system:
|
||||||
|
continue
|
||||||
|
|
||||||
|
webhook = WebhookEndpoint.objects.create(
|
||||||
|
external_system=system,
|
||||||
|
name=fake.word().title() + " Webhook",
|
||||||
|
description=fake.text(max_nb_chars=120),
|
||||||
|
url_path=f"/webhooks/{fake.slug()}",
|
||||||
|
allowed_methods=[self.random_choice(IntegrationEndpoint.METHODS)],
|
||||||
|
authentication_type=self.random_choice(WebhookEndpoint.AUTHENTICATION_TYPES),
|
||||||
|
authentication_config=self._random_auth_config(),
|
||||||
|
processing_config=self._random_config(),
|
||||||
|
rate_limit_per_minute=random.randint(30, 120),
|
||||||
|
rate_limit_per_hour=random.randint(500, 2000),
|
||||||
|
is_active=self.random_bool(0.9),
|
||||||
|
created_by=User.objects.filter(is_staff=True).first()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optionally link a data mapping
|
||||||
|
if DataMapping.objects.exists():
|
||||||
|
webhook.data_mapping = random.choice(list(DataMapping.objects.all()))
|
||||||
|
webhook.save()
|
||||||
|
|
||||||
|
self.generate_webhook_executions(webhook, None)
|
||||||
|
|
||||||
|
# Logs for the webhook itself
|
||||||
|
self.generate_logs(webhook, count=random.randint(1, 3))
|
||||||
|
|
||||||
|
def generate_webhook_executions(self, webhook, parent_exec=None):
|
||||||
|
for _ in range(random.randint(0, 3)):
|
||||||
|
method = self.random_choice(IntegrationEndpoint.METHODS)
|
||||||
|
status = self.random_choice(WebhookExecution.STATUSES)
|
||||||
|
received_at = timezone.now() - timedelta(seconds=random.randint(0, 300))
|
||||||
|
processed_at = received_at + timedelta(seconds=random.randint(0, 30)) if status in ("completed", "failed") else None
|
||||||
|
|
||||||
|
WebhookExecution.objects.create(
|
||||||
|
webhook=webhook,
|
||||||
|
method=method,
|
||||||
|
headers=self._random_headers(),
|
||||||
|
query_params=self._random_params(),
|
||||||
|
payload=self._random_json(),
|
||||||
|
payload_size_bytes=random.randint(100, 2000),
|
||||||
|
client_ip=fake.ipv4_public(),
|
||||||
|
user_agent=fake.user_agent(),
|
||||||
|
status=status,
|
||||||
|
received_at=received_at,
|
||||||
|
processed_at=processed_at,
|
||||||
|
processing_time_ms=random.randint(50, 2000) if processed_at else None,
|
||||||
|
response_status=200 if status == "completed" else random.choice([400, 401, 403, 500]),
|
||||||
|
response_data=self._random_json(),
|
||||||
|
error_message="" if status == "completed" else fake.sentence(),
|
||||||
|
error_details=self._random_json() if status != "completed" else {},
|
||||||
|
external_id=fake.uuid4(),
|
||||||
|
correlation_id=fake.uuid4(),
|
||||||
|
metadata=self._random_json()
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Integration logs (generic)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def generate_logs(self, source, count=5):
|
||||||
|
for _ in range(count):
|
||||||
|
timestamp = timezone.now() - timedelta(minutes=random.randint(0, 120))
|
||||||
|
log = IntegrationLog.objects.create(
|
||||||
|
external_system=getattr(source, "external_system", None),
|
||||||
|
endpoint=getattr(source, "endpoint", None),
|
||||||
|
execution=getattr(source, "execution", None),
|
||||||
|
level=self.random_choice(IntegrationLog.LOG_LEVELS),
|
||||||
|
category=self.random_choice(IntegrationLog.CATEGORIES),
|
||||||
|
message=fake.sentence(),
|
||||||
|
details=self._random_json(),
|
||||||
|
correlation_id=fake.uuid4(),
|
||||||
|
user=User.objects.filter(is_staff=True).first(),
|
||||||
|
timestamp=timestamp,
|
||||||
|
metadata=self._random_json()
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Random helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
@staticmethod
|
||||||
|
def _random_json():
|
||||||
|
return {
|
||||||
|
"key1": fake.word(),
|
||||||
|
"key2": fake.word(),
|
||||||
|
"nested": {"sub": fake.word()}
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _random_headers():
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {uuid.uuid4()}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _random_params():
|
||||||
|
return {
|
||||||
|
fake.word(): fake.word(),
|
||||||
|
fake.word(): fake.word()
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _random_mapping():
|
||||||
|
return {
|
||||||
|
"source": fake.word(),
|
||||||
|
"target": fake.word()
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _random_schema():
|
||||||
|
return {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
fake.word(): {"type": "string"},
|
||||||
|
fake.word(): {"type": "integer"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _random_validation():
|
||||||
|
return {
|
||||||
|
"regex": "^[A-Za-z0-9_]+$",
|
||||||
|
"max_length": 50
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _random_auth_config():
|
||||||
|
return {
|
||||||
|
"username": fake.user_name(),
|
||||||
|
"password": fake.password(),
|
||||||
|
"token_url": fake.url()
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _random_config():
|
||||||
|
return {
|
||||||
|
"env": random.choice(["dev", "test", "prod"]),
|
||||||
|
"retries": random.randint(0, 5),
|
||||||
|
"timeout": random.randint(10, 60)
|
||||||
|
}
|
||||||
@ -53,15 +53,15 @@ class IntegrationDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
# Recent activity
|
# Recent activity
|
||||||
context.update({
|
context.update({
|
||||||
'recent_executions': IntegrationExecution.objects.filter(
|
'recent_executions': IntegrationExecution.objects.filter(
|
||||||
endpoint__tenant=self.request.user.tenant
|
endpoint__external_system__tenant=self.request.user.tenant
|
||||||
).order_by('-execution_time')[:10],
|
).order_by('-started_at')[:10],
|
||||||
|
|
||||||
'recent_webhook_executions': WebhookExecution.objects.filter(
|
'recent_webhook_executions': WebhookExecution.objects.filter(
|
||||||
tenant=self.request.user.tenant
|
webhook__external_system__tenant=self.request.user.tenant
|
||||||
).order_by('-execution_time')[:5],
|
).order_by('-processed_at')[:5],
|
||||||
|
|
||||||
'recent_logs': IntegrationLog.objects.filter(
|
'recent_logs': IntegrationLog.objects.filter(
|
||||||
tenant=self.request.user.tenant
|
endpoint__external_system__tenant=self.request.user.tenant
|
||||||
).order_by('-timestamp')[:10],
|
).order_by('-timestamp')[:10],
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -69,19 +69,19 @@ class IntegrationDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
context.update({
|
context.update({
|
||||||
'executions_today': IntegrationExecution.objects.filter(
|
'executions_today': IntegrationExecution.objects.filter(
|
||||||
tenant=self.request.user.tenant,
|
endpoint__external_system__tenant=self.request.user.tenant,
|
||||||
execution_time__date=today
|
started_at__date=today
|
||||||
).count(),
|
).count(),
|
||||||
|
|
||||||
'successful_executions': IntegrationExecution.objects.filter(
|
'successful_executions': IntegrationExecution.objects.filter(
|
||||||
tenant=self.request.user.tenant,
|
endpoint__external_system__tenant=self.request.user.tenant,
|
||||||
execution_time__date=today,
|
started_at__date=today,
|
||||||
status='SUCCESS'
|
status='SUCCESS'
|
||||||
).count(),
|
).count(),
|
||||||
|
|
||||||
'failed_executions': IntegrationExecution.objects.filter(
|
'failed_executions': IntegrationExecution.objects.filter(
|
||||||
tenant=self.request.user.tenant,
|
endpoint__external_system__tenant=self.request.user.tenant,
|
||||||
execution_time__date=today,
|
started_at__date=today,
|
||||||
status='FAILED'
|
status='FAILED'
|
||||||
).count(),
|
).count(),
|
||||||
})
|
})
|
||||||
@ -91,13 +91,13 @@ class IntegrationDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
'healthy_systems': ExternalSystem.objects.filter(
|
'healthy_systems': ExternalSystem.objects.filter(
|
||||||
tenant=self.request.user.tenant,
|
tenant=self.request.user.tenant,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
last_health_check_status='HEALTHY'
|
is_healthy=True
|
||||||
).count(),
|
).count(),
|
||||||
|
|
||||||
'unhealthy_systems': ExternalSystem.objects.filter(
|
'unhealthy_systems': ExternalSystem.objects.filter(
|
||||||
tenant=self.request.user.tenant,
|
tenant=self.request.user.tenant,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
last_health_check_status='UNHEALTHY'
|
is_healthy=False,
|
||||||
).count(),
|
).count(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ class ExternalSystemCreateView(LoginRequiredMixin, CreateView):
|
|||||||
"""
|
"""
|
||||||
model = ExternalSystem
|
model = ExternalSystem
|
||||||
form_class = ExternalSystemForm
|
form_class = ExternalSystemForm
|
||||||
template_name = 'integration/external_system_form.html'
|
template_name = 'integration/systems/external_system_form.html'
|
||||||
success_url = reverse_lazy('integration:external_system_list')
|
success_url = reverse_lazy('integration:external_system_list')
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
@ -214,7 +214,7 @@ class ExternalSystemUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
"""
|
"""
|
||||||
model = ExternalSystem
|
model = ExternalSystem
|
||||||
form_class = ExternalSystemForm
|
form_class = ExternalSystemForm
|
||||||
template_name = 'integration/external_system_form.html'
|
template_name = 'integration/systems/external_system_form.html'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return ExternalSystem.objects.filter(tenant=self.request.user.tenant)
|
return ExternalSystem.objects.filter(tenant=self.request.user.tenant)
|
||||||
@ -232,7 +232,7 @@ class ExternalSystemDeleteView(LoginRequiredMixin, DeleteView):
|
|||||||
Delete an external system.
|
Delete an external system.
|
||||||
"""
|
"""
|
||||||
model = ExternalSystem
|
model = ExternalSystem
|
||||||
template_name = 'integration/external_system_confirm_delete.html'
|
template_name = 'integration/systems/external_system_confirm_delete.html'
|
||||||
success_url = reverse_lazy('integration:external_system_list')
|
success_url = reverse_lazy('integration:external_system_list')
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -846,7 +846,7 @@ def integration_stats(request):
|
|||||||
|
|
||||||
context = {
|
context = {
|
||||||
'total_systems': ExternalSystem.objects.filter(tenant=request.user.tenant).count(),
|
'total_systems': ExternalSystem.objects.filter(tenant=request.user.tenant).count(),
|
||||||
'total_endpoints': IntegrationEndpoint.objects.filter(tenant=request.user.tenant).count(),
|
'total_endpoints': IntegrationEndpoint.objects.filter(endpoint__external_system__tenant=request.user.tenant).count(),
|
||||||
'total_mappings': DataMapping.objects.filter(tenant=request.user.tenant).count(),
|
'total_mappings': DataMapping.objects.filter(tenant=request.user.tenant).count(),
|
||||||
'total_webhooks': WebhookEndpoint.objects.filter(tenant=request.user.tenant).count(),
|
'total_webhooks': WebhookEndpoint.objects.filter(tenant=request.user.tenant).count(),
|
||||||
'executions_today': IntegrationExecution.objects.filter(
|
'executions_today': IntegrationExecution.objects.filter(
|
||||||
@ -877,12 +877,12 @@ def system_health(request):
|
|||||||
'healthy_systems': ExternalSystem.objects.filter(
|
'healthy_systems': ExternalSystem.objects.filter(
|
||||||
tenant=request.user.tenant,
|
tenant=request.user.tenant,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
last_health_check_status='HEALTHY'
|
is_healthy=True
|
||||||
).count(),
|
).count(),
|
||||||
'unhealthy_systems': ExternalSystem.objects.filter(
|
'unhealthy_systems': ExternalSystem.objects.filter(
|
||||||
tenant=request.user.tenant,
|
tenant=request.user.tenant,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
last_health_check_status='UNHEALTHY'
|
is_healthy=False
|
||||||
).count(),
|
).count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -593,7 +593,7 @@ class ImagingSeriesListView(LoginRequiredMixin, ListView):
|
|||||||
"""
|
"""
|
||||||
model = ImagingSeries
|
model = ImagingSeries
|
||||||
template_name = 'radiology/series/imaging_series_list.html'
|
template_name = 'radiology/series/imaging_series_list.html'
|
||||||
context_object_name = 'imaging_series'
|
context_object_name = 'series'
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -742,7 +742,7 @@ class RadiologyReportListView(LoginRequiredMixin, ListView):
|
|||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = RadiologyReport.objects.filter(tenant=self.request.user.tenant)
|
queryset = RadiologyReport.objects.filter(study__tenant=self.request.user.tenant)
|
||||||
|
|
||||||
# Search functionality
|
# Search functionality
|
||||||
search = self.request.GET.get('search')
|
search = self.request.GET.get('search')
|
||||||
@ -771,7 +771,7 @@ class RadiologyReportListView(LoginRequiredMixin, ListView):
|
|||||||
queryset = queryset.filter(radiologist_id=radiologist_id)
|
queryset = queryset.filter(radiologist_id=radiologist_id)
|
||||||
|
|
||||||
return queryset.select_related(
|
return queryset.select_related(
|
||||||
'study__order__patient', 'radiologist', 'template'
|
'study__imaging_order__patient', 'radiologist', 'template_used'
|
||||||
).order_by('-created_at')
|
).order_by('-created_at')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -788,10 +788,10 @@ class RadiologyReportDetailView(LoginRequiredMixin, DetailView):
|
|||||||
"""
|
"""
|
||||||
model = RadiologyReport
|
model = RadiologyReport
|
||||||
template_name = 'radiology/reports/radiology_report_detail.html'
|
template_name = 'radiology/reports/radiology_report_detail.html'
|
||||||
context_object_name = 'radiology_report'
|
context_object_name = 'report'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return RadiologyReport.objects.filter(tenant=self.request.user.tenant)
|
return RadiologyReport.objects.filter(study__tenant=self.request.user.tenant)
|
||||||
|
|
||||||
|
|
||||||
class RadiologyReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
class RadiologyReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
||||||
@ -810,7 +810,7 @@ class RadiologyReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, Cre
|
|||||||
response = super().form_valid(form)
|
response = super().form_valid(form)
|
||||||
|
|
||||||
# Log the action
|
# Log the action
|
||||||
AuditLogger.log_action(
|
AuditLogger.log_event(
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
action='RADIOLOGY_REPORT_CREATED',
|
action='RADIOLOGY_REPORT_CREATED',
|
||||||
model='RadiologyReport',
|
model='RadiologyReport',
|
||||||
|
|||||||
@ -14,6 +14,17 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.avatar-sm {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-gradient {
|
||||||
|
background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||||
|
}
|
||||||
|
|
||||||
.metric-card {
|
.metric-card {
|
||||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||||
color: white;
|
color: white;
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
<script src="{% static 'plugins/apexcharts/dist/apexcharts.min.js' %}"></script>
|
<script src="{% static 'plugins/apexcharts/dist/apexcharts.min.js' %}"></script>
|
||||||
|
<script src="{% static 'plugins/chart.js/dist/chart.js' %}"></script>
|
||||||
<!-- HTMX -->
|
<!-- HTMX -->
|
||||||
<script src="{% static 'js/htmx.min.js' %}"></script>
|
<script src="{% static 'js/htmx.min.js' %}"></script>
|
||||||
<!-- ================== END core-css ================== -->
|
<!-- ================== END core-css ================== -->
|
||||||
|
|||||||
@ -55,18 +55,19 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Recent Activity -->
|
<!-- Recent Activity -->
|
||||||
<div class="col-lg-8 mb-4">
|
<div class="col-lg-8 mb-4">
|
||||||
<div class="card h-100">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<h4 class="panel-title"><i class="fas fa-history me-2"></i> {{ _("Recent Activity")}}</h4>
|
||||||
<h5 class="mb-0">
|
<div class="panel-heading-btn">
|
||||||
<i class="fas fa-history me-2"></i>Recent Activity
|
<a href="{% url 'core:audit_log' %}" class="btn btn-xs btn-outline-theme"><small>{{ _("View All")}}</small></a>
|
||||||
</h5>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="{% url 'core:audit_log' %}" class="btn btn-sm btn-outline-primary">
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<i class="fas fa-external-link-alt me-1"></i>View All
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
</a>
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div id="recent-audit-logs">
|
<div id="recent-audit-logs">
|
||||||
{% for log in recent_audit_logs %}
|
{% for log in recent_audit_logs %}
|
||||||
<div class="d-flex align-items-start mb-3 pb-3 {% if not forloop.last %}border-bottom{% endif %}">
|
<div class="d-flex align-items-start mb-3 pb-3 {% if not forloop.last %}border-bottom{% endif %}">
|
||||||
|
|||||||
@ -168,33 +168,7 @@
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<nav aria-label="Clinical notes pagination">
|
{% include 'partial/pagination.html' %}
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
<li class="page-item active">
|
|
||||||
<span class="page-link">
|
|
||||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -160,33 +160,7 @@
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<nav aria-label="Problem list pagination">
|
{% include 'partial/pagination.html' %}
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% 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 %}
|
|
||||||
|
|
||||||
<li class="page-item active">
|
|
||||||
<span class="page-link">
|
|
||||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% 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>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,11 +4,11 @@
|
|||||||
{% block title %}Vital Signs - EMR{% endblock %}
|
{% block title %}Vital Signs - EMR{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link href="{% static 'assets/plugins/datatables.net-bs5/css/dataTables.bootstrap5.min.css' %}" rel="stylesheet" />
|
<link href="{% static '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-responsive-bs5/css/responsive.bootstrap5.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
||||||
<link href="{% static 'assets/plugins/chart.js/dist/Chart.min.css' %}" rel="stylesheet" />
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
@ -319,13 +319,13 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
<script src="{% static 'assets/plugins/datatables.net/js/jquery.dataTables.min.js' %}"></script>
|
<script src="{% static 'plugins/datatables.net/js/dataTables.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/datatables.net-bs5/js/dataTables.bootstrap5.min.js' %}"></script>
|
<script src="{% static '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 '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-responsive-bs5/js/responsive.bootstrap5.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
||||||
<script src="{% static 'assets/plugins/chart.js/dist/Chart.min.js' %}"></script>
|
<script src="{% static 'plugins/chart.js/dist/Chart.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
var table;
|
var table;
|
||||||
|
|||||||
@ -130,7 +130,7 @@
|
|||||||
<th>Annual Budget:</th>
|
<th>Annual Budget:</th>
|
||||||
<td>
|
<td>
|
||||||
{% if department.annual_budget %}
|
{% if department.annual_budget %}
|
||||||
${{ department.annual_budget|floatformat:2 }}
|
<span class="symbol">ê</span>{{ department.annual_budget|floatformat:'2g' }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not specified</span>
|
<span class="text-muted">Not specified</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -160,7 +160,7 @@
|
|||||||
{% if department.department_head %}
|
{% if department.department_head %}
|
||||||
<div class="text-center mb-3">
|
<div class="text-center mb-3">
|
||||||
<div class="avatar avatar-xl">
|
<div class="avatar avatar-xl">
|
||||||
<img src="{% static 'img/user/default-avatar.jpg' %}" alt="Department Head" class="rounded-circle">
|
<img src="{% static 'img/user/user-1.jpg' %}" alt="Department Head" class="rounded-circle">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mb-3">
|
<div class="text-center mb-3">
|
||||||
@ -220,7 +220,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="stat-card bg-info bg-opacity-10 p-3 rounded text-center">
|
<div class="stat-card bg-info bg-opacity-10 p-3 rounded text-center">
|
||||||
<h3 class="mb-1">{{ department.annual_budget|default:"N/A" }}</h3>
|
<h3 class="mb-1"><span class="symbol">ê</span>{{ department.annual_budget|floatformat:'2g' }}</h3>
|
||||||
<p class="mb-0">Annual Budget</p>
|
<p class="mb-0">Annual Budget</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -294,7 +294,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="avatar avatar-sm me-2">
|
<div class="avatar avatar-sm me-2">
|
||||||
<img src="{% static 'img/user/default-avatar.jpg' %}" alt="{{ employee.get_full_name }}" class="rounded-circle">
|
<img src="{{ employee.user.profile_picture.url }}" alt="{{ employee.get_full_name }}" class="rounded-circle">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'hr:employee_detail' employee.id %}">
|
<a href="{% url 'hr:employee_detail' employee.id %}">
|
||||||
@ -427,10 +427,10 @@
|
|||||||
labels: ['Full-time', 'Part-time', 'Contract', 'Temporary'],
|
labels: ['Full-time', 'Part-time', 'Contract', 'Temporary'],
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: [
|
data: [
|
||||||
{{ employees.filter(employment_type='FULL_TIME').count }},
|
{{ employees.count }},
|
||||||
{{ employees.filter(employment_type='PART_TIME').count }},
|
{# {{ employees.filter(employment_type='PART_TIME').count }},#}
|
||||||
{{ employees.filter(employment_type='CONTRACT').count }},
|
{# {{ employees.filter(employment_type='CONTRACT').count }},#}
|
||||||
{{ employees.filter(employment_type='TEMPORARY').count }}
|
{# {{ employees.filter(employment_type='TEMPORARY').count }}#}
|
||||||
],
|
],
|
||||||
backgroundColor: [
|
backgroundColor: [
|
||||||
'rgba(54, 162, 235, 0.7)',
|
'rgba(54, 162, 235, 0.7)',
|
||||||
|
|||||||
@ -320,38 +320,9 @@
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<div class="card-footer">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div class="text-muted small">
|
|
||||||
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ page_obj.paginator.count }} employees
|
|
||||||
</div>
|
|
||||||
<nav aria-label="Employees pagination">
|
|
||||||
<ul class="pagination pagination-sm mb-0">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page=1{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">First</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Previous</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<li class="page-item active">
|
{% include 'partial/pagination.html' %}
|
||||||
<span class="page-link">{{ page_obj.number }}</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Next</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% for key, value in request.GET.items %}{% if key != 'page' %}&{{ key }}={{ value }}{% endif %}{% endfor %}">Last</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
{% load static custom_filters%}
|
||||||
|
|
||||||
{% block title %}Integration Dashboard - {{ block.super }}{% endblock %}
|
{% block title %}Integration Dashboard - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
@ -13,7 +13,7 @@
|
|||||||
</h1>
|
</h1>
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb mb-0">
|
<ol class="breadcrumb mb-0">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'accounts:dashboard' %}">Dashboard</a></li>
|
<li class="breadcrumb-item"><a href="">Dashboard</a></li>
|
||||||
<li class="breadcrumb-item active">Integration</li>
|
<li class="breadcrumb-item active">Integration</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@ -367,12 +367,12 @@
|
|||||||
<span>Templates</span>
|
<span>Templates</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-2 col-md-4 col-sm-6">
|
{# <div class="col-lg-2 col-md-4 col-sm-6">#}
|
||||||
<a href="{% url 'radiology:imaging_series_list' %}" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">
|
{# <a href="{% url 'radiology:imaging_series_list' %}" class="btn btn-outline-secondary w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">#}
|
||||||
<i class="fas fa-layer-group fa-2x mb-2"></i>
|
{# <i class="fas fa-layer-group fa-2x mb-2"></i>#}
|
||||||
<span>View Series</span>
|
{# <span>View Series</span>#}
|
||||||
</a>
|
{# </a>#}
|
||||||
</div>
|
{# </div>#}
|
||||||
<div class="col-lg-2 col-md-4 col-sm-6">
|
<div class="col-lg-2 col-md-4 col-sm-6">
|
||||||
<a href="{% url 'radiology:dicom_image_list' %}" class="btn btn-outline-dark w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">
|
<a href="{% url 'radiology:dicom_image_list' %}" class="btn btn-outline-dark w-100 h-100 d-flex flex-column align-items-center justify-content-center py-3">
|
||||||
<i class="fas fa-images fa-2x mb-2"></i>
|
<i class="fas fa-images fa-2x mb-2"></i>
|
||||||
|
|||||||
@ -59,7 +59,7 @@
|
|||||||
<strong>Study Date:</strong> {{ report.study.study_datetime|date:"M d, Y g:i A" }}
|
<strong>Study Date:</strong> {{ report.study.study_datetime|date:"M d, Y g:i A" }}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
<strong>Patient:</strong> {{ report.study.patient.get_full_name }} ({{ report.study.patient.patient_id }})
|
<strong>Patient:</strong> {{ report.study.patient.get_full_name }} <strong>MRN:</strong> {{ report.study.patient.mrn }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
<div class="col-md-4 text-end">
|
||||||
@ -241,8 +241,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div class="avatar-circle bg-primary text-white me-3">
|
|
||||||
{{ report.radiologist.first_name.0 }}{{ report.radiologist.last_name.0 }}
|
<div class="avatar-sm bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
|
<span class="fw-bold">{{ report.radiologist.first_name.0 }}{{ report.radiologist.last_name.0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 class="mb-0">{{ report.radiologist.get_full_name }}</h6>
|
<h6 class="mb-0">{{ report.radiologist.get_full_name }}</h6>
|
||||||
@ -251,8 +252,8 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if report.dictated_by and report.dictated_by != report.radiologist %}
|
{% if report.dictated_by and report.dictated_by != report.radiologist %}
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div class="avatar-circle bg-secondary text-white me-3" style="width: 32px; height: 32px; font-size: 12px;">
|
<div class="avatar-sm bg-purple bg-gradient rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
{{ report.dictated_by.first_name.0 }}{{ report.dictated_by.last_name.0 }}
|
<span class="fw-bold">{{ report.dictated_by.first_name.0 }}{{ report.dictated_by.last_name.0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 class="mb-0">{{ report.dictated_by.get_full_name }}</h6>
|
<h6 class="mb-0">{{ report.dictated_by.get_full_name }}</h6>
|
||||||
@ -262,8 +263,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if report.transcribed_by %}
|
{% if report.transcribed_by %}
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="avatar-circle bg-info text-white me-3" style="width: 32px; height: 32px; font-size: 12px;">
|
<div class="avatar-sm bg-info bg-gradient rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
{{ report.transcribed_by.first_name.0 }}{{ report.transcribed_by.last_name.0 }}
|
<span class="fw-bold">{{ report.transcribed_by.first_name.0 }}{{ report.transcribed_by.last_name.0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 class="mb-0">{{ report.transcribed_by.get_full_name }}</h6>
|
<h6 class="mb-0">{{ report.transcribed_by.get_full_name }}</h6>
|
||||||
@ -321,12 +322,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex align-items-center mb-3">
|
||||||
<div class="avatar-circle bg-secondary text-white me-3">
|
<div class="avatar-sm bg-secondary bg-gradient rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||||
{{ report.study.patient.first_name.0 }}{{ report.study.patient.last_name.0 }}
|
{{ report.study.patient.first_name.0 }}{{ report.study.patient.last_name.0 }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h6 class="mb-0">{{ report.study.patient.get_full_name }}</h6>
|
<h6 class="mb-0">{{ report.study.patient.get_full_name }}</h6>
|
||||||
<small class="text-muted">{{ report.study.patient.patient_id }}</small>
|
<small class="text-muted">{{ report.study.patient.mrn }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
@ -344,7 +345,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<small class="text-muted d-block">MRN</small>
|
<small class="text-muted d-block">MRN</small>
|
||||||
<span>{{ report.study.patient.medical_record_number }}</span>
|
<span>{{ report.study.patient.mrn }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
|
|||||||
@ -250,9 +250,9 @@
|
|||||||
<a href="{% url 'radiology:radiology_report_detail' report.pk %}" class="btn btn-outline-primary" title="View">
|
<a href="{% url 'radiology:radiology_report_detail' report.pk %}" class="btn btn-outline-primary" title="View">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'radiology:radiology_report_update' report.pk %}" class="btn btn-outline-secondary" title="Edit">
|
{# <a href="{% url 'radiology:rxxxxxxxxxx' report.pk %}" class="btn btn-outline-secondary" title="Edit">#}
|
||||||
<i class="fas fa-edit"></i>
|
{# <i class="fas fa-edit"></i>#}
|
||||||
</a>
|
{# </a>#}
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<button type="button" class="btn btn-outline-info dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" title="More">
|
<button type="button" class="btn btn-outline-info dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" title="More">
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
@ -261,7 +261,7 @@
|
|||||||
<li><a class="dropdown-item" href="#" onclick="printReport({{ report.pk }})"><i class="fas fa-print me-2"></i>Print</a></li>
|
<li><a class="dropdown-item" href="#" onclick="printReport({{ report.pk }})"><i class="fas fa-print me-2"></i>Print</a></li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="emailReport({{ report.pk }})"><i class="fas fa-envelope me-2"></i>Email</a></li>
|
<li><a class="dropdown-item" href="#" onclick="emailReport({{ report.pk }})"><i class="fas fa-envelope me-2"></i>Email</a></li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'radiology:radiology_report_delete' report.pk %}"><i class="fas fa-trash me-2"></i>Delete</a></li>
|
{# <li><a class="dropdown-item" href="{% url 'radiology:radiology_report_delete' report.pk %}"><i class="fas fa-trash me-2"></i>Delete</a></li>#}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user