- Add Dockerfile, docker-compose.yml, .dockerignore for containerised deployment - Add deploy/ scripts (deploy.sh, nginx/apache configs, password generator) - Replace MSAL/Azure AD auth with local username/password authentication - Add login.html template - Simplify app.py, middleware, and auth routes for production use - Update gunicorn_config.py and wsgi.py for Docker/production - Update templates to work with new auth and URL prefix handling Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
231 lines
11 KiB
HTML
231 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>QC Report Dashboard</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
|
</head>
|
|
<body>
|
|
<nav class="navbar navbar-dark bg-dark">
|
|
<div class="container-fluid">
|
|
<span class="navbar-brand mb-0 h1">QC Report Dashboard</span>
|
|
<div id="userInfo" class="d-flex align-items-center" style="display: none !important;">
|
|
<span id="userName" class="text-white me-3"></span>
|
|
<button id="logoutBtn" class="btn btn-sm btn-outline-light">Logout</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Authentication Loading State -->
|
|
<div id="authLoading" class="container mt-5" style="display: none;">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-6 text-center">
|
|
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 text-muted">Authenticating...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Authentication Required Screen -->
|
|
<div id="authRequired" class="container mt-5" style="display: none;">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-6">
|
|
<div class="card shadow text-center">
|
|
<div class="card-body p-5">
|
|
<i class="bi bi-shield-lock" style="font-size: 4rem; color: #0d6efd;"></i>
|
|
<h3 class="mt-4 mb-3">Authentication Required</h3>
|
|
<p class="text-muted mb-4">
|
|
Please sign in with your Microsoft account to access the HM QC Report Dashboard
|
|
</p>
|
|
<button id="loginBtn" class="btn btn-primary btn-lg">
|
|
<i class="bi bi-microsoft"></i> Sign in with Microsoft
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content (show only when authenticated) -->
|
|
<div id="mainContent" style="display: none;">
|
|
<div class="container mt-5">
|
|
<div class="row justify-content-center">
|
|
<div class="col-md-8 col-lg-6">
|
|
<div class="card shadow">
|
|
<div class="card-body p-5">
|
|
<h2 class="card-title text-center mb-4">Search QC Reports</h2>
|
|
<p class="text-muted text-center mb-4">
|
|
Enter a job number to view all QC reports and quality checks
|
|
</p>
|
|
|
|
<form id="searchForm">
|
|
<div class="mb-3">
|
|
<label for="jobNumber" class="form-label">Job Number</label>
|
|
<input
|
|
type="text"
|
|
class="form-control form-control-lg"
|
|
id="jobNumber"
|
|
placeholder="e.g., 10107-06, 1022A, 9000_10107-06"
|
|
required
|
|
autocomplete="off"
|
|
>
|
|
<div class="form-text">
|
|
Enter the reference/job number from the filename
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-grid">
|
|
<button type="submit" class="btn btn-primary btn-lg" id="searchBtn">
|
|
<span id="searchBtnText">Search Reports</span>
|
|
<span id="searchBtnSpinner" class="spinner-border spinner-border-sm ms-2 d-none" role="status" aria-hidden="true"></span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Progress Bar -->
|
|
<div id="progressContainer" class="mt-4 d-none">
|
|
<div class="card border-primary">
|
|
<div class="card-body">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div class="spinner-border text-primary me-3" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h6 class="mb-1" id="progressStatus">Searching for campaign...</h6>
|
|
<small class="text-muted" id="progressDetail">Please wait while we search through the campaigns folder</small>
|
|
</div>
|
|
</div>
|
|
<div class="progress" style="height: 8px;">
|
|
<div id="progressBar"
|
|
class="progress-bar progress-bar-striped progress-bar-animated bg-primary"
|
|
role="progressbar"
|
|
style="width: 0%"
|
|
aria-valuenow="0"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="searchResults" class="mt-4 d-none"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-center mt-4">
|
|
<p class="text-muted">
|
|
<small>Reports are retrieved from Box.com</small>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div><!-- End mainContent -->
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
|
|
<script>
|
|
// Progress bar update function
|
|
function updateProgress(percent, status, detail) {
|
|
const progressBar = document.getElementById('progressBar');
|
|
const progressStatus = document.getElementById('progressStatus');
|
|
const progressDetail = document.getElementById('progressDetail');
|
|
|
|
progressBar.style.width = `${percent}%`;
|
|
progressBar.setAttribute('aria-valuenow', percent);
|
|
progressStatus.textContent = status;
|
|
progressDetail.textContent = detail;
|
|
}
|
|
|
|
document.getElementById('searchForm').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
|
|
const jobNumber = document.getElementById('jobNumber').value.trim();
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
const searchBtnText = document.getElementById('searchBtnText');
|
|
const searchBtnSpinner = document.getElementById('searchBtnSpinner');
|
|
const resultsDiv = document.getElementById('searchResults');
|
|
const progressContainer = document.getElementById('progressContainer');
|
|
|
|
if (!jobNumber) {
|
|
showError('Please enter a job number');
|
|
return;
|
|
}
|
|
|
|
// Show loading state
|
|
searchBtn.disabled = true;
|
|
searchBtnSpinner.classList.remove('d-none');
|
|
searchBtnText.textContent = 'Searching...';
|
|
resultsDiv.classList.add('d-none');
|
|
progressContainer.classList.remove('d-none');
|
|
|
|
// Simulate progress updates (since we don't have real-time updates from backend)
|
|
updateProgress(10, 'Connecting to Box...', 'Authenticating with Box API');
|
|
|
|
setTimeout(() => {
|
|
updateProgress(30, 'Searching campaigns folder...', `Looking for campaign ${jobNumber}`);
|
|
}, 500);
|
|
|
|
try {
|
|
const response = await fetch((window.BASE_URL || '') + '/search', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ job_number: jobNumber })
|
|
});
|
|
|
|
// Update progress while waiting for response
|
|
updateProgress(60, 'Processing results...', 'Retrieving QC reports from campaign folder');
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(data.error || 'Search failed');
|
|
}
|
|
|
|
if (data.reports && data.reports.length > 0) {
|
|
// Show success state
|
|
updateProgress(100, 'Success!', `Found ${data.reports.length} QC report(s). Redirecting to dashboard...`);
|
|
|
|
// Wait a moment before redirecting so user sees the success message
|
|
setTimeout(() => {
|
|
window.location.href = `${window.BASE_URL || ''}/dashboard/${encodeURIComponent(jobNumber)}`;
|
|
}, 800);
|
|
} else {
|
|
progressContainer.classList.add('d-none');
|
|
showError(data.message || 'No reports found for this job number');
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
progressContainer.classList.add('d-none');
|
|
showError(error.message || 'Failed to search for reports');
|
|
} finally {
|
|
// Reset button state (only if not redirecting)
|
|
if (progressContainer.classList.contains('d-none')) {
|
|
searchBtn.disabled = false;
|
|
searchBtnSpinner.classList.add('d-none');
|
|
searchBtnText.textContent = 'Search Reports';
|
|
}
|
|
}
|
|
});
|
|
|
|
function showError(message) {
|
|
const resultsDiv = document.getElementById('searchResults');
|
|
resultsDiv.innerHTML = `
|
|
<div class="alert alert-warning" role="alert">
|
|
<i class="bi bi-exclamation-triangle"></i> ${message}
|
|
</div>
|
|
`;
|
|
resultsDiv.classList.remove('d-none');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|