Files
docupulse/templates/main/instance_detail.html
2025-06-10 13:51:59 +02:00

1573 lines
67 KiB
HTML

{% extends "common/base.html" %}
{% from "components/header.html" import header %}
{% block title %}{{ instance.name }} - DocuPulse{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/settings.css') }}?v={{ 'css/settings.css'|asset_version }}">
{% endblock %}
{% block content %}
{{ header(
title="Instance Details",
description=instance.name + " for " + instance.company,
icon="fa-server",
buttons=[
{
'text': 'Back to Instances',
'url': '/instances',
'icon': 'fa-arrow-left',
'class': 'btn-secondary'
}
]
) }}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="row mb-4">
<!-- Activity Status Card -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="status-icon-wrapper">
<i class="fas fa-circle-notch fa-spin {% if instance.status == 'active' %}text-success{% else %}text-danger{% endif %} fa-2x"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h5 class="card-title mb-1">Activity Status</h5>
<p class="card-text mb-0">
<span class="badge bg-{{ 'success' if instance.status == 'active' else 'danger' }}">
{{ instance.status|title }}
</span>
{% if instance.status_details %}
<small class="text-muted d-block mt-1">{{ instance.status_details }}</small>
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Authentication Status Card -->
<div class="col-md-6">
<div class="card h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="status-icon-wrapper">
<i class="fas fa-shield-alt {% if instance.connection_token %}text-success{% else %}text-warning{% endif %} fa-2x"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h5 class="card-title mb-1">Authentication Status</h5>
<p class="card-text mb-0">
<span class="badge bg-{{ 'success' if instance.connection_token else 'warning' }}">
{{ 'Authenticated' if instance.connection_token else 'Not Authenticated' }}
</span>
{% if not instance.connection_token %}
<small class="text-muted d-block mt-1">This instance needs to be authenticated</small>
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabs Section -->
<div class="card">
<div class="card-header bg-white">
<ul class="nav nav-tabs card-header-tabs" id="instanceTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="overview-tab" data-bs-toggle="tab" data-bs-target="#overview" type="button" role="tab" aria-controls="overview" aria-selected="true">
<i class="fas fa-building me-2"></i>Company Information
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contacts-tab" data-bs-toggle="tab" data-bs-target="#contacts" type="button" role="tab" aria-controls="contacts" aria-selected="false">
<i class="fas fa-users me-2"></i>Contacts
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="logs-tab" data-bs-toggle="tab" data-bs-target="#logs" type="button" role="tab" aria-controls="logs" aria-selected="false">
<i class="fas fa-history me-2"></i>Activity Logs
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="mails-tab" data-bs-toggle="tab" data-bs-target="#mails" type="button" role="tab" aria-controls="mails" aria-selected="false">
<i class="fas fa-envelope me-2"></i>Mail Logs
</button>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content" id="instanceTabsContent">
<!-- Overview Tab -->
<div class="tab-pane fade show active" id="overview" role="tabpanel" aria-labelledby="overview-tab">
<div class="row">
<div class="col-md-6">
<h5 class="mb-3">Company Details</h5>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;" id="company-name-label">Company Name:</div>
<div class="company-value" id="company-name-value">Loading...</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Industry:</div>
<div class="company-value" id="company-industry">Loading...</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Description:</div>
<div class="company-value" id="company-description">Loading...</div>
</div>
</div>
</div>
<div class="col-md-6">
<h5 class="mb-3">Contact Information</h5>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Email:</div>
<div class="company-value" id="company-email">Loading...</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Phone:</div>
<div class="company-value" id="company-phone">Loading...</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Website:</div>
<div class="company-value" id="company-website">Loading...</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex">
<div class="text-muted me-2" style="min-width: 120px;">Address:</div>
<div class="company-value" id="company-address">Loading...</div>
</div>
</div>
</div>
</div>
</div>
<!-- Contacts Tab -->
<div class="tab-pane fade" id="contacts" role="tabpanel" aria-labelledby="contacts-tab">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0">Contact List</h5>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addContactModal">
<i class="fas fa-plus me-2"></i>Add Contact
</button>
</div>
<!-- Contacts Table -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Company</th>
<th>Position</th>
<th>Role</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="contactsTableBody">
<tr>
<td colspan="7" class="text-center">Loading contacts...</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Logs Tab -->
<div class="tab-pane fade" id="logs" role="tabpanel" aria-labelledby="logs-tab">
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">Activity Log</h5>
<div class="d-flex gap-2">
<select class="form-select form-select-sm" id="logCategory">
<option value="">All Categories</option>
<option value="AUTH">Authentication</option>
<option value="FILE">File Operations</option>
<option value="USER">User Management</option>
<option value="SYSTEM">System</option>
</select>
<select class="form-select form-select-sm" id="dateRangeFilter">
<option value="24h">Last 24 Hours</option>
<option value="7d" selected>Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="all">All Time</option>
</select>
<button class="btn btn-secondary btn-sm" id="clearLogFiltersBtn">
<i class="fas fa-times me-1"></i>Clear Filters
</button>
</div>
</div>
<!-- Logs Table -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Timestamp</th>
<th>Event Type</th>
<th>User</th>
<th>IP Address</th>
</tr>
</thead>
<tbody id="logsTableBody">
<!-- Logs will be loaded here via JavaScript -->
</tbody>
</table>
</div>
<!-- Pagination -->
<nav aria-label="Logs pagination" class="mt-4">
<ul class="pagination justify-content-center" id="logsPagination">
<!-- Pagination will be loaded here via JavaScript -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Mails Tab -->
<div class="tab-pane fade" id="mails" role="tabpanel" aria-labelledby="mails-tab">
<div class="card shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="card-title mb-0">Mail Log</h5>
<div class="d-flex gap-2">
<select class="form-select form-select-sm" id="statusFilter">
<option value="">All Statuses</option>
<option value="pending">Pending</option>
<option value="sent">Sent</option>
<option value="failed">Failed</option>
</select>
<select class="form-select form-select-sm" id="dateRangeFilter">
<option value="24h">Last 24 Hours</option>
<option value="7d" selected>Last 7 Days</option>
<option value="30d">Last 30 Days</option>
<option value="all">All Time</option>
</select>
<button class="btn btn-secondary btn-sm" id="clearFiltersBtn">
<i class="fas fa-times me-1"></i>Clear Filters
</button>
</div>
</div>
<!-- Mail Table -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Created At</th>
<th>Recipient</th>
<th>Subject</th>
<th>Sent At</th>
</tr>
</thead>
<tbody id="mailsTableBody">
<!-- Mails will be loaded here via JavaScript -->
</tbody>
</table>
</div>
<!-- Pagination -->
<nav aria-label="Mails pagination" class="mt-4">
<ul class="pagination justify-content-center" id="mailsPagination">
<!-- Pagination will be loaded here via JavaScript -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Contact Modal -->
<div class="modal fade" id="addContactModal" tabindex="-1" aria-labelledby="addContactModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="addContactModalLabel">Add New Contact</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="addContactForm" onsubmit="addContact(event)">
<div class="mb-3">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
</div>
<div class="mb-3">
<label for="phone" class="form-label">Phone</label>
<input type="tel" class="form-control" id="phone" name="phone">
</div>
<div class="mb-3">
<label for="company" class="form-label">Company</label>
<input type="text" class="form-control" id="company" name="company">
</div>
<div class="mb-3">
<label for="position" class="form-label">Position</label>
<input type="text" class="form-control" id="position" name="position">
</div>
<div class="mb-3">
<label for="role" class="form-label">Role</label>
<select class="form-select" id="role" name="role" required>
<option value="user">Standard User</option>
<option value="manager">Manager</option>
<option value="admin">Administrator</option>
</select>
</div>
<div class="mb-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Add Contact</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit Contact Modal -->
<div class="modal fade" id="editContactModal" tabindex="-1" aria-labelledby="editContactModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editContactModalLabel">Edit Contact</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editContactForm" onsubmit="updateContact(event)">
<input type="hidden" id="edit-contact-id" name="id">
<div class="mb-3">
<label for="edit-name" class="form-label">Name</label>
<input type="text" class="form-control" id="edit-name" name="name" required>
</div>
<div class="mb-3">
<label for="edit-email" class="form-label">Email</label>
<input type="email" class="form-control" id="edit-email" name="email" required>
</div>
<div class="mb-3">
<label for="edit-phone" class="form-label">Phone</label>
<input type="tel" class="form-control" id="edit-phone" name="phone">
</div>
<div class="mb-3">
<label for="edit-company" class="form-label">Company</label>
<input type="text" class="form-control" id="edit-company" name="company">
</div>
<div class="mb-3">
<label for="edit-position" class="form-label">Position</label>
<input type="text" class="form-control" id="edit-position" name="position">
</div>
<div class="mb-3">
<label for="edit-role" class="form-label">Role</label>
<select class="form-select" id="edit-role" name="role" required>
<option value="user">Standard User</option>
<option value="manager">Manager</option>
<option value="admin">Administrator</option>
</select>
</div>
<div class="mb-3">
<label for="edit-status" class="form-label">Status</label>
<select class="form-select" id="edit-status" name="status">
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div class="text-end">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Delete Contact Modal -->
<div class="modal fade" id="deleteContactModal" tabindex="-1" aria-labelledby="deleteContactModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteContactModalLabel">Delete Contact</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this contact? This action cannot be undone.</p>
<div class="text-end">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDelete">Delete</button>
</div>
</div>
</div>
</div>
</div>
<!-- Log Details Modal -->
<div class="modal fade" id="logDetailsModal" tabindex="-1" aria-labelledby="logDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="logDetailsModalLabel">Log Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="logDetailsContent">
<!-- Log details will be loaded here -->
</div>
</div>
</div>
</div>
<!-- Mail Details Modal -->
<div class="modal fade" id="mailDetailsModal" tabindex="-1" aria-labelledby="mailDetailsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mailDetailsModalLabel">Mail Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="mailDetailsContent">
<!-- Mail details will be loaded here -->
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Basic test to verify script execution
console.log('Script block loaded');
// Test function to verify DOM elements exist
function testElements() {
console.log('Testing DOM elements...');
const elements = [
'company-name-value',
'company-industry',
'company-description',
'company-email',
'company-phone',
'company-website',
'company-address'
];
elements.forEach(id => {
const element = document.getElementById(id);
console.log(`Element ${id} exists:`, !!element);
});
}
let statusUpdateInterval;
// Function to check instance status
async function checkInstanceStatus() {
console.log('checkInstanceStatus called');
try {
const response = await fetch(`/instances/{{ instance.id }}/status`);
if (!response.ok) throw new Error('Failed to check instance status');
const data = await response.json();
console.log('Status data:', data);
// Update activity status
const activityIcon = document.querySelector('.card:first-child .fa-circle-notch');
const activityBadge = document.querySelector('.card:first-child .badge');
const activityDetails = document.querySelector('.card:first-child .text-muted');
if (activityIcon) {
activityIcon.className = `fas fa-circle-notch fa-spin ${data.status === 'active' ? 'text-success' : 'text-danger'} fa-2x`;
}
if (activityBadge) {
activityBadge.className = `badge bg-${data.status === 'active' ? 'success' : 'danger'}`;
activityBadge.textContent = data.status.charAt(0).toUpperCase() + data.status.slice(1);
}
if (activityDetails) {
// Only update if we have status details
if (data.status_details) {
activityDetails.textContent = data.status_details;
activityDetails.style.display = 'block';
} else {
activityDetails.style.display = 'none';
}
}
} catch (error) {
console.error('Error checking instance status:', error);
}
}
// Function to check authentication status
async function checkAuthStatus() {
console.log('checkAuthStatus called');
try {
const response = await fetch(`/instances/{{ instance.id }}/auth-status`);
if (!response.ok) throw new Error('Failed to check authentication status');
const data = await response.json();
console.log('Auth data:', data);
// Update authentication status
const authIcon = document.querySelector('.card:nth-child(2) .fa-shield-alt');
const authBadge = document.querySelector('.card:nth-child(2) .badge');
const authDetails = document.querySelector('.card:nth-child(2) .text-muted');
if (authIcon) {
authIcon.className = `fas fa-shield-alt ${data.authenticated ? 'text-success' : 'text-warning'} fa-2x`;
}
if (authBadge) {
authBadge.className = `badge bg-${data.authenticated ? 'success' : 'warning'}`;
authBadge.textContent = data.authenticated ? 'Authenticated' : 'Not Authenticated';
}
if (authDetails) {
authDetails.textContent = data.authenticated ? '' : 'This instance needs to be authenticated';
}
} catch (error) {
console.error('Error checking authentication status:', error);
}
}
// Function to fetch company information
async function fetchCompanyInfo() {
console.log('fetchCompanyInfo called');
try {
// First get JWT token
console.log('Getting management token...');
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
console.log('Token response status:', tokenResponse.status);
if (!tokenResponse.ok) {
console.error('Failed to get management token:', tokenResponse.status);
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
console.log('Token data received:', tokenData);
if (!tokenData.token) {
console.error('No token in response');
throw new Error('No token received');
}
// Then fetch settings using the JWT token
console.log('Fetching settings from:', `{{ instance.main_url }}/api/admin/settings`);
const response = await fetch(`{{ instance.main_url }}/api/admin/settings`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
console.log('Settings response status:', response.status);
if (!response.ok) {
console.error('Settings fetch failed:', response.status);
throw new Error('Failed to fetch company information');
}
const data = await response.json();
console.log('Settings data received:', data);
// Update company name separately with debugging
const companyNameElement = document.getElementById('company-name-value');
const companyNameLabel = document.getElementById('company-name-label');
// Ensure label text is set and maintained
if (companyNameLabel) {
companyNameLabel.textContent = 'Company Name:';
}
if (companyNameElement) {
companyNameElement.textContent = data.company_name || 'Not set';
}
// Update other company details
const updateElement = (id, value) => {
const element = document.getElementById(id);
if (element && element.classList.contains('company-value')) {
element.textContent = value || 'Not set';
}
};
// Update other company details
updateElement('company-industry', data.company_industry);
updateElement('company-description', data.company_description);
// Update contact information
const emailElement = document.getElementById('company-email');
const phoneElement = document.getElementById('company-phone');
const websiteElement = document.getElementById('company-website');
// Update email with mailto link
if (emailElement && emailElement.classList.contains('company-value')) {
if (data.company_email && data.company_email !== 'Not set') {
emailElement.innerHTML = `<a href="mailto:${data.company_email}" class="text-decoration-none" style="color: var(--primary-color);">
<i class="fas fa-envelope me-2"></i>${data.company_email}
</a>`;
} else {
emailElement.textContent = 'Not set';
}
}
// Update phone with tel link
if (phoneElement && phoneElement.classList.contains('company-value')) {
if (data.company_phone && data.company_phone !== 'Not set') {
phoneElement.innerHTML = `<a href="tel:${data.company_phone}" class="text-decoration-none" style="color: var(--primary-color);">
<i class="fas fa-phone me-2"></i>${data.company_phone}
</a>`;
} else {
phoneElement.textContent = 'Not set';
}
}
// Update website with external link
if (websiteElement && websiteElement.classList.contains('company-value')) {
if (data.company_website && data.company_website !== 'Not set') {
// Ensure website has http/https prefix
let websiteUrl = data.company_website;
if (!websiteUrl.startsWith('http://') && !websiteUrl.startsWith('https://')) {
websiteUrl = 'https://' + websiteUrl;
}
websiteElement.innerHTML = `<a href="${websiteUrl}" target="_blank" rel="noopener noreferrer" class="text-decoration-none" style="color: var(--primary-color);">
<i class="fas fa-globe me-2"></i>${data.company_website}
</a>`;
} else {
websiteElement.textContent = 'Not set';
}
}
// Format address
const addressParts = [
data.company_address,
data.company_city,
data.company_state,
data.company_zip,
data.company_country
].filter(Boolean);
updateElement('company-address', addressParts.length ? addressParts.join(', ') : 'Not set');
console.log('Company info update complete');
} catch (error) {
console.error('Error in fetchCompanyInfo:', error);
// Show error state in all fields
const fields = ['company-name-value', 'company-industry', 'company-description',
'company-email', 'company-phone', 'company-website', 'company-address'];
fields.forEach(field => {
const element = document.getElementById(field);
if (element && element.classList.contains('company-value')) {
element.textContent = 'Error loading data';
}
});
}
}
// Function to update all statuses
async function updateAllStatuses() {
console.log('Starting updateAllStatuses...');
try {
await Promise.all([
checkInstanceStatus(),
checkAuthStatus(),
fetchCompanyInfo()
]);
console.log('All statuses updated successfully');
} catch (error) {
console.error('Error in updateAllStatuses:', error);
}
}
// Initialize status updates
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded, initializing updates...');
testElements(); // Test if elements exist
// Initial update
updateAllStatuses();
// Set up periodic updates (every 30 seconds)
statusUpdateInterval = setInterval(updateAllStatuses, 30000);
console.log('Periodic updates initialized');
});
// Clean up interval when page is unloaded
window.addEventListener('beforeunload', function() {
if (statusUpdateInterval) {
clearInterval(statusUpdateInterval);
}
});
// Contacts Management Functions
async function fetchContacts() {
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const response = await fetch(`{{ instance.main_url }}/api/admin/contacts`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch contacts');
}
const contacts = await response.json();
const contactsList = document.getElementById('contactsTableBody');
contactsList.innerHTML = '';
contacts.forEach(contact => {
const row = document.createElement('tr');
// Role badge color mapping
const roleBadgeClass = {
'admin': 'bg-danger',
'manager': 'bg-warning',
'user': 'bg-info'
}[contact.role] || 'bg-secondary';
// Status badge color mapping
const statusBadgeClass = contact.is_active ? 'bg-success' : 'bg-secondary';
// Format phone number for tel: link (remove any non-digit characters)
const phoneNumber = contact.phone ? contact.phone.replace(/\D/g, '') : '';
row.innerHTML = `
<td>${contact.username} ${contact.last_name}</td>
<td>
<a href="mailto:${contact.email}" class="text-decoration-none">
<span class="badge bg-primary">
<i class="fas fa-envelope me-1"></i>${contact.email}
</span>
</a>
</td>
<td>
${contact.phone ? `
<a href="tel:${phoneNumber}" class="text-decoration-none">
<span class="badge bg-secondary">
<i class="fas fa-phone me-1"></i>${contact.phone}
</span>
</a>
` : '-'}
</td>
<td>${contact.company || '-'}</td>
<td>${contact.position || '-'}</td>
<td><span class="badge ${roleBadgeClass}">${contact.role.charAt(0).toUpperCase() + contact.role.slice(1)}</span></td>
<td><span class="badge ${statusBadgeClass}">${contact.is_active ? 'Active' : 'Inactive'}</span></td>
<td>
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editContact(${contact.id})">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteContact(${contact.id})">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
contactsList.appendChild(row);
});
} catch (error) {
console.error('Error fetching contacts:', error);
showToast('Error loading contacts', 'error');
}
}
async function addContact(event) {
event.preventDefault();
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const formData = new FormData(event.target);
const response = await fetch(`{{ instance.main_url }}/api/admin/contacts`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
},
body: JSON.stringify({
username: formData.get('name'),
email: formData.get('email'),
phone: formData.get('phone'),
company: formData.get('company'),
position: formData.get('position'),
role: formData.get('role'),
is_active: formData.get('status') === 'active'
})
});
if (!response.ok) {
throw new Error('Failed to add contact');
}
// Close modal and refresh list
const modal = bootstrap.Modal.getInstance(document.getElementById('addContactModal'));
modal.hide();
fetchContacts();
// Show success message
showToast('Contact added successfully', 'success');
} catch (error) {
console.error('Error adding contact:', error);
showToast('Error adding contact', 'error');
}
}
async function editContact(id) {
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const response = await fetch(`{{ instance.main_url }}/api/admin/contacts/${id}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch contact details');
}
const contact = await response.json();
// Populate form
document.getElementById('edit-contact-id').value = contact.id;
document.getElementById('edit-name').value = contact.username;
document.getElementById('edit-email').value = contact.email || '';
document.getElementById('edit-phone').value = contact.phone || '';
document.getElementById('edit-company').value = contact.company || '';
document.getElementById('edit-position').value = contact.position || '';
document.getElementById('edit-role').value = contact.role;
document.getElementById('edit-status').value = contact.is_active ? 'active' : 'inactive';
// Show modal
const modal = new bootstrap.Modal(document.getElementById('editContactModal'));
modal.show();
} catch (error) {
console.error('Error fetching contact details:', error);
showToast('Error loading contact details', 'error');
}
}
async function updateContact(event) {
event.preventDefault();
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const formData = new FormData(event.target);
const id = formData.get('id');
const response = await fetch(`{{ instance.main_url }}/api/admin/contacts/${id}`, {
method: 'PUT',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
},
body: JSON.stringify({
username: formData.get('name'),
email: formData.get('email'),
phone: formData.get('phone'),
company: formData.get('company'),
position: formData.get('position'),
role: formData.get('role'),
is_active: formData.get('status') === 'active'
})
});
if (!response.ok) {
throw new Error('Failed to update contact');
}
// Close modal and refresh list
const modal = bootstrap.Modal.getInstance(document.getElementById('editContactModal'));
modal.hide();
fetchContacts();
// Show success message
showToast('Contact updated successfully', 'success');
} catch (error) {
console.error('Error updating contact:', error);
showToast('Error updating contact', 'error');
}
}
async function deleteContact(id) {
// Store the contact ID for the delete confirmation
const deleteModal = new bootstrap.Modal(document.getElementById('deleteContactModal'));
document.getElementById('confirmDelete').onclick = async () => {
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const response = await fetch(`{{ instance.main_url }}/api/admin/contacts/${id}`, {
method: 'DELETE',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) {
throw new Error('Failed to delete contact');
}
// Close modal and refresh list
deleteModal.hide();
fetchContacts();
// Show success message
showToast('Contact deleted successfully', 'success');
} catch (error) {
console.error('Error deleting contact:', error);
showToast('Error deleting contact', 'error');
}
};
// Show the delete confirmation modal
deleteModal.show();
}
// Helper function to show toast messages
function showToast(title, message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<strong>${title}</strong><br>
${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
const toastContainer = document.getElementById('toastContainer') || (() => {
const container = document.createElement('div');
container.id = 'toastContainer';
container.className = 'toast-container position-fixed bottom-0 end-0 p-3';
document.body.appendChild(container);
return container;
})();
toastContainer.appendChild(toast);
const bsToast = new bootstrap.Toast(toast);
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
// Event Listeners
document.addEventListener('DOMContentLoaded', function() {
// ... existing event listeners ...
// Fetch contacts when contacts tab is shown
document.getElementById('contacts-tab').addEventListener('shown.bs.tab', fetchContacts);
// Fetch logs when logs tab is shown
document.getElementById('logs-tab').addEventListener('shown.bs.tab', fetchLogs);
// Fetch mails when mails tab is shown
document.getElementById('mails-tab').addEventListener('shown.bs.tab', fetchMails);
// Debug the company name label
const label = document.getElementById('company-name-label');
if (label) {
console.log('Company name label found:', label);
// Create a MutationObserver to watch for changes
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log('Label changed:', mutation);
console.log('Old value:', mutation.oldValue);
console.log('New value:', label.textContent);
});
});
// Start observing
observer.observe(label, {
attributes: true,
childList: true,
characterData: true,
subtree: true
});
} else {
console.log('Company name label not found');
}
});
// Function to fetch logs
async function fetchLogs() {
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const response = await fetch(`{{ instance.main_url }}/api/admin/logs`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch logs');
}
const data = await response.json();
const logsTableBody = document.getElementById('logsTableBody');
logsTableBody.innerHTML = '';
// Fetch all users to map IDs to names
const usersResponse = await fetch(`{{ instance.main_url }}/api/admin/contacts`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!usersResponse.ok) {
throw new Error('Failed to fetch users');
}
const usersData = await usersResponse.json();
const userMap = new Map(usersData.map(user => [user.id, `${user.username} ${user.last_name}`]));
data.events.forEach(event => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${new Date(event.timestamp).toLocaleString()}</td>
<td>${event.event_type || '-'}</td>
<td>${event.user_id ? userMap.get(event.user_id) || `User ${event.user_id}` : '-'}</td>
<td>${event.ip_address || '-'}</td>
`;
logsTableBody.appendChild(row);
});
// Update pagination
updateLogsPagination(data.current_page, data.pages);
} catch (error) {
console.error('Error fetching logs:', error);
showToast('Error loading logs', 'error');
}
}
// Function to fetch mails
async function fetchMails() {
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const response = await fetch(`{{ instance.main_url }}/api/admin/mail-logs`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch mail logs');
}
const data = await response.json();
const mailsTableBody = document.querySelector('#mails .table tbody');
mailsTableBody.innerHTML = '';
data.mails.forEach(mail => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${new Date(mail.created_at).toLocaleString()}</td>
<td>${mail.recipient}</td>
<td>${mail.subject}</td>
<td>${mail.sent_at ? new Date(mail.sent_at).toLocaleString() : '-'}</td>
`;
mailsTableBody.appendChild(row);
});
// Update pagination
updateMailsPagination(data.current_page, data.pages);
} catch (error) {
console.error('Error fetching mail logs:', error);
showToast('Error loading mail logs', 'error');
}
}
// Helper function to get mail status badge class
function getMailStatusBadgeClass(status) {
const classes = {
'pending': 'warning',
'sent': 'success',
'failed': 'danger'
};
return classes[status] || 'secondary';
}
// Function to update logs pagination
function updateLogsPagination(currentPage, totalPages) {
const pagination = document.getElementById('logsPagination');
if (!pagination) return;
pagination.innerHTML = '';
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `
<button class="page-link" onclick="changeLogsPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
<i class="fas fa-chevron-left"></i>
</button>
`;
pagination.appendChild(prevLi);
// Page numbers
for (let i = 1; i <= totalPages; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `
<button class="page-link" onclick="changeLogsPage(${i})">${i}</button>
`;
pagination.appendChild(li);
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `
<button class="page-link" onclick="changeLogsPage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>
<i class="fas fa-chevron-right"></i>
</button>
`;
pagination.appendChild(nextLi);
}
// Function to update mails pagination
function updateMailsPagination(currentPage, totalPages) {
const pagination = document.querySelector('#mails .pagination');
if (!pagination) return;
pagination.innerHTML = '';
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `
<button class="page-link" onclick="changeMailsPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>
<i class="fas fa-chevron-left"></i>
</button>
`;
pagination.appendChild(prevLi);
// Page numbers
for (let i = 1; i <= totalPages; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === currentPage ? 'active' : ''}`;
li.innerHTML = `
<button class="page-link" onclick="changeMailsPage(${i})">${i}</button>
`;
pagination.appendChild(li);
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`;
nextLi.innerHTML = `
<button class="page-link" onclick="changeMailsPage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>
<i class="fas fa-chevron-right"></i>
</button>
`;
pagination.appendChild(nextLi);
}
// Function to change logs page
async function changeLogsPage(page) {
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const response = await fetch(`{{ instance.main_url }}/api/admin/logs?page=${page}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch logs');
}
const data = await response.json();
// Fetch all users to map IDs to names
const usersResponse = await fetch(`{{ instance.main_url }}/api/admin/contacts`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!usersResponse.ok) {
throw new Error('Failed to fetch users');
}
const usersData = await usersResponse.json();
const userMap = new Map(usersData.map(user => [user.id, `${user.username} ${user.last_name}`]));
const logsTableBody = document.getElementById('logsTableBody');
logsTableBody.innerHTML = '';
data.events.forEach(event => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${new Date(event.timestamp).toLocaleString()}</td>
<td>${event.event_type || '-'}</td>
<td>${event.user_id ? userMap.get(event.user_id) || `User ${event.user_id}` : '-'}</td>
<td>${event.ip_address || '-'}</td>
`;
logsTableBody.appendChild(row);
});
// Update pagination
updateLogsPagination(data.current_page, data.pages);
} catch (error) {
console.error('Error changing logs page:', error);
showToast('Error loading logs', 'error');
}
}
// Function to change mails page
async function changeMailsPage(page) {
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const response = await fetch(`{{ instance.main_url }}/api/admin/mail-logs?page=${page}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch mail logs');
}
const data = await response.json();
const mailsTableBody = document.querySelector('#mails .table tbody');
mailsTableBody.innerHTML = '';
data.mails.forEach(mail => {
const row = document.createElement('tr');
row.innerHTML = `
<td>${new Date(mail.created_at).toLocaleString()}</td>
<td>${mail.recipient}</td>
<td>${mail.subject}</td>
<td>${mail.sent_at ? new Date(mail.sent_at).toLocaleString() : '-'}</td>
`;
mailsTableBody.appendChild(row);
});
// Update pagination
updateMailsPagination(data.current_page, data.pages);
} catch (error) {
console.error('Error changing mail logs page:', error);
showToast('Error loading mail logs', 'error');
}
}
// Function to view mail details
async function viewMailDetails(mailId) {
try {
// First get JWT token
const tokenResponse = await fetch(`{{ instance.main_url }}/api/admin/management-token`, {
method: 'POST',
headers: {
'X-API-Key': '{{ instance.connection_token }}',
'Accept': 'application/json'
}
});
if (!tokenResponse.ok) {
throw new Error('Failed to get management token');
}
const tokenData = await tokenResponse.json();
if (!tokenData.token) {
throw new Error('No token received');
}
const response = await fetch(`{{ instance.main_url }}/api/admin/mail-logs/${mailId}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${tokenData.token}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch mail details');
}
const mail = await response.json();
// Update modal content
document.getElementById('modalRecipient').textContent = mail.recipient;
document.getElementById('modalStatus').innerHTML = `
<span class="badge ${getMailStatusBadgeClass(mail.status)}">${mail.status}</span>
`;
document.getElementById('modalTemplate').textContent = mail.template_id ? mail.template_id : '-';
document.getElementById('modalCreatedAt').textContent = new Date(mail.created_at).toLocaleString();
document.getElementById('modalSentAt').textContent = mail.sent_at ? new Date(mail.sent_at).toLocaleString() : '-';
document.getElementById('modalSubject').textContent = mail.subject;
document.getElementById('modalBody').innerHTML = mail.body;
// Show modal
const modal = new bootstrap.Modal(document.getElementById('mailDetailsModal'));
modal.show();
} catch (error) {
console.error('Error viewing mail details:', error);
showToast('Error loading mail details', 'error');
}
}
// Function to view log details
function viewLogDetails(event) {
const modal = new bootstrap.Modal(document.getElementById('logDetailsModal'));
const content = document.getElementById('logDetailsContent');
// Format the details in a readable way
let details = event.details;
let formattedDetails = 'No details available';
if (details) {
if (typeof details === 'string') {
formattedDetails = details.replace(/\n/g, '<br>');
} else if (typeof details === 'object') {
formattedDetails = JSON.stringify(details, null, 2).replace(/\n/g, '<br>');
} else {
formattedDetails = String(details);
}
}
content.innerHTML = `
<div class="mb-3">
<strong>Timestamp:</strong> ${new Date(event.timestamp).toLocaleString()}
</div>
<div class="mb-3">
<strong>Event Type:</strong> ${event.event_type || '-'}
</div>
<div class="mb-3">
<strong>User:</strong> ${event.user_id ? userMap.get(event.user_id) || `User ${event.user_id}` : '-'}
</div>
<div class="mb-3">
<strong>IP Address:</strong> ${event.ip_address || '-'}
</div>
<div class="mb-3">
<strong>Details:</strong>
<div class="mt-2 p-3 bg-light rounded">
${formattedDetails}
</div>
</div>
`;
modal.show();
}
// Add event listener for clear filters button
document.addEventListener('DOMContentLoaded', function() {
// ... existing event listeners ...
const clearLogFiltersBtn = document.getElementById('clearLogFiltersBtn');
if (clearLogFiltersBtn) {
clearLogFiltersBtn.addEventListener('click', function() {
document.getElementById('logCategory').value = '';
document.getElementById('dateRangeFilter').value = '7d';
fetchLogs();
});
}
});
</script>
{% endblock %}