Files
openclaw-workspace-2026/Projects/site-survey-ai/dashboard.html
T
JC Beasley cb5e26b561 Update memory service with working NocoDB configuration
- Set correct table ID: mx149yctebfwvys (ai_data_Memory)
- Updated type column with SingleSelect options
- Added all required columns for corrections, preferences, episodes, decisions, validation
- Fixed column options for type, severity, status fields
2026-07-04 17:02:40 -07:00

416 lines
14 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IT Site Survey AI | Dashboard</title>
<style>
:root {
--bg: #0d1117;
--bg-2: #161b22;
--bg-3: #21262d;
--border: #30363d;
--text: #c9d1d9;
--text-2: #8b949e;
--accent: #58a6ff;
--accent-2: #1f6feb;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--radius: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px 0;
border-bottom: 1px solid var(--border);
}
.logo {
font-size: 3rem;
margin-bottom: 10px;
}
h1 {
font-size: 2rem;
font-weight: 600;
margin-bottom: 5px;
}
.subtitle {
color: var(--text-2);
font-size: 1.1rem;
}
.panel {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 25px;
margin-bottom: 20px;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border);
}
.panel-header h2 {
font-size: 1.5rem;
font-weight: 600;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
text-decoration: none;
font-size: 0.9rem;
}
.btn-primary {
background: var(--accent-2);
color: white;
}
.btn-primary:hover {
background: var(--accent);
}
.btn-secondary {
background: var(--bg-3);
color: var(--text);
border-color: var(--border);
text-decoration: none;
}
.btn-secondary:hover {
background: var(--bg-3);
}
.alert {
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 20px;
}
.alert.info {
background: rgba(88, 166, 255, 0.1);
border: 1px solid rgba(88, 166, 255, 0.3);
}
.alert.error {
background: rgba(248, 81, 73, 0.1);
border: 1px solid rgba(248, 81, 73, 0.3);
}
.hidden {
display: none !important;
}
.response-card {
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin-bottom: 15px;
}
.response-header {
display: flex;
justify-content: space-between;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid var(--border);
}
.response-meta {
font-size: 0.9rem;
color: var(--text-2);
}
.response-answers {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 15px;
}
.answer-item {
margin-bottom: 10px;
}
.answer-label {
font-weight: 600;
font-size: 0.9rem;
color: var(--text-2);
margin-bottom: 3px;
}
.answer-value {
font-size: 0.95rem;
}
.actions {
display: flex;
gap: 10px;
margin-top: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 25px;
}
.stat-card {
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: var(--accent);
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9rem;
color: var(--text-2);
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
.response-answers {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">🏢</div>
<h1>IT Site Survey AI</h1>
<div class="subtitle">Network Infrastructure Planning & Recommendations</div>
</header>
<div class="panel">
<div class="panel-header">
<h2>📊 Survey Dashboard</h2>
<a href="/" class="btn btn-secondary">← Back to Survey</a>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="totalSurveys">0</div>
<div class="stat-label">Total Surveys</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalResponses">0</div>
<div class="stat-label">Total Responses</div>
</div>
<div class="stat-card">
<div class="stat-value" id="todayResponses">0</div>
<div class="stat-label">Today's Responses</div>
</div>
<div class="stat-card">
<div class="stat-value" id="avgCompletion">0%</div>
<div class="stat-label">Avg. Completion</div>
</div>
</div>
<div id="loadingMessage" class="alert info">
Loading survey responses...
</div>
<div id="errorMessage" class="alert error hidden">
Error loading survey responses. Please try again.
</div>
<div id="responsesContainer">
<!-- Responses will be loaded here -->
</div>
</div>
</div>
<script>
// Format date for display
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleString();
}
// Format response value for display
function formatResponseValue(value) {
if (Array.isArray(value)) {
return value.join(', ');
}
return value || 'Not answered';
}
// Load all survey responses
async function loadResponses() {
const loadingMessage = document.getElementById('loadingMessage');
const errorMessage = document.getElementById('errorMessage');
const responsesContainer = document.getElementById('responsesContainer');
try {
// Fetch all responses
const response = await fetch('/api/surveys/responses');
if (!response.ok) {
throw new Error('Failed to load responses');
}
const data = await response.json();
const responses = data.responses || [];
// Update stats
updateStats(responses);
// Hide loading message
loadingMessage.classList.add('hidden');
// Display responses
if (responses.length === 0) {
responsesContainer.innerHTML = '<div class="alert info">No survey responses yet. Complete a survey to see results here.</div>';
return;
}
// Sort responses by submission date (newest first)
responses.sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at));
// Generate HTML for responses
let html = '';
for (const response of responses) {
html += `
<div class="response-card">
<div class="response-header">
<div>
<strong>Response #${response.id.substring(0, 8)}</strong>
</div>
<div class="response-meta">
Submitted: ${formatDate(response.submitted_at)} by ${response.submitted_by || 'Anonymous'}
</div>
</div>
<div class="response-answers">
${generateAnswersHtml(response.responses)}
</div>
<div class="actions">
<button class="btn btn-primary" onclick="viewSurvey('${response.survey_id}')">View Survey</button>
<button class="btn btn-secondary" onclick="analyzeResponse('${response.survey_id}')">Analyze</button>
<button class="btn btn-secondary" onclick="generateQuote('${response.survey_id}')">Generate Quote</button>
</div>
</div>
`;
}
responsesContainer.innerHTML = html;
} catch (error) {
console.error('Error loading responses:', error);
loadingMessage.classList.add('hidden');
errorMessage.classList.remove('hidden');
}
}
// Generate HTML for answers
function generateAnswersHtml(responses) {
let html = '';
// Define the order of questions based on the template
const questionOrder = [
'company_name', 'industry', 'site_size', 'building_type', 'square_footage',
'network_topology', 'network_size', 'internet_bandwidth', 'isp_type',
'wireless_standard', 'wireless_coverage', 'critical_applications',
'device_types', 'user_density', 'performance_requirements',
'security_requirements', 'compliance_needs', 'budget_range',
'timeline', 'additional_notes'
];
// Create a map of responses for easier access
const responseMap = responses || {};
// Generate HTML for each question in order
for (const questionId of questionOrder) {
if (responseMap.hasOwnProperty(questionId)) {
const value = responseMap[questionId];
const label = getQuestionLabel(questionId);
html += `
<div class="answer-item">
<div class="answer-label">${label}</div>
<div class="answer-value">${formatResponseValue(value)}</div>
</div>
`;
}
}
return html;
}
// Get human-readable label for question ID
function getQuestionLabel(questionId) {
const labels = {
'company_name': 'Company Name',
'industry': 'Industry',
'site_size': 'Site Size',
'building_type': 'Building Type',
'square_footage': 'Square Footage',
'network_topology': 'Network Topology',
'network_size': 'Network Size',
'internet_bandwidth': 'Internet Bandwidth',
'isp_type': 'ISP Type',
'wireless_standard': 'Wireless Standard',
'wireless_coverage': 'Wireless Coverage',
'critical_applications': 'Critical Applications',
'device_types': 'Device Types',
'user_density': 'User Density',
'performance_requirements': 'Performance Requirements',
'security_requirements': 'Security Requirements',
'compliance_needs': 'Compliance Needs',
'budget_range': 'Budget Range',
'timeline': 'Project Timeline',
'additional_notes': 'Additional Notes'
};
return labels[questionId] || questionId;
}
// Update statistics
function updateStats(responses) {
document.getElementById('totalResponses').textContent = responses.length;
// For now, we'll set other stats to default values
// In a more complete implementation, we would calculate these from the data
document.getElementById('totalSurveys').textContent = responses.length > 0 ? '1' : '0';
document.getElementById('todayResponses').textContent = '0';
document.getElementById('avgCompletion').textContent = '100%';
}
// View survey details
function viewSurvey(surveyId) {
window.location.href = `/?survey=${surveyId}`;
}
// Analyze response
function analyzeResponse(surveyId) {
// In a full implementation, this would trigger the AI analysis
alert('AI analysis would be triggered here for survey: ' + surveyId);
}
// Generate quote
function generateQuote(surveyId) {
// In a full implementation, this would generate a quote
alert('Quote generation would be triggered here for survey: ' + surveyId);
}
// Load responses when page loads
document.addEventListener('DOMContentLoaded', function() {
loadResponses();
});
</script>
</body>
</html>