2025-06-19 14:25:42 -04:00
<!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" >
2025-10-20 17:40:04 -04:00
< link rel = "stylesheet" href = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" >
2025-10-20 18:16:45 -04:00
< 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 >
2025-06-19 14:25:42 -04:00
< / head >
< body >
2025-10-20 18:16:45 -04:00
<!-- 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 >
2025-06-19 14:25:42 -04:00
2025-06-19 14:35:13 -04:00
{% 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 %}
2025-10-20 18:16:45 -04:00
<!-- 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 >
2025-06-19 14:25:42 -04:00
< / div >
2025-10-20 18:16:45 -04:00
< / 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 >
2025-06-19 14:35:13 -04:00
< / div >
2025-06-19 14:25:42 -04:00
< / div >
2025-10-20 18:16:45 -04:00
< / 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 >
< form method = "POST" action = "/tenant/{{ tenant.id }}/delete" class = "d-inline" onsubmit = "return confirmDelete('{{ tenant.name }}')" >
< button type = "submit" class = "btn btn-danger btn-sm" >
< i class = "bi bi-trash" > < / i > Delete
< / button >
< / form >
< / div >
2025-06-19 14:25:42 -04:00
< / div >
< / div >
2025-10-20 18:16:45 -04:00
{% 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 >
2025-06-19 14:25:42 -04:00
< / div >
< / div >
2025-10-20 18:16:45 -04:00
{% 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 >
2025-06-19 14:25:42 -04:00
< / div >
< / div >
< / div >
< / div >
2025-10-20 17:40:04 -04:00
<!-- 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" >
2025-10-20 18:16:45 -04:00
< h5 class = "modal-title" id = "qrModalLabel-{{ tenant.id }}" > AR Template URL - {{ tenant.name }}< / h5 >
2025-10-20 17:40:04 -04:00
< button type = "button" class = "btn-close" data-bs-dismiss = "modal" aria-label = "Close" > < / button >
< / div >
< div class = "modal-body text-center" >
2025-10-20 18:16:45 -04:00
< p class = "text-muted mb-3" > Scan this QR code for Knox Capture AR:< / p >
2025-10-20 17:40:04 -04:00
< 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 %}
2025-06-19 14:35:13 -04:00
< script src = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" > < / script >
2025-06-19 14:25:42 -04:00
< script >
2025-10-20 18:16:45 -04:00
const reservedIds = ['admin', 'api', 'login', 'logout', 'arcontentfields', 'arinfo',
2025-06-19 14:35:13 -04:00
'images', 'barcodes', 'static', 'assets', 'js', 'css', 'tenant',
'auth', 'oauth', 'callback', 'webhook', 'health', 'status'];
2025-10-20 18:16:45 -04:00
// Create tenant form submission
2025-06-19 14:25:42 -04:00
document.getElementById('newTenantForm').addEventListener('submit', function(e) {
e.preventDefault();
const tenantId = document.getElementById('tenantId').value;
if (tenantId) {
2025-06-19 16:08:18 -04:00
const normalizedId = tenantId.toLowerCase();
if (reservedIds.includes(normalizedId)) {
2025-06-19 14:35:13 -04:00
alert(`"${tenantId}" is a reserved name and cannot be used as a tenant ID.`);
return;
}
2025-06-19 16:08:18 -04:00
window.location.href = '/' + normalizedId + '/';
2025-06-19 14:25:42 -04:00
}
});
2025-10-20 18:16:45 -04:00
// 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 confirmation
2025-06-19 14:35:13 -04:00
function confirmDelete(tenantName) {
return confirm(`Are you sure you want to delete the tenant "${tenantName}"?\n\nThis will permanently delete all products and data associated with this tenant.`);
}
2025-06-19 14:25:42 -04:00
< / script >
< / body >
< / html >