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('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('problem/<int:problem_id>/resolve/', views.resolve_problem, name='resolve_problem'),
|
||||
|
||||
# API endpoints
|
||||
# path('api/', include('emr.api.urls')),
|
||||
|
||||
30
emr/views.py
30
emr/views.py
@ -459,7 +459,7 @@ class ClinicalNoteDeleteView(
|
||||
SuccessMessageMixin, DeleteView
|
||||
):
|
||||
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_message = _('Clinical note deleted successfully.')
|
||||
|
||||
@ -1497,20 +1497,20 @@ def get_status_class(status):
|
||||
# return redirect('emr:encounter_detail', pk=pk)
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def resolve_problem(request, pk):
|
||||
# prob = get_object_or_404(ProblemList, pk=pk, patient__tenant=request.user.tenant)
|
||||
# if prob.status == 'ACTIVE':
|
||||
# prob.status = 'RESOLVED'; prob.save()
|
||||
# AuditLogEntry.objects.create(
|
||||
# tenant=request.user.tenant, user=request.user,
|
||||
# action='UPDATE', model_name='ProblemList',
|
||||
# object_id=str(prob.pk), changes={'status': 'Problem resolved'}
|
||||
# )
|
||||
# messages.success(request, _('Problem resolved.'))
|
||||
# else:
|
||||
# messages.error(request, _('Only active problems can be resolved.'))
|
||||
# return redirect('emr:problem_detail', pk=pk)
|
||||
@login_required
|
||||
def resolve_problem(request, pk):
|
||||
prob = get_object_or_404(ProblemList, pk=pk, patient__tenant=request.user.tenant)
|
||||
if prob.status == 'ACTIVE':
|
||||
prob.status = 'RESOLVED'; prob.save()
|
||||
AuditLogEntry.objects.create(
|
||||
tenant=request.user.tenant, user=request.user,
|
||||
action='UPDATE', model_name='ProblemList',
|
||||
object_id=str(prob.pk), changes={'status': 'Problem resolved'}
|
||||
)
|
||||
messages.success(request, _('Problem resolved.'))
|
||||
else:
|
||||
messages.error(request, _('Only active problems can be resolved.'))
|
||||
return redirect('emr:problem_detail', pk=pk)
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
|
||||
Binary file not shown.
@ -278,9 +278,7 @@ class DepartmentListView(LoginRequiredMixin, ListView):
|
||||
Q(description__icontains=search)
|
||||
)
|
||||
|
||||
return queryset.annotate(
|
||||
employee_count=Count('employees')
|
||||
).order_by('name')
|
||||
return queryset
|
||||
|
||||
|
||||
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
|
||||
context.update({
|
||||
'recent_executions': IntegrationExecution.objects.filter(
|
||||
endpoint__tenant=self.request.user.tenant
|
||||
).order_by('-execution_time')[:10],
|
||||
endpoint__external_system__tenant=self.request.user.tenant
|
||||
).order_by('-started_at')[:10],
|
||||
|
||||
'recent_webhook_executions': WebhookExecution.objects.filter(
|
||||
tenant=self.request.user.tenant
|
||||
).order_by('-execution_time')[:5],
|
||||
webhook__external_system__tenant=self.request.user.tenant
|
||||
).order_by('-processed_at')[:5],
|
||||
|
||||
'recent_logs': IntegrationLog.objects.filter(
|
||||
tenant=self.request.user.tenant
|
||||
endpoint__external_system__tenant=self.request.user.tenant
|
||||
).order_by('-timestamp')[:10],
|
||||
})
|
||||
|
||||
@ -69,19 +69,19 @@ class IntegrationDashboardView(LoginRequiredMixin, TemplateView):
|
||||
today = timezone.now().date()
|
||||
context.update({
|
||||
'executions_today': IntegrationExecution.objects.filter(
|
||||
tenant=self.request.user.tenant,
|
||||
execution_time__date=today
|
||||
endpoint__external_system__tenant=self.request.user.tenant,
|
||||
started_at__date=today
|
||||
).count(),
|
||||
|
||||
'successful_executions': IntegrationExecution.objects.filter(
|
||||
tenant=self.request.user.tenant,
|
||||
execution_time__date=today,
|
||||
endpoint__external_system__tenant=self.request.user.tenant,
|
||||
started_at__date=today,
|
||||
status='SUCCESS'
|
||||
).count(),
|
||||
|
||||
'failed_executions': IntegrationExecution.objects.filter(
|
||||
tenant=self.request.user.tenant,
|
||||
execution_time__date=today,
|
||||
endpoint__external_system__tenant=self.request.user.tenant,
|
||||
started_at__date=today,
|
||||
status='FAILED'
|
||||
).count(),
|
||||
})
|
||||
@ -91,13 +91,13 @@ class IntegrationDashboardView(LoginRequiredMixin, TemplateView):
|
||||
'healthy_systems': ExternalSystem.objects.filter(
|
||||
tenant=self.request.user.tenant,
|
||||
is_active=True,
|
||||
last_health_check_status='HEALTHY'
|
||||
is_healthy=True
|
||||
).count(),
|
||||
|
||||
'unhealthy_systems': ExternalSystem.objects.filter(
|
||||
tenant=self.request.user.tenant,
|
||||
is_active=True,
|
||||
last_health_check_status='UNHEALTHY'
|
||||
is_healthy=False,
|
||||
).count(),
|
||||
})
|
||||
|
||||
@ -199,7 +199,7 @@ class ExternalSystemCreateView(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
model = ExternalSystem
|
||||
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')
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -214,7 +214,7 @@ class ExternalSystemUpdateView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
model = ExternalSystem
|
||||
form_class = ExternalSystemForm
|
||||
template_name = 'integration/external_system_form.html'
|
||||
template_name = 'integration/systems/external_system_form.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return ExternalSystem.objects.filter(tenant=self.request.user.tenant)
|
||||
@ -232,7 +232,7 @@ class ExternalSystemDeleteView(LoginRequiredMixin, DeleteView):
|
||||
Delete an external system.
|
||||
"""
|
||||
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')
|
||||
|
||||
def get_queryset(self):
|
||||
@ -846,7 +846,7 @@ def integration_stats(request):
|
||||
|
||||
context = {
|
||||
'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_webhooks': WebhookEndpoint.objects.filter(tenant=request.user.tenant).count(),
|
||||
'executions_today': IntegrationExecution.objects.filter(
|
||||
@ -877,12 +877,12 @@ def system_health(request):
|
||||
'healthy_systems': ExternalSystem.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
is_active=True,
|
||||
last_health_check_status='HEALTHY'
|
||||
is_healthy=True
|
||||
).count(),
|
||||
'unhealthy_systems': ExternalSystem.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
is_active=True,
|
||||
last_health_check_status='UNHEALTHY'
|
||||
is_healthy=False
|
||||
).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
|
||||
template_name = 'radiology/series/imaging_series_list.html'
|
||||
context_object_name = 'imaging_series'
|
||||
context_object_name = 'series'
|
||||
paginate_by = 25
|
||||
|
||||
def get_queryset(self):
|
||||
@ -742,7 +742,7 @@ class RadiologyReportListView(LoginRequiredMixin, ListView):
|
||||
paginate_by = 25
|
||||
|
||||
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 = self.request.GET.get('search')
|
||||
@ -771,7 +771,7 @@ class RadiologyReportListView(LoginRequiredMixin, ListView):
|
||||
queryset = queryset.filter(radiologist_id=radiologist_id)
|
||||
|
||||
return queryset.select_related(
|
||||
'study__order__patient', 'radiologist', 'template'
|
||||
'study__imaging_order__patient', 'radiologist', 'template_used'
|
||||
).order_by('-created_at')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -788,10 +788,10 @@ class RadiologyReportDetailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
model = RadiologyReport
|
||||
template_name = 'radiology/reports/radiology_report_detail.html'
|
||||
context_object_name = 'radiology_report'
|
||||
context_object_name = 'report'
|
||||
|
||||
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):
|
||||
@ -810,7 +810,7 @@ class RadiologyReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, Cre
|
||||
response = super().form_valid(form)
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_action(
|
||||
AuditLogger.log_event(
|
||||
user=self.request.user,
|
||||
action='RADIOLOGY_REPORT_CREATED',
|
||||
model='RadiologyReport',
|
||||
|
||||
@ -14,6 +14,17 @@
|
||||
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 {
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
color: white;
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
|
||||
</body>
|
||||
<script src="{% static 'plugins/apexcharts/dist/apexcharts.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/chart.js/dist/chart.js' %}"></script>
|
||||
<!-- HTMX -->
|
||||
<script src="{% static 'js/htmx.min.js' %}"></script>
|
||||
<!-- ================== END core-css ================== -->
|
||||
|
||||
@ -55,18 +55,19 @@
|
||||
<div class="row">
|
||||
<!-- Recent Activity -->
|
||||
<div class="col-lg-8 mb-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-history me-2"></i>Recent Activity
|
||||
</h5>
|
||||
<a href="{% url 'core:audit_log' %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View All
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title"><i class="fas fa-history me-2"></i> {{ _("Recent Activity")}}</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="{% url 'core:audit_log' %}" class="btn btn-xs btn-outline-theme"><small>{{ _("View All")}}</small></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></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 class="panel-body">
|
||||
<div id="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 %}">
|
||||
|
||||
@ -168,33 +168,7 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Clinical notes pagination">
|
||||
<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>
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -160,33 +160,7 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Problem list pagination">
|
||||
<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>
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,11 +4,11 @@
|
||||
{% block title %}Vital Signs - EMR{% 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 'assets/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 'assets/plugins/chart.js/dist/Chart.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" />
|
||||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -319,13 +319,13 @@
|
||||
{% 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 'assets/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 'assets/plugins/chart.js/dist/Chart.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/select2/dist/js/select2.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
||||
<script src="{% static 'plugins/chart.js/dist/Chart.js' %}"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
var table;
|
||||
|
||||
@ -130,7 +130,7 @@
|
||||
<th>Annual Budget:</th>
|
||||
<td>
|
||||
{% if department.annual_budget %}
|
||||
${{ department.annual_budget|floatformat:2 }}
|
||||
<span class="symbol">ê</span>{{ department.annual_budget|floatformat:'2g' }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not specified</span>
|
||||
{% endif %}
|
||||
@ -160,7 +160,7 @@
|
||||
{% if department.department_head %}
|
||||
<div class="text-center mb-3">
|
||||
<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 class="text-center mb-3">
|
||||
@ -220,7 +220,7 @@
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -294,7 +294,7 @@
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<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>
|
||||
<a href="{% url 'hr:employee_detail' employee.id %}">
|
||||
@ -427,10 +427,10 @@
|
||||
labels: ['Full-time', 'Part-time', 'Contract', 'Temporary'],
|
||||
datasets: [{
|
||||
data: [
|
||||
{{ employees.filter(employment_type='FULL_TIME').count }},
|
||||
{{ employees.filter(employment_type='PART_TIME').count }},
|
||||
{{ employees.filter(employment_type='CONTRACT').count }},
|
||||
{{ employees.filter(employment_type='TEMPORARY').count }}
|
||||
{{ employees.count }},
|
||||
{# {{ employees.filter(employment_type='PART_TIME').count }},#}
|
||||
{# {{ employees.filter(employment_type='CONTRACT').count }},#}
|
||||
{# {{ employees.filter(employment_type='TEMPORARY').count }}#}
|
||||
],
|
||||
backgroundColor: [
|
||||
'rgba(54, 162, 235, 0.7)',
|
||||
|
||||
@ -320,38 +320,9 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{% 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">
|
||||
<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>
|
||||
|
||||
{% include 'partial/pagination.html' %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load static custom_filters%}
|
||||
|
||||
{% block title %}Integration Dashboard - {{ block.super }}{% endblock %}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<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>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@ -367,12 +367,12 @@
|
||||
<span>Templates</span>
|
||||
</a>
|
||||
</div>
|
||||
<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">
|
||||
<i class="fas fa-layer-group fa-2x mb-2"></i>
|
||||
<span>View Series</span>
|
||||
</a>
|
||||
</div>
|
||||
{# <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">#}
|
||||
{# <i class="fas fa-layer-group fa-2x mb-2"></i>#}
|
||||
{# <span>View Series</span>#}
|
||||
{# </a>#}
|
||||
{# </div>#}
|
||||
<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">
|
||||
<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" }}
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
@ -241,8 +241,9 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<h6 class="mb-0">{{ report.radiologist.get_full_name }}</h6>
|
||||
@ -251,8 +252,8 @@
|
||||
</div>
|
||||
{% if report.dictated_by and report.dictated_by != report.radiologist %}
|
||||
<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;">
|
||||
{{ report.dictated_by.first_name.0 }}{{ report.dictated_by.last_name.0 }}
|
||||
<div class="avatar-sm bg-purple bg-gradient rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||
<span class="fw-bold">{{ report.dictated_by.first_name.0 }}{{ report.dictated_by.last_name.0 }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0">{{ report.dictated_by.get_full_name }}</h6>
|
||||
@ -262,8 +263,8 @@
|
||||
{% endif %}
|
||||
{% if report.transcribed_by %}
|
||||
<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;">
|
||||
{{ report.transcribed_by.first_name.0 }}{{ report.transcribed_by.last_name.0 }}
|
||||
<div class="avatar-sm bg-info bg-gradient rounded-circle d-flex align-items-center justify-content-center me-2">
|
||||
<span class="fw-bold">{{ report.transcribed_by.first_name.0 }}{{ report.transcribed_by.last_name.0 }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-0">{{ report.transcribed_by.get_full_name }}</h6>
|
||||
@ -321,12 +322,12 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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 }}
|
||||
</div>
|
||||
<div>
|
||||
<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 class="row g-2">
|
||||
@ -344,7 +345,7 @@
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<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 class="mt-3">
|
||||
|
||||
@ -250,9 +250,9 @@
|
||||
<a href="{% url 'radiology:radiology_report_detail' report.pk %}" class="btn btn-outline-primary" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'radiology:radiology_report_update' report.pk %}" class="btn btn-outline-secondary" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{# <a href="{% url 'radiology:rxxxxxxxxxx' report.pk %}" class="btn btn-outline-secondary" title="Edit">#}
|
||||
{# <i class="fas fa-edit"></i>#}
|
||||
{# </a>#}
|
||||
<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">
|
||||
<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="emailReport({{ report.pk }})"><i class="fas fa-envelope me-2"></i>Email</a></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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user