360 lines
17 KiB
HTML
360 lines
17 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}Audit Log Detail - {{ audit_log.action }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Breadcrumb -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="page-title-box d-sm-flex align-items-center justify-content-between">
|
|
<h4 class="mb-sm-0">Audit Log Detail</h4>
|
|
<div class="page-title-right">
|
|
<ol class="breadcrumb m-0">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'core:audit_log' %}">Audit Logs</a></li>
|
|
<li class="breadcrumb-item active">Log Detail</li>
|
|
</ol>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Main Content -->
|
|
<div class="col-lg-8">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-shrink-0">
|
|
<div class="avatar-sm me-3">
|
|
<span class="avatar-title rounded-circle bg-{% if audit_log.event_type == 'CREATE' %}success{% elif audit_log.event_type == 'UPDATE' %}primary{% elif audit_log.event_type == 'DELETE' %}danger{% elif audit_log.event_type == 'LOGIN' %}info{% elif audit_log.event_type == 'LOGOUT' %}secondary{% else %}warning{% endif %} text-white">
|
|
<i class="fas fa-{% if audit_log.event_type == 'CREATE' %}plus{% elif audit_log.event_type == 'UPDATE' %}edit{% elif audit_log.event_type == 'DELETE' %}trash{% elif audit_log.event_type == 'LOGIN' %}sign-in-alt{% elif audit_log.event_type == 'LOGOUT' %}sign-out-alt{% else %}exclamation{% endif %}"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h5 class="card-title mb-0">{{ audit_log.action }}</h5>
|
|
<small class="text-muted">{{ audit_log.get_event_type_display }} | {{ audit_log.get_event_category_display }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="d-flex mb-3">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-clock text-muted me-2"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-0">Timestamp</h6>
|
|
<p class="text-muted mb-0">{{ audit_log.timestamp|date:"M d, Y g:i:s A" }}</p>
|
|
<small class="text-muted">{{ audit_log.timestamp|timesince }} ago</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="d-flex mb-3">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-user text-muted me-2"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-0">User</h6>
|
|
<p class="text-muted mb-0">
|
|
{% if audit_log.user %}
|
|
{{ audit_log.user.get_full_name|default:audit_log.user.username }}
|
|
{% else %}
|
|
System
|
|
{% endif %}
|
|
</p>
|
|
{% if audit_log.user %}
|
|
<small class="text-muted">{{ audit_log.user.email }}</small>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="d-flex mb-3">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-tag text-muted me-2"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-0">Event Type</h6>
|
|
<p class="mb-0">
|
|
<span class="badge bg-{% if audit_log.event_type == 'CREATE' %}success{% elif audit_log.event_type == 'UPDATE' %}primary{% elif audit_log.event_type == 'DELETE' %}danger{% elif audit_log.event_type == 'LOGIN' %}info{% elif audit_log.event_type == 'LOGOUT' %}secondary{% else %}warning{% endif %}">
|
|
{{ audit_log.get_event_type_display }}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="d-flex mb-3">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-folder text-muted me-2"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-0">Category</h6>
|
|
<p class="mb-0">
|
|
<span class="badge bg-info">
|
|
{{ audit_log.get_event_category_display }}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Description</h6>
|
|
<div class="p-3 bg-light rounded">
|
|
{{ audit_log.description }}
|
|
</div>
|
|
</div>
|
|
|
|
{% if audit_log.content_type %}
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Affected Object</h6>
|
|
<div class="p-3 bg-light rounded">
|
|
<div class="d-flex align-items-center">
|
|
<div class="flex-shrink-0">
|
|
<i class="fas fa-database text-muted me-2"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<p class="mb-0">
|
|
<strong>{{ audit_log.content_type.name|title }}</strong>
|
|
{% if audit_log.object_id %}
|
|
<span class="text-muted">(ID: {{ audit_log.object_id }})</span>
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if audit_log.data %}
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Changed Data</h6>
|
|
<div class="p-3 bg-light rounded">
|
|
<pre class="mb-0"><code>{{ audit_log.data|pprint }}</code></pre>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if audit_log.ip_address %}
|
|
<div class="mb-4">
|
|
<h6 class="text-muted mb-2">Request Information</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-bordered mb-0">
|
|
<tbody>
|
|
<tr>
|
|
<th style="width: 30%">IP Address</th>
|
|
<td>{{ audit_log.ip_address }}</td>
|
|
</tr>
|
|
{% if audit_log.user_agent %}
|
|
<tr>
|
|
<th>User Agent</th>
|
|
<td>{{ audit_log.user_agent }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% if audit_log.request_method %}
|
|
<tr>
|
|
<th>Request Method</th>
|
|
<td>{{ audit_log.request_method }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% if audit_log.request_path %}
|
|
<tr>
|
|
<th>Request Path</th>
|
|
<td>{{ audit_log.request_path }}</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Quick Actions -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-bolt me-2"></i>
|
|
Quick Actions
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-grid gap-2">
|
|
<a href="{% url 'core:audit_log' %}" class="btn btn-outline-primary">
|
|
<i class="fas fa-arrow-left me-2"></i>
|
|
Back to Audit Logs
|
|
</a>
|
|
|
|
<button type="button" class="btn btn-outline-secondary" onclick="exportLog()">
|
|
<i class="fas fa-download me-2"></i>
|
|
Export Log Entry
|
|
</button>
|
|
|
|
{% if audit_log.content_type and audit_log.object_id %}
|
|
<button type="button" class="btn btn-outline-info" onclick="viewRelatedObject()">
|
|
<i class="fas fa-external-link-alt me-2"></i>
|
|
View Related Object
|
|
</button>
|
|
{% endif %}
|
|
|
|
{% if audit_log.event_type == 'UPDATE' %}
|
|
<button type="button" class="btn btn-outline-warning" onclick="viewChanges()">
|
|
<i class="fas fa-history me-2"></i>
|
|
View Changes
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Related Logs -->
|
|
{% if related_logs %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-link me-2"></i>
|
|
Related Logs
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="list-group">
|
|
{% for log in related_logs %}
|
|
<a href="{% url 'core:audit_log_detail' log.pk %}" class="list-group-item list-group-item-action">
|
|
<div class="d-flex w-100 justify-content-between">
|
|
<h6 class="mb-1">{{ log.action }}</h6>
|
|
<small class="text-muted">{{ log.timestamp|timesince }} ago</small>
|
|
</div>
|
|
<p class="mb-1 text-muted">{{ log.description|truncatechars:80 }}</p>
|
|
<small>
|
|
<span class="badge bg-{% if log.event_type == 'CREATE' %}success{% elif log.event_type == 'UPDATE' %}primary{% elif log.event_type == 'DELETE' %}danger{% elif log.event_type == 'LOGIN' %}info{% elif log.event_type == 'LOGOUT' %}secondary{% else %}warning{% endif %}">
|
|
{{ log.get_event_type_display }}
|
|
</span>
|
|
{% if log.user %}
|
|
<span class="text-muted">by {{ log.user.get_full_name|default:log.user.username }}</span>
|
|
{% endif %}
|
|
</small>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Tenant Information -->
|
|
{% if audit_log.tenant %}
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-building me-2"></i>
|
|
Tenant Information
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div class="flex-shrink-0">
|
|
<div class="avatar-sm me-3">
|
|
<span class="avatar-title rounded-circle bg-primary text-white">
|
|
{{ audit_log.tenant.name|first|upper }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-0">{{ audit_log.tenant.name }}</h6>
|
|
<small class="text-muted">{{ audit_log.tenant.get_organization_type_display }}</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-0">
|
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
|
<small class="text-muted">Status</small>
|
|
<small class="text-{% if audit_log.tenant.is_active %}success{% else %}danger{% endif %}">
|
|
{% if audit_log.tenant.is_active %}Active{% else %}Inactive{% endif %}
|
|
</small>
|
|
</div>
|
|
<div class="progress" style="height: 6px;">
|
|
<div class="progress-bar bg-{% if audit_log.tenant.is_active %}success{% else %}danger{% endif %}"
|
|
style="width: {% if audit_log.tenant.is_active %}100{% else %}100{% endif %}%"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
function exportLog() {
|
|
window.open('{% url "core:audit_log_detail" audit_log.pk %}?export=json', '_blank');
|
|
}
|
|
|
|
function viewRelatedObject() {
|
|
{% if audit_log.content_type and audit_log.object_id %}
|
|
// This is a simplified approach - in a real app, you'd need to determine the correct URL
|
|
// based on the content type and object ID
|
|
const contentType = '{{ audit_log.content_type.model }}';
|
|
const objectId = '{{ audit_log.object_id }}';
|
|
|
|
// Map common content types to their URLs
|
|
const urlMap = {
|
|
'tenant': `/core/tenants/${objectId}/`,
|
|
'department': `/core/departments/${objectId}/`,
|
|
'systemconfiguration': `/core/system-configuration/${objectId}/`,
|
|
'systemnotification': `/core/notifications/${objectId}/`,
|
|
'user': `/accounts/users/${objectId}/`,
|
|
'patient': `/patients/profiles/${objectId}/`,
|
|
'employee': `/hr/employees/${objectId}/`,
|
|
'medicalbill': `/billing/bills/${objectId}/`,
|
|
'appointment': `/appointments/${objectId}/`,
|
|
};
|
|
|
|
const url = urlMap[contentType] || '#';
|
|
if (url !== '#') {
|
|
window.open(url, '_blank');
|
|
} else {
|
|
alert(`Cannot navigate to object of type: ${contentType}`);
|
|
}
|
|
{% endif %}
|
|
}
|
|
|
|
function viewChanges() {
|
|
{% if audit_log.data %}
|
|
const data = {{ audit_log.data|safe }};
|
|
let changes = '';
|
|
|
|
if (data.changes) {
|
|
for (const [field, values] of Object.entries(data.changes)) {
|
|
changes += `Field: ${field}\n`;
|
|
changes += `Old value: ${values[0]}\n`;
|
|
changes += `New value: ${values[1]}\n\n`;
|
|
}
|
|
|
|
alert(`Changes:\n\n${changes}`);
|
|
} else {
|
|
alert('No detailed change information available');
|
|
}
|
|
{% else %}
|
|
alert('No change data available for this log entry');
|
|
{% endif %}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|