Files
KCAPDemoServer/src/app/templates/tenant_selection.html
Matt Hills 0ef9020d3b Fix tenant deletion page not found error and improve delete confirmation
- Fixed incorrect route path from /tenant/{id}/delete to /{id}/delete
- Replaced browser confirm() dialog with Bootstrap modal for better UX
- Added styled confirmation modal with warning message
- Modal displays tenant name and ID before deletion
2025-10-20 18:26:49 -04:00

315 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Select or Create Tenant - KCAP Demo Server</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<style>
.tenant-card {
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
height: 100%;
}
.tenant-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.tenant-card-clickable {
text-decoration: none;
color: inherit;
display: block;
}
.settings-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.search-container {
position: sticky;
top: 0;
background: white;
z-index: 100;
padding: 20px 0;
}
.tenant-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
.create-tenant-card {
border: 2px dashed #dee2e6;
background: #f8f9fa;
}
.create-tenant-card:hover {
border-color: #0d6efd;
background: #e7f1ff;
}
</style>
</head>
<body>
<!-- Top Right Action Buttons -->
<div class="settings-btn">
<button class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#createTenantModal" title="New Tenant">
<i class="bi bi-plus-circle-fill"></i>
</button>
<a href="/settings" class="btn btn-secondary" title="Server Settings">
<i class="bi bi-gear-fill"></i>
</a>
</div>
<div class="container-fluid px-4 py-4">
<h1 class="text-center mb-4">KCAP Demo Server</h1>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'danger' }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<!-- Search and Filter -->
<div class="search-container">
<div class="row mb-3">
<div class="col-md-8 offset-md-2">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" id="searchTenants" class="form-control" placeholder="Search tenants...">
<button class="btn btn-outline-secondary" type="button" id="clearSearch">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Tenant Grid -->
<div class="tenant-grid" id="tenantGrid">
{% if tenants %}
{% for tenant in tenants %}
<div class="tenant-item" data-tenant-name="{{ tenant.name|lower }}" data-tenant-id="{{ tenant.id|lower }}">
<div class="card tenant-card">
<a href="/{{ tenant.id }}/" class="tenant-card-clickable">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">{{ tenant.name }}</h5>
<span class="badge bg-secondary">{{ tenant.id }}</span>
</div>
<p class="card-text text-muted small mb-2">
<i class="bi bi-person"></i> {{ tenant.username }}
</p>
<p class="card-text text-muted small mb-2">
<i class="bi bi-calendar"></i> {{ tenant.created_at }}
</p>
<div class="mb-2">
<small class="text-muted">AR Template URL:</small>
<div class="input-group input-group-sm">
<input type="text" class="form-control form-control-sm" value="{{ server_url }}/{{ tenant.id }}/" readonly onclick="this.select()">
</div>
</div>
</div>
</a>
<div class="card-footer bg-white d-flex justify-content-between" onclick="event.stopPropagation();">
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#qrModal-{{ tenant.id }}">
<i class="bi bi-qr-code"></i> QR
</button>
<button type="button" class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#deleteTenantModal" data-tenant-id="{{ tenant.id }}" data-tenant-name="{{ tenant.name }}">
<i class="bi bi-trash"></i> Delete
</button>
<form id="delete-tenant-form-{{ tenant.id }}" method="POST" action="/{{ tenant.id }}/delete" style="display: none;">
</form>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="col-12">
<div class="text-center py-5">
<i class="bi bi-inbox" style="font-size: 4rem; color: #dee2e6;"></i>
<p class="text-muted mt-3">No tenants exist yet. Create your first tenant!</p>
</div>
</div>
{% endif %}
</div>
<!-- No Results Message -->
<div id="noResults" class="text-center py-5" style="display: none;">
<i class="bi bi-search" style="font-size: 4rem; color: #dee2e6;"></i>
<p class="text-muted mt-3">No tenants found matching your search.</p>
</div>
</div>
<!-- Create Tenant Modal -->
<div class="modal fade" id="createTenantModal" tabindex="-1" aria-labelledby="createTenantModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createTenantModalLabel">Create New Tenant</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="newTenantForm">
<div class="mb-3">
<label for="tenantId" class="form-label">Tenant ID</label>
<input type="text" class="form-control" id="tenantId" placeholder="e.g., demo-company" required pattern="[a-zA-Z0-9_-]+">
<small class="form-text text-muted">Only letters, numbers, hyphens, and underscores allowed</small>
</div>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<small>Default credentials: <strong>admin</strong> / <strong>admin</strong></small>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="newTenantForm" class="btn btn-primary">Create & Enter</button>
</div>
</div>
</div>
</div>
<!-- QR Code Modals for each tenant -->
{% for tenant in tenants %}
<div class="modal fade" id="qrModal-{{ tenant.id }}" tabindex="-1" aria-labelledby="qrModalLabel-{{ tenant.id }}" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="qrModalLabel-{{ tenant.id }}">AR Template URL - {{ tenant.name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body text-center">
<p class="text-muted mb-3">Scan this QR code for Knox Capture AR:</p>
<div class="mb-3">
<img src="/{{ tenant.id }}/qrcode/template" alt="AR Template QR Code" class="img-fluid" style="max-width: 300px;">
</div>
<code class="d-block mt-2">{{ server_url }}/{{ tenant.id }}/</code>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- Delete Tenant Confirmation Modal -->
<div class="modal fade" id="deleteTenantModal" tabindex="-1" aria-labelledby="deleteTenantModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="deleteTenantModalLabel">
<i class="bi bi-exclamation-triangle-fill me-2"></i>Confirm Tenant Deletion
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-3">Are you sure you want to delete the tenant <strong id="delete-tenant-name"></strong>?</p>
<div class="alert alert-danger mb-0">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>Warning:</strong> This will permanently delete all products and data associated with this tenant. This action cannot be undone.
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-1"></i>Cancel
</button>
<button type="button" class="btn btn-danger" id="confirm-delete-tenant-btn">
<i class="bi bi-trash me-1"></i>Delete Tenant
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const reservedIds = ['admin', 'api', 'login', 'logout', 'arcontentfields', 'arinfo',
'images', 'barcodes', 'static', 'assets', 'js', 'css', 'tenant',
'auth', 'oauth', 'callback', 'webhook', 'health', 'status'];
// Create tenant form submission
document.getElementById('newTenantForm').addEventListener('submit', function(e) {
e.preventDefault();
const tenantId = document.getElementById('tenantId').value;
if (tenantId) {
const normalizedId = tenantId.toLowerCase();
if (reservedIds.includes(normalizedId)) {
alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`);
return;
}
window.location.href = '/' + normalizedId + '/';
}
});
// Search functionality
const searchInput = document.getElementById('searchTenants');
const clearButton = document.getElementById('clearSearch');
const tenantItems = document.querySelectorAll('.tenant-item');
const noResults = document.getElementById('noResults');
const tenantGrid = document.getElementById('tenantGrid');
function filterTenants() {
const searchTerm = searchInput.value.toLowerCase().trim();
let visibleCount = 0;
tenantItems.forEach(item => {
const tenantName = item.getAttribute('data-tenant-name');
const tenantId = item.getAttribute('data-tenant-id');
if (tenantName.includes(searchTerm) || tenantId.includes(searchTerm)) {
item.style.display = '';
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Show/hide no results message
if (visibleCount === 0 && searchTerm !== '') {
tenantGrid.style.display = 'none';
noResults.style.display = 'block';
} else {
tenantGrid.style.display = 'grid';
noResults.style.display = 'none';
}
}
searchInput.addEventListener('input', filterTenants);
clearButton.addEventListener('click', function() {
searchInput.value = '';
filterTenants();
searchInput.focus();
});
// Delete tenant confirmation modal functionality
const deleteTenantModal = document.getElementById('deleteTenantModal');
if (deleteTenantModal) {
deleteTenantModal.addEventListener('show.bs.modal', function (event) {
// Button that triggered the modal
const button = event.relatedTarget;
// Extract tenant info from data attributes
const tenantId = button.getAttribute('data-tenant-id');
const tenantName = button.getAttribute('data-tenant-name');
// Update modal content
const modalTenantName = document.getElementById('delete-tenant-name');
modalTenantName.textContent = `"${tenantName}" (ID: ${tenantId})`;
// Setup the confirm button action
const confirmDeleteBtn = document.getElementById('confirm-delete-tenant-btn');
confirmDeleteBtn.onclick = function() {
document.getElementById('delete-tenant-form-' + tenantId).submit();
};
});
}
</script>
</body>
</html>