This commit is contained in:
Marwan Alwali 2025-08-27 18:45:14 +03:00
parent 193ee7f34a
commit 25f548825b
34 changed files with 12585 additions and 183 deletions

View File

Binary file not shown.

View 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')

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

View 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 fields 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 inflight 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)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -130,7 +130,7 @@
<th>Annual Budget:</th>
<td>
{% if department.annual_budget %}
${{ department.annual_budget|floatformat:2 }}
<span class="symbol">&#xea;</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">&#xea;</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)',

View File

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

View File

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

View File

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

View File

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

View File

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