lvmwd_usecases / script.js
Varun10000's picture
Upload 7 files
faf8398 verified
Raw
History Blame Contribute Delete
102 kB
// Enhanced use case details with structured information
const useCaseDetails = {
'1.1': {
category: 'Predictive Analytics & Maintenance',
icon: 'fa-chart-line',
color: '#ffd700',
overview: 'AI analyzes sensor data to predict when pumps, treatment systems, and infrastructure will fail, enabling proactive maintenance.',
expandedText: 'The Oracle of Water system represents a transformative approach to water utility maintenance. By continuously monitoring sensor data from critical infrastructure—including pump vibration patterns, pressure readings, temperature fluctuations, and flow rates—machine learning models learn to recognize early warning signs of degradation. These models can predict failures weeks or even months in advance, allowing maintenance teams to schedule repairs during optimal windows rather than responding to catastrophic failures. This proactive approach fundamentally changes the maintenance paradigm from reactive emergency response to strategic preventive action.',
context: 'Water utilities face constant pressure to maintain aging infrastructure while managing costs. Traditional maintenance often operates on fixed schedules irrespective of actual asset condition, leading to either premature replacements or catastrophic failures. Critical equipment downtime can disrupt water service to entire communities and create expensive emergency situations.',
keyBenefits: ['Prevent unexpected downtime', 'Extend asset lifespan by 15-25%', 'Reduce emergency repair costs by 40-60%', 'Optimize maintenance schedules', 'Improve asset reliability to 99.2%+', 'Enable predictive budgeting'],
challenges: 'Degradation patterns vary by equipment type, age, and environmental conditions. Requires integration of data from multiple sensor types and sources.',
implementation: 'Machine learning models analyze historical sensor data patterns from water treatment equipment to forecast maintenance needs before failures occur. Real-time monitoring feeds continuous data into predictive algorithms that calculate risk scores and alert maintenance teams to schedule preventive interventions.',
expectedOutcomes: 'Utilities typically see 30-40% reduction in maintenance costs, 80% reduction in emergency repairs, and 25-35% extension of asset life within 12-18 months of deployment.',
impact: ['Cost reduction', 'Reliability improvement', 'Service continuity', 'Staff efficiency', 'Budget predictability'],
industryApplications: ['Water treatment plants', 'Pump stations', 'Distribution networks', 'Wastewater facilities', 'Desalination plants']
},
'1.2': {
category: 'Predictive Analytics & Maintenance',
icon: 'fa-chart-line',
color: '#ffd700',
overview: 'AI predicts future inventory needs to prevent stockouts while avoiding excess costs.',
keyBenefits: ['Minimize stockouts', 'Reduce excess inventory', 'Lower storage costs', 'Improve supply chain efficiency'],
implementation: 'Demand forecasting models analyze historical usage patterns and seasonal trends to predict inventory requirements.',
impact: ['Budget optimization', 'Supply efficiency', 'Operational readiness']
},
'1.3': {
category: 'Predictive Analytics & Maintenance',
icon: 'fa-chart-line',
color: '#ffd700',
overview: 'AI optimizes control systems at treatment facilities by analyzing operational parameters in real-time.',
keyBenefits: ['Automatic process optimization', 'Real-time parameter adjustment', 'Enhanced efficiency', 'Quality assurance'],
implementation: 'AI continuously monitors treatment parameters and recommends optimal control settings for pumps, valves, and chemical dosing systems.',
impact: ['Efficiency gains', 'Quality improvement', 'Energy optimization']
},
'1.4': {
category: 'Predictive Analytics & Maintenance',
icon: 'fa-chart-line',
color: '#ffd700',
overview: 'AI recommends optimal chemical and energy usage to reduce operational costs.',
keyBenefits: ['Lower chemical costs', 'Reduce energy consumption', 'Environmental protection', 'Cost transparency'],
implementation: 'Machine learning models analyze treatment requirements and optimize chemical dosing and energy usage without compromising quality.',
impact: ['Cost reduction', 'Sustainability', 'Environmental stewardship']
},
'2.1': {
category: 'Data Management & Reporting',
icon: 'fa-database',
color: '#ffed4e',
overview: 'AI automates data extraction from multiple HR systems and generates standardized reports.',
keyBenefits: ['Automated data consolidation', 'Reduce manual entry errors', 'Faster report generation', 'Consistent formatting'],
implementation: 'AI systems connect to HR databases, extract relevant data, and compile automated reports for payroll, benefits, and compliance.',
impact: ['Time savings', 'Accuracy improvement', 'Process efficiency']
},
'2.2': {
category: 'Data Management & Reporting',
icon: 'fa-database',
color: '#ffed4e',
overview: 'AI processes transactions to automatically generate summaries suitable for public posting.',
keyBenefits: ['Transparency enhancement', 'Compliance reporting', 'Public trust building', 'Data privacy'],
implementation: 'AI aggregates financial transactions and creates summarized reports while protecting sensitive information.',
impact: ['Transparency', 'Community engagement', 'Regulatory compliance']
},
'3.1': {
category: 'Document & Knowledge Management',
icon: 'fa-folder-tree',
color: '#ffffff',
overview: 'AI uses OCR to convert legacy engineering reports, drawings, geotechnical data, meeting minutes into searchable digital text with context-aware search.',
keyBenefits: ['Digitize historical records', 'Context-aware search', 'Reduce document storage', 'Instant information retrieval'],
implementation: 'Optical character recognition (OCR) converts scanned documents to digital text, which is indexed and made searchable with AI-powered context.',
impact: ['Knowledge preservation', 'Faster research', 'Historical insights']
},
'4.1': {
category: 'Process Automation & Workflow',
icon: 'fa-gears',
color: '#ffd700',
overview: 'AI automates onboarding paperwork, offboarding checklists, payroll transaction processing, tracking employee form submissions.',
keyBenefits: ['Eliminate manual paperwork', 'Ensure process compliance', 'Reduce administrative burden', 'Accelerate employee transitions'],
implementation: 'AI workflows automatically process HR forms, verify documentation, and track completion across employee lifecycle events.',
impact: ['Efficiency', 'Compliance', 'Employee satisfaction']
},
'5.1': {
category: 'Employee Experience & Customer Communication',
icon: 'fa-users',
color: '#ffffff',
overview: 'AI helps explain complex policies and procedures by translating technical HR language into understandable formats.',
keyBenefits: ['Improved policy comprehension', 'Reduced HR inquiries', 'Employee empowerment', 'Clearer communication'],
implementation: 'AI takes complex HR documentation and generates plain-language explanations tailored to different employee roles and needs.',
impact: ['Employee engagement', 'Reduced confusion', 'Better compliance']
},
'5.3': {
category: 'Employee Experience & Customer Communication',
icon: 'fa-users',
color: '#ffffff',
overview: 'AI powers chatbots and customer dashboards providing real-time information on water usage, billing, account status.',
keyBenefits: ['24/7 customer support', 'Real-time usage insights', 'Instant billing information', 'Improved customer satisfaction'],
implementation: 'AI chatbots integrated with billing systems provide customers instant access to usage data, payment options, and account information.',
impact: ['Customer satisfaction', 'Self-service reduction', 'Engagement improvement']
},
'6.1': {
category: 'Strategic Planning & Governance',
icon: 'fa-chess-knight',
color: '#ffd700',
overview: 'AI analyzes upcoming projects and retirements to forecast hiring needs and identify skill gaps.',
keyBenefits: ['Proactive hiring planning', 'Identify skill gaps', 'Succession planning', 'Workforce optimization'],
implementation: 'AI analyzes project pipelines, employee retirement dates, and current skill inventories to forecast future staffing requirements.',
impact: ['Strategic foresight', 'Talent management', 'Organizational readiness']
},
'6.6': {
category: 'Strategic Planning & Governance',
icon: 'fa-chess-knight',
color: '#ffd700',
overview: 'AI creates virtual replicas of facilities and systems for design visualization and scenario simulation.',
keyBenefits: ['Risk-free experimentation', 'Design optimization', 'Scenario planning', 'Cost-effective testing'],
implementation: 'AI builds detailed digital models of water treatment facilities enabling simulation of operational changes before implementation.',
impact: ['Innovation', 'Risk mitigation', 'Strategic planning']
}
};
// Function to get enhanced details or generate from template
function getUseCaseDetail(useCaseId, title, desc) {
if (useCaseDetails[useCaseId]) {
return useCaseDetails[useCaseId];
}
// Generate template for other use cases
return {
overview: desc,
keyBenefits: ['Improved efficiency', 'Better decision making', 'Cost optimization', 'Enhanced operations'],
implementation: `This use case leverages AI to automatically ${desc.toLowerCase()}`,
impact: ['Operational excellence', 'Strategic advantage', 'Stakeholder value']
};
}
const categories = [
{
id: 'cat-1',
title: 'Predictive Analytics & Maintenance',
icon: 'fa-chart-line',
color: 'var(--accent-1)',
description: 'AI-driven insights to proactively maintain assets and optimize processes.',
useCases: [
{ id: '1.1', title: 'Predictive Analytics & Asset Management', desc: 'AI analyzes sensor data to predict when pumps, treatment systems, and infrastructure will fail, enabling proactive maintenance.' },
{ id: '1.2', title: 'Inventory Forecasting & Predictive Analysis', desc: 'AI predicts future inventory needs to prevent stockouts while avoiding excess costs.' },
{ id: '1.3', title: 'Control Systems Optimization', desc: 'AI optimizes control systems at treatment facilities by analyzing operational parameters in real-time.' },
{ id: '1.4', title: 'Energy Efficiency and Chemical Use Optimization', desc: 'AI recommend optimal chemical and energy usage to reduce operational costs.' },
{ id: '1.5', title: 'Sewer I&I Monitoring', desc: 'AI detects unusual inflow and infiltration in the sewer system.' },
{ id: '1.6', title: 'SCADA Analytics & Operational Insight', desc: 'AI processes large volumes of SCADA data to identify correlations humans might miss.' },
{ id: '1.7', title: 'Predictive Tank Levels during Communication Outages', desc: 'AI predicts pump run times when telemetry fails.' },
{ id: '1.8', title: 'Predicting Chemical Dosing in Treatment Processes', desc: 'Machine learning predicts optimal chemical doses.' },
{ id: '1.9', title: 'Predictive Cleaning Schedules for Demonstration Facilities', desc: 'AI predict when cleaning is needed rather than fixed schedules.' },
{ id: '1.10', title: 'Forecasting Flows and Stormwater Impact', desc: 'AI predicts flows at facilities to prepare for high-flow events.' },
{ id: '1.11', title: 'Pipeline Replacement Prioritization', desc: 'AI identifies which pipelines are most likely to fail next.' },
{ id: '1.12', title: 'Data-driven Decision Making for SCADA System Interventions', desc: 'AI recommends operational actions during telemetry failure.' },
{ id: '1.13', title: 'Assistance in Emergency Events', desc: 'AI forecasts potential disruptions from fires, debris flows, or extreme rain.' }
]
},
{
id: 'cat-2',
title: 'Data Management & Reporting',
icon: 'fa-database',
color: 'var(--accent-2)',
description: 'Automating data extraction, synthesis, and actionable reporting across the district.',
useCases: [
{ id: '2.1', title: 'Data Management and Reporting (HR)', desc: 'AI automates data extraction from multiple HR systems and generates standardized reports.' },
{ id: '2.2', title: 'Financial Data Analysis for Public Transparency', desc: 'AI processes transactions to automatically generate summaries suitable for public posting.' },
{ id: '2.3', title: 'Budget Planning & Historical Spend Analysis', desc: 'AI automatically pulls historical spending data during the budget cycle.' },
{ id: '2.4', title: 'Staff Report Generation & Repetitive Analysis Tasks', desc: 'AI creates initial drafts of staff reports to cut down on report generation time.' },
{ id: '2.5', title: 'Advanced Data Analytics across IT, Finance & Billing', desc: 'AI performs deep analysis across large datasets faster than manual methods.' },
{ id: '2.6', title: 'Self-service Natural Language Reporting', desc: 'Staff can type questions in plain English and AI queries databases.' },
{ id: '2.7', title: 'Cross-department Data Sharing & Insight', desc: 'AI bridges data silos between IT, Operations, Resource Conservation, Finance, and Customer Service.' },
{ id: '2.8', title: 'District-wide Data Layer / Data Lake AI Application', desc: 'AI creates a unified abstraction layer over disparate source systems.' },
{ id: '2.9', title: 'Accounting & Financial Process Automation', desc: 'AI automates invoice-to-PO matching, flagging discrepancies.' },
{ id: '2.10', title: 'Reporting Automation / Pre-filling State-required Reports', desc: 'AI automatically populates regulatory reports.' },
{ id: '2.11', title: 'Trend Analysis and Visualization', desc: 'AI creates visualizations of flows, tank drawdown, treatment parameters.' },
{ id: '2.12', title: 'Operational Co-Pilot across Facilities', desc: 'AI provides integrated operational support across Tapia, Pure Water, and drinking water facilities.' },
{ id: '2.13', title: 'Customer Sentiment & Trend Analysis', desc: 'AI analyzes customer service notes, call reasons, survey responses to identify recurring concerns.' },
{ id: '2.14', title: 'Communications Effectiveness Analysis', desc: 'AI tracks engagement metrics across communication channels.' }
]
},
{
id: 'cat-3',
title: 'Document & Knowledge Management',
icon: 'fa-folder-tree',
color: '#ffffff',
description: 'Digitizing, organizing, and retrieving institutional knowledge efficiently.',
useCases: [
{ id: '3.1', title: 'Document Digitization & Intelligent Search', desc: 'AI uses OCR to convert legacy engineering reports, drawings, geotechnical data, meeting minutes into searchable digital text with context-aware search.' },
{ id: '3.2', title: 'Legacy Drawings and Files', desc: 'AI automatically examines historical drawings to identify type, subject matter, location, and applies consistent labels.' },
{ id: '3.3', title: 'Centralized Database and Records Retrieval', desc: 'AI creates a unified search interface across multiple disparate systems and repositories.' },
{ id: '3.4', title: 'Knowledge Capture for Policies and District Procedures', desc: 'AI extracts institutional knowledge from retiring employees\' emails, documents, and notes into searchable knowledge bases.' },
{ id: '3.5', title: 'AI-assisted Research & White Paper Development', desc: 'AI acts as research assistant, gathering relevant information from internal documents, external sources, and historical records.' },
{ id: '3.6', title: 'Meeting Minutes Management', desc: 'AI transcribes meetings, generates draft minutes with key decisions highlighted, makes minutes searchable across years.' },
{ id: '3.7', title: 'Knowledge Base & Case Retrieval', desc: 'AI searches historical customer service cases and policy decisions based on situation criteria rather than exact keywords.' }
]
},
{
id: 'cat-4',
title: 'Process Automation & Workflow',
icon: 'fa-gears',
color: '#ffd700',
description: 'Streamlining repetitive tasks and optimizing multi-step workflows.',
useCases: [
{ id: '4.1', title: 'Process Automation', desc: 'AI automates onboarding paperwork, offboarding checklists, payroll transaction processing, tracking employee form submissions.' },
{ id: '4.2', title: 'Invoice Processing & Validation', desc: 'AI automatically ingests invoices, extracts key information, validates against purchase orders, checks GL codes.' },
{ id: '4.3', title: 'Contract and Bid Evaluation', desc: 'AI pre-scores bids against predefined evaluation criteria ensuring objective evaluation.' },
{ id: '4.4', title: 'Bank Reconciliation Automation', desc: 'AI automatically matches bank transactions to accounting records.' },
{ id: '4.5', title: 'RFP Analysis and Evaluation Efficiency', desc: 'AI screens incoming proposals for completeness and compliance.' },
{ id: '4.6', title: 'Board Memos and PowerPoint Presentations', desc: 'AI assists with drafting board memos by organizing information and suggesting structure.' },
{ id: '4.7', title: 'Project Management Tools and Procedural Documentation', desc: 'AI generates cheat sheets and standard operating procedures for project delivery.' },
{ id: '4.8', title: 'Construction Management Support', desc: 'AI tracks and organizes construction project communications including emails, submittals, and other key documents.' },
{ id: '4.9', title: 'Engineering Templates Automation', desc: 'AI automatically formats engineering staff reports according to district standards.' },
{ id: '4.10', title: 'Automated Log Aggregation & Analysis', desc: 'AI continuously collects logs from network devices, servers, SCADA systems to identify anomalies.' },
{ id: '4.11', title: 'Asset & Inventory Lifecycle Automation', desc: 'AI automatically tracks IT hardware and software assigned to employees.' },
{ id: '4.12', title: 'Cross-division Project & Workflow Management', desc: 'AI tracks projects, deadlines, and dependencies across public affairs, customer service, and conservation program.' }
]
},
{
id: 'cat-5',
title: 'Employee Experience & Customer Communication',
icon: 'fa-users',
color: '#ffffff',
description: 'Enhancing interactions through personalized, intelligent communication tools.',
useCases: [
{ id: '5.1', title: 'Employee Communication (HR)', desc: 'AI helps explain complex policies and procedures by translating technical HR language into understandable formats.' },
{ id: '5.2', title: 'Employee Engagement & Experience', desc: 'AI supports new hire orientation by providing personalized information and answering common questions.' },
{ id: '5.3', title: 'Customer-facing AI Solutions / Water Usage Analytics', desc: 'AI powers chatbots and customer dashboards providing real-time information on water usage, billing, account status.' },
{ id: '5.4', title: 'Security Monitoring and Camera Analysis', desc: 'Enables automated AI video feeds for inspection, intrusion detection, infrastructure integrity assessment, and real-time identification of personnel in the field.' },
{ id: '5.5', title: 'Self-service Website Search & Chatbot', desc: 'AI-powered search understands customer intent and finds relevant information even when customers don\'t use exact terminology.' },
{ id: '5.6', title: 'Customer Service Call Deflection (with human override)', desc: 'During high-volume events, AI handles simple repetitive questions automatically while customers can always reach a live agent.' },
{ id: '5.7', title: 'Crisis & Emergency Communication Templates', desc: 'AI helps rapidly assemble pre-approved messaging templates for emergencies.' },
{ id: '5.8', title: 'Employee Self Service', desc: 'AI helps employees access information easily to make decisions and perform tasks that improve their satisfaction and focus on value-added activities.' },
{ id: '5.9', title: 'Personalized Conservation Insights & Program Matching', desc: 'AI analyzes a customer’s water usage, highlights personalized conservation opportunities. It also evaluates the customer’s data to match them with eligible water conservation programs.' },
{ id: '5.10', title: 'Proactive Outreach Based on Usage Patterns', desc: 'AI identifies customers with unusual usage patterns and triggers proactive outreach with targeted recommendations.' },
{ id: '5.11', title: 'Staff Training Support / AI as a Mentor', desc: 'AI acts as on-demand mentor for newer operators, answering questions about standard procedures and equipment operation.' }
]
},
{
id: 'cat-6',
title: 'Strategic Planning & Governance',
icon: 'fa-chess-knight',
color: '#ffd700',
description: 'Data-driven foresight to guide policy, resource allocation, and organizational strategy.',
useCases: [
{ id: '6.1', title: 'Recruiting & Talent', desc: 'AI analyzes upcoming projects and retirements to forecast hiring needs and identify skill gaps.' },
{ id: '6.2', title: 'Compliance & Legal Management', desc: 'AI monitors new employment laws and regulations, summarizes changes relevant to the district.' },
{ id: '6.3', title: 'Strategic HR / Organizational Planning', desc: 'AI supports workforce planning by analyzing organizational structure, staffing levels, skill distributions.' },
{ id: '6.4', title: 'Personal / Clerical Assistance', desc: 'AI assists with scheduling orientations and meetings, drafting professional emails, fact-checking information.' },
{ id: '6.5', title: 'Cost Savings Measurement and Validation', desc: 'AI tracks and quantifies actual cost savings achieved through automation and AI initiatives.' },
{ id: '6.6', title: 'Digital Twin Creation and Modeling', desc: 'AI creates virtual replicas of facilities and systems for design visualization and scenario simulation.' },
{ id: '6.7', title: 'AI Literacy & Workforce Training', desc: 'AI-powered training programs teach staff how to effectively use AI tools, write better prompts, and interpret outputs critically.' },
{ id: '6.8', title: 'AI Governance, Policy & Measurement Framework', desc: 'AI helps establish and enforce policies around acceptable AI use, data protection, cybersecurity, customer privacy, and public records retention.' },
{ id: '6.9', title: 'Inter-agency AI Collaboration', desc: 'The district uses AI tools to facilitate collaboration with other water agencies, sharing lessons learned and best practices.' }
]
}
];
const allUseCases = categories.flatMap((cat) => cat.useCases);
function findUseCaseById(useCaseId) {
return allUseCases.find((uc) => uc.id === useCaseId);
}
function selectUseCasesById(ids) {
return ids.map((id) => findUseCaseById(id)).filter(Boolean);
}
const groups = [
{
id: 'group-1',
title: 'Group 1: Operations',
icon: 'fa-screwdriver-wrench',
color: 'var(--accent-1)',
description: 'Operational intelligence use cases for predictive maintenance, real-time optimization, and resilience.',
sections: [
{
title: 'Predictive Analytics and Maintenance',
useCases: categories.find((cat) => cat.id === 'cat-1').useCases
},
{
title: 'Digital Twin',
useCases: selectUseCasesById(['6.6'])
}
]
},
{
id: 'group-2a',
title: 'Group 2A: Back Office',
icon: 'fa-briefcase',
color: 'var(--accent-2)',
description: 'Back-office data and reporting priorities with focused knowledge, governance, and value tracking.',
sections: [
{
title: 'Data Management and Reporting',
useCases: selectUseCasesById(['2.1', '2.2', '2.3', '2.4', '2.6', '2.10', '2.11', '2.12', '2.13', '2.14'])
},
{
title: 'Document Management and Knowledge Management',
useCases: selectUseCasesById(['3.3'])
},
{
title: 'Cost Savings and Validation',
useCases: selectUseCasesById(['6.5'])
},
{
title: 'Governance and Policy Measurement',
useCases: selectUseCasesById(['6.8'])
}
]
},
{
id: 'group-2b',
title: 'Group 2B: Back Office',
icon: 'fa-briefcase',
color: '#f4f4f4',
description: 'Back-office document systems, workflow automation, compliance, and clerical support.',
sections: [
{
title: 'Data Management and Reporting',
useCases: selectUseCasesById(['2.8', '2.9'])
},
{
title: 'Document Management and Knowledge Management',
useCases: selectUseCasesById(['3.1', '3.4', '3.5', '3.6'])
},
{
title: 'Process Automation and Workflow',
useCases: selectUseCasesById(['4.1', '4.2', '4.4', '4.5', '4.6', '4.9', '4.10', '4.11', '4.12'])
},
{
title: 'Compliance and Legal',
useCases: selectUseCasesById(['6.2'])
},
{
title: 'Personal Clerical Assistance',
useCases: selectUseCasesById(['6.4'])
}
]
},
{
id: 'group-3',
title: 'Group 3: Communications',
icon: 'fa-comments',
color: '#ffffff',
description: 'Customer and employee communication, workforce planning, and AI readiness at scale.',
sections: [
{
title: 'Customer Communication & Employee Experience',
useCases: selectUseCasesById(['5.1', '5.2', '5.3', '5.4', '5.6', '5.7', '5.8', '5.9', '5.10', '5.11'])
},
{
title: 'AI Literacy and Workforce Training',
useCases: selectUseCasesById(['6.7'])
},
{
title: 'Interagency AI Collaboration',
useCases: selectUseCasesById(['6.9'])
},
{
title: 'Strategic HR and Organizational Planning',
useCases: selectUseCasesById(['6.3'])
},
{
title: 'Recruiting and Talent Management',
useCases: selectUseCasesById(['6.1'])
}
]
}
];
const groupLabelMap = {
'group-1': 'Group 1',
'group-2a': 'Group 2A',
'group-2b': 'Group 2B',
'group-3': 'Group 3',
'all-groups': 'All Groups'
};
function getGroupUseCaseCount(group) {
return group.sections.reduce((sum, section) => sum + section.useCases.length, 0);
}
function getAccessibleGroups() {
if (!currentUser || !currentUser.groupId) {
return [];
}
if (currentUser.groupId === 'all-groups') {
return groups;
}
// Legacy support if a previous user profile still has group-2 saved.
if (currentUser.groupId === 'group-2') {
return groups.filter((group) => group.id === 'group-2a' || group.id === 'group-2b');
}
return groups.filter((group) => group.id === currentUser.groupId);
}
// DOM Elements
const categoriesGrid = document.getElementById('categories-grid');
const useCasesContainer = document.getElementById('use-cases-container');
const categoriesView = document.getElementById('categories-view');
const useCasesView = document.getElementById('use-cases-view');
const detailView = document.getElementById('detail-view');
const backBtn = document.getElementById('back-btn');
const detailModal = document.getElementById('detail-modal');
const modalClose = document.getElementById('modal-close');
const modalDetailContent = document.getElementById('modal-detail-content');
const loginOverlay = document.getElementById('login-overlay');
const appShell = document.getElementById('app-shell');
const loginForm = document.getElementById('login-form');
const loginSubmitBtn = document.getElementById('login-submit-btn');
const loginNameInput = document.getElementById('login-name');
const loginDepartmentInput = document.getElementById('login-department');
const loginEmailInput = document.getElementById('login-email');
const loginGroupInput = document.getElementById('login-group');
const currentUserLabel = document.getElementById('current-user-label');
const adminPanelBtn = document.getElementById('admin-panel-btn');
const adminView = document.getElementById('admin-view');
const adminBackBtn = document.getElementById('admin-back-btn');
const addAdminForm = document.getElementById('add-admin-form');
const adminUsersList = document.getElementById('admin-users-list');
const adminLoginsList = document.getElementById('admin-logins-list');
const adminPostForm = document.getElementById('admin-post-form');
const adminPostGroupInput = document.getElementById('admin-post-group');
const adminPostUseCaseInput = document.getElementById('admin-post-usecase');
const adminPostTextInput = document.getElementById('admin-post-text');
const adminPostImageBtn = document.getElementById('admin-post-image-btn');
const adminPostImageClearBtn = document.getElementById('admin-post-image-clear-btn');
const adminPostImageInput = document.getElementById('admin-post-image-input');
const adminPostImageStatus = document.getElementById('admin-post-image-status');
const adminPostImagePreview = document.getElementById('admin-post-image-preview');
const adminPostsList = document.getElementById('admin-posts-list');
const adminSubmissionsTable = document.getElementById('admin-submissions-table');
const adminUserData = document.getElementById('admin-user-data');
const adminDownloadPdfBtn = document.getElementById('admin-download-pdf-btn');
const logoutBtn = document.getElementById('logout-btn');
const USERS_STORAGE_KEY = 'lvmwd_users';
const NOTES_STORAGE_KEY = 'lvmwd_user_notes';
const ACTIVE_USER_STORAGE_KEY = 'lvmwd_active_user';
const PERSONAL_NOTES_STORAGE_KEY = 'lvmwd_personal_notes';
const ADMIN_USERS_STORAGE_KEY = 'lvmwd_admin_users';
const ADMIN_POSTS_STORAGE_KEY = 'lvmwd_admin_posts';
const PRIMARY_ADMIN_EMAIL = 'admin@company.com';
const APP_STATE_KEYS = [
USERS_STORAGE_KEY,
NOTES_STORAGE_KEY,
PERSONAL_NOTES_STORAGE_KEY,
ADMIN_USERS_STORAGE_KEY,
ADMIN_POSTS_STORAGE_KEY
];
// Active Category Header Elements
const activeCategoryIcon = document.getElementById('active-category-icon');
const activeCategoryTitle = document.getElementById('active-category-title');
const activeCategoryCount = document.getElementById('active-category-count');
let currentCategory = null;
let currentUser = null;
let adminPostDraftImages = [];
let adminEditingPostId = null;
let adminSelectedUserEmail = null;
let supabaseClient = null;
let useSupabase = false;
let appStateCache = {};
let cloudFlushTimer = null;
const pendingCloudWrites = new Map();
function normalizeEmail(email) {
return String(email || '').trim().toLowerCase();
}
function escapeHtml(text) {
return String(text || '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function cloneObject(value) {
return JSON.parse(JSON.stringify(value || {}));
}
async function loadLocalStateIntoCache() {
const nextState = {};
APP_STATE_KEYS.forEach((key) => {
try {
const raw = localStorage.getItem(key);
nextState[key] = raw ? JSON.parse(raw) : {};
} catch (error) {
nextState[key] = {};
}
});
appStateCache = nextState;
}
async function initializeSupabaseDataLayer() {
await loadLocalStateIntoCache();
const supabaseUrl = String(window.SUPABASE_URL || '').trim();
const supabaseAnonKey = String(window.SUPABASE_ANON_KEY || '').trim();
const supabaseFactory = window.supabase && window.supabase.createClient;
if (!supabaseUrl || !supabaseAnonKey || !supabaseFactory) {
useSupabase = false;
return;
}
try {
supabaseClient = supabaseFactory(supabaseUrl, supabaseAnonKey);
const { data, error } = await supabaseClient
.from('app_state')
.select('state_key,payload');
if (error) {
useSupabase = false;
return;
}
APP_STATE_KEYS.forEach((key) => {
const row = (data || []).find((item) => item.state_key === key);
if (row && row.payload && typeof row.payload === 'object') {
appStateCache[key] = row.payload;
}
});
useSupabase = true;
} catch (error) {
useSupabase = false;
}
}
async function flushCloudWrites() {
if (!useSupabase || !supabaseClient || pendingCloudWrites.size === 0) {
return;
}
const entries = Array.from(pendingCloudWrites.entries());
const payload = entries.map(([stateKey, value]) => ({
state_key: stateKey,
payload: value,
updated_at: new Date().toISOString()
}));
// Keep a retry-safe queue: only remove entries after successful upsert.
entries.forEach(([stateKey]) => pendingCloudWrites.delete(stateKey));
try {
const { error } = await supabaseClient.from('app_state').upsert(payload, { onConflict: 'state_key' });
if (error) {
throw error;
}
} catch (error) {
entries.forEach(([stateKey, value]) => pendingCloudWrites.set(stateKey, value));
if (cloudFlushTimer) {
clearTimeout(cloudFlushTimer);
}
cloudFlushTimer = setTimeout(() => {
flushCloudWrites();
}, 1000);
}
}
function scheduleCloudFlush() {
if (!useSupabase) {
return;
}
if (cloudFlushTimer) {
clearTimeout(cloudFlushTimer);
}
cloudFlushTimer = setTimeout(() => {
flushCloudWrites();
}, 250);
}
function readStorageObject(key) {
if (useSupabase) {
const value = appStateCache[key];
return value && typeof value === 'object' ? cloneObject(value) : {};
}
try {
const raw = localStorage.getItem(key);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : {};
} catch (error) {
return {};
}
}
function writeStorageObject(key, value) {
const safeValue = value && typeof value === 'object' ? cloneObject(value) : {};
// Always persist a local copy as durability fallback.
try {
localStorage.setItem(key, JSON.stringify(safeValue));
} catch (error) {
// Ignore local storage failures and continue with cloud attempt.
}
if (useSupabase) {
appStateCache[key] = safeValue;
pendingCloudWrites.set(key, safeValue);
scheduleCloudFlush();
return true;
}
try {
localStorage.setItem(key, JSON.stringify(safeValue));
return true;
} catch (error) {
return false;
}
}
function generateId(prefix) {
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
}
function getGroupLabel(groupId) {
return groupLabelMap[groupId] || groupId || 'Unknown Group';
}
function getAdminUsers() {
const raw = readStorageObject(ADMIN_USERS_STORAGE_KEY);
const users = Array.isArray(raw.users) ? raw.users : [];
return users.filter((item) => item && item.email);
}
function saveAdminUsers(users) {
writeStorageObject(ADMIN_USERS_STORAGE_KEY, { users });
}
function isCurrentUserAdmin() {
if (!currentUser) {
return false;
}
const userEmail = normalizeEmail(currentUser.email);
return userEmail === PRIMARY_ADMIN_EMAIL;
}
function ensureCurrentUserAdminSeed() {
if (!currentUser) {
return;
}
const userEmail = normalizeEmail(currentUser.email);
if (userEmail !== PRIMARY_ADMIN_EMAIL) {
return;
}
const adminUsers = getAdminUsers();
const exists = adminUsers.some((admin) => normalizeEmail(admin.email) === userEmail);
if (exists) {
return;
}
adminUsers.push({
id: generateId('admin'),
name: currentUser.name,
department: currentUser.department,
email: userEmail,
createdAt: new Date().toISOString(),
addedBy: userEmail
});
saveAdminUsers(adminUsers);
}
function getAdminPosts() {
const raw = readStorageObject(ADMIN_POSTS_STORAGE_KEY);
const posts = Array.isArray(raw.posts) ? raw.posts : [];
return posts.filter((item) => item && item.id);
}
function saveAdminPosts(posts) {
writeStorageObject(ADMIN_POSTS_STORAGE_KEY, { posts });
}
function getVisibleAdminPostsForUseCase(useCaseId) {
if (!currentUser) {
return [];
}
const posts = getAdminPosts();
return posts
.filter((post) => post.useCaseId === useCaseId)
.filter((post) => {
if (post.targetGroupId === 'all-groups') {
return true;
}
if (currentUser.groupId === 'all-groups') {
return true;
}
return post.targetGroupId === currentUser.groupId;
})
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
}
function renderAdminPostBlock(useCaseId) {
const visiblePosts = getVisibleAdminPostsForUseCase(useCaseId);
if (visiblePosts.length === 0) {
return '';
}
return `
<div class="admin-broadcast-list">
<h4><i class="fa-solid fa-bullhorn"></i> Admin Notes</h4>
${visiblePosts.map((post) => {
const ts = new Date(post.createdAt).toLocaleString();
const imagesBlock = renderNoteImagesBlock(post.images || []);
return `
<div class="admin-broadcast-item">
<p>${escapeHtml(post.text || '')}</p>
${imagesBlock}
<span>${escapeHtml(post.createdByName || 'Admin')}${escapeHtml(getGroupLabel(post.targetGroupId))}${escapeHtml(ts)}</span>
</div>
`;
}).join('')}
</div>
`;
}
function isImageFile(file) {
return Boolean(file && typeof file.type === 'string' && file.type.startsWith('image/'));
}
function fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ''));
reader.onerror = () => reject(new Error('Unable to read image file'));
reader.readAsDataURL(file);
});
}
function normalizeImageAttachment(imageAttachment) {
if (!imageAttachment || typeof imageAttachment !== 'object' || !imageAttachment.dataUrl) {
return null;
}
return {
dataUrl: String(imageAttachment.dataUrl),
name: String(imageAttachment.name || ''),
type: String(imageAttachment.type || ''),
size: Number(imageAttachment.size || 0)
};
}
function normalizeImageAttachments(imageAttachments) {
if (!Array.isArray(imageAttachments)) {
return [];
}
return imageAttachments
.map(normalizeImageAttachment)
.filter(Boolean);
}
function getStoredNoteImages(note) {
if (!note || typeof note !== 'object') {
return [];
}
if (Array.isArray(note.images)) {
return normalizeImageAttachments(note.images);
}
const legacyImage = note.imageDataUrl || note.image || '';
if (!legacyImage) {
return [];
}
return [{
dataUrl: String(legacyImage),
name: String(note.imageName || ''),
type: String(note.imageType || ''),
size: Number(note.imageSize || 0)
}];
}
function renderNoteImagesBlock(imageAttachments) {
const images = normalizeImageAttachments(imageAttachments);
if (images.length === 0) {
return '';
}
const caption = images.length === 1 ? (images[0].name ? escapeHtml(images[0].name) : 'Attached image') : `${images.length} attached images`;
return `
<div class="note-image-gallery">
${images.map((imageAttachment, index) => {
const imageCaption = imageAttachment.name ? escapeHtml(imageAttachment.name) : `Image ${index + 1}`;
return `
<figure class="note-image-figure">
<img src="${escapeHtml(imageAttachment.dataUrl)}" alt="${imageCaption}" loading="lazy">
<figcaption>${imageCaption}</figcaption>
</figure>
`;
}).join('')}
${images.length > 1 ? `<p class="note-image-gallery-count">${caption}</p>` : ''}
</div>
`;
}
function getUserNotesBucket() {
if (!currentUser) {
return {};
}
const allNotes = readStorageObject(NOTES_STORAGE_KEY);
const emailKey = normalizeEmail(currentUser.email);
return allNotes[emailKey] || {};
}
function getUserPersonalNotesBucket() {
if (!currentUser) {
return {};
}
const allNotes = readStorageObject(PERSONAL_NOTES_STORAGE_KEY);
const emailKey = normalizeEmail(currentUser.email);
return allNotes[emailKey] || {};
}
function getPersonalNoteForUseCase(useCaseId) {
const entry = getPersonalNoteEntry(useCaseId);
return entry.text;
}
function getPersonalNoteEntry(useCaseId) {
const bucket = getUserPersonalNotesBucket();
const value = bucket[useCaseId];
if (typeof value === 'string') {
return {
text: value,
imageAttachments: []
};
}
if (value && typeof value === 'object') {
const legacyImages = value.images ? normalizeImageAttachments(value.images) : getStoredNoteImages(value);
return {
text: typeof value.text === 'string' ? value.text : '',
imageAttachments: legacyImages
};
}
return {
text: '',
imageAttachments: []
};
}
function savePersonalNoteForUseCase(useCaseId, noteText, imageAttachments = []) {
if (!currentUser) {
return;
}
const emailKey = normalizeEmail(currentUser.email);
const allNotes = readStorageObject(PERSONAL_NOTES_STORAGE_KEY);
if (!allNotes[emailKey]) {
allNotes[emailKey] = {};
}
const normalizedImageAttachments = normalizeImageAttachments(imageAttachments);
allNotes[emailKey][useCaseId] = {
text: String(noteText || ''),
images: normalizedImageAttachments,
updatedAt: new Date().toISOString(),
authorName: currentUser.name,
department: currentUser.department,
groupId: currentUser.groupId,
imageDataUrl: normalizedImageAttachments[0]?.dataUrl || '',
imageName: normalizedImageAttachments[0]?.name || '',
imageType: normalizedImageAttachments[0]?.type || '',
imageSize: normalizedImageAttachments[0]?.size || 0
};
writeStorageObject(PERSONAL_NOTES_STORAGE_KEY, allNotes);
}
function clearPersonalNoteForUseCase(useCaseId) {
savePersonalNoteForUseCase(useCaseId, '', []);
}
function getNotesForUseCase(useCaseId) {
const bucket = getUserNotesBucket();
return Array.isArray(bucket[useCaseId]) ? bucket[useCaseId] : [];
}
function saveNoteForUseCase(useCaseId, noteText, imageAttachments = []) {
if (!currentUser) {
return;
}
const emailKey = normalizeEmail(currentUser.email);
const allNotes = readStorageObject(NOTES_STORAGE_KEY);
if (!allNotes[emailKey]) {
allNotes[emailKey] = {};
}
if (!Array.isArray(allNotes[emailKey][useCaseId])) {
allNotes[emailKey][useCaseId] = [];
}
const normalizedImageAttachments = normalizeImageAttachments(imageAttachments);
allNotes[emailKey][useCaseId].unshift({
text: noteText,
createdAt: new Date().toISOString(),
authorName: currentUser.name,
authorEmail: emailKey,
department: currentUser.department,
groupId: currentUser.groupId,
images: normalizedImageAttachments,
imageDataUrl: normalizedImageAttachments[0]?.dataUrl || '',
imageName: normalizedImageAttachments[0]?.name || '',
imageType: normalizedImageAttachments[0]?.type || '',
imageSize: normalizedImageAttachments[0]?.size || 0
});
writeStorageObject(NOTES_STORAGE_KEY, allNotes);
}
function renderNotesList(useCaseId) {
const listEl = document.getElementById('user-notes-list');
if (!listEl) {
return;
}
const notes = getNotesForUseCase(useCaseId);
if (notes.length === 0) {
listEl.innerHTML = '<p class="note-empty">No notes yet for this use case.</p>';
return;
}
listEl.innerHTML = notes
.map((note) => {
const dateLabel = new Date(note.createdAt).toLocaleString();
const imageBlock = renderNoteImagesBlock(getStoredNoteImages(note));
return `
<div class="note-item">
${imageBlock}
${note.text ? `<p>${escapeHtml(note.text)}</p>` : ''}
<span>${escapeHtml(note.authorName)} (${escapeHtml(note.department)}) • ${escapeHtml(dateLabel)}</span>
</div>
`;
})
.join('');
}
function renderInlineNotesList(listEl, useCaseId) {
if (!listEl) {
return;
}
const notes = getNotesForUseCase(useCaseId);
if (notes.length === 0) {
listEl.innerHTML = '<p class="note-empty">No notes yet for this use case.</p>';
return;
}
listEl.innerHTML = notes
.map((note) => {
const dateLabel = new Date(note.createdAt).toLocaleString();
const imageBlock = renderNoteImagesBlock(getStoredNoteImages(note));
return `
<div class="note-item">
${imageBlock}
${note.text ? `<p>${escapeHtml(note.text)}</p>` : ''}
<span>${escapeHtml(note.authorName)} (${escapeHtml(note.department)}) • ${escapeHtml(dateLabel)}</span>
</div>
`;
})
.join('');
}
function bindInlineNotesEditor(container, useCaseId) {
if (!container) {
return;
}
const inputEl = container.querySelector('.inline-note-input');
const clearBtn = container.querySelector('.inline-note-clear-btn');
const statusEl = container.querySelector('.inline-note-status');
const imageInputEl = container.querySelector('.inline-note-image-input');
const imageButtonEl = container.querySelector('.inline-note-image-btn');
const imageClearBtn = container.querySelector('.inline-note-image-clear-btn');
const imagePreviewEl = container.querySelector('.inline-note-image-preview');
if (!inputEl || !statusEl) {
return;
}
let saveTimeout = null;
let currentImageAttachments = getPersonalNoteEntry(useCaseId).imageAttachments;
const markSaved = () => {
statusEl.textContent = 'Auto-saved';
};
const renderImagePreview = () => {
if (!imagePreviewEl) {
return;
}
if (currentImageAttachments.length === 0) {
imagePreviewEl.innerHTML = '';
imagePreviewEl.classList.remove('visible');
if (imageClearBtn) {
imageClearBtn.classList.add('hidden');
}
return;
}
const countLabel = currentImageAttachments.length === 1
? (currentImageAttachments[0].name ? escapeHtml(currentImageAttachments[0].name) : 'Attached image')
: `${currentImageAttachments.length} attached images`;
imagePreviewEl.innerHTML = `
<div class="note-image-preview-grid">
${currentImageAttachments.map((attachment, index) => {
const caption = attachment.name ? escapeHtml(attachment.name) : `Image ${index + 1}`;
return `
<div class="note-image-preview-card">
<button class="note-image-remove-btn" type="button" data-image-index="${index}" aria-label="Remove ${caption}">×</button>
<img src="${escapeHtml(attachment.dataUrl)}" alt="${caption}">
<div class="note-image-preview-meta">
<span>${caption}</span>
</div>
</div>
`;
}).join('')}
<div class="note-image-preview-summary">
<span>${countLabel}</span>
</div>
</div>
`;
imagePreviewEl.classList.add('visible');
if (imageClearBtn) {
imageClearBtn.classList.remove('hidden');
}
};
const persistInlineNote = () => {
savePersonalNoteForUseCase(useCaseId, inputEl.value, currentImageAttachments);
markSaved();
if (statusEl) {
statusEl.textContent = 'Auto-saved';
}
};
const addImageAttachmentsFromFiles = async (files) => {
const imageFiles = Array.from(files || []).filter(isImageFile);
if (imageFiles.length === 0) {
return;
}
for (const file of imageFiles) {
if (file.size > 2 * 1024 * 1024) {
statusEl.textContent = 'One of the images is too large. Use images under 2 MB.';
continue;
}
statusEl.textContent = 'Saving image...';
const dataUrl = await fileToDataUrl(file);
currentImageAttachments.push({
dataUrl,
name: file.name,
type: file.type,
size: file.size
});
}
renderImagePreview();
if (statusEl) {
statusEl.textContent = 'Image(s) added';
}
persistInlineNote();
};
const removeImageAttachmentAtIndex = (imageIndex) => {
currentImageAttachments = currentImageAttachments.filter((_, index) => index !== imageIndex);
renderImagePreview();
persistInlineNote();
};
const clearImageAttachment = () => {
currentImageAttachments = [];
if (imageInputEl) {
imageInputEl.value = '';
}
renderImagePreview();
persistInlineNote();
};
const personalNoteEntry = getPersonalNoteEntry(useCaseId);
inputEl.value = personalNoteEntry.text;
currentImageAttachments = personalNoteEntry.imageAttachments;
renderImagePreview();
markSaved();
inputEl.addEventListener('input', (event) => {
event.stopPropagation();
statusEl.textContent = 'Saving...';
if (saveTimeout) {
clearTimeout(saveTimeout);
}
saveTimeout = setTimeout(() => {
persistInlineNote();
markSaved();
}, 300);
});
inputEl.addEventListener('paste', async (event) => {
const clipboardItems = Array.from(event.clipboardData?.items || []);
const imageItem = clipboardItems.find((item) => item.kind === 'file' && isImageFile(item.getAsFile()));
if (!imageItem) {
return;
}
event.preventDefault();
const pastedFiles = clipboardItems
.filter((item) => item.kind === 'file' && isImageFile(item.getAsFile()))
.map((item) => item.getAsFile())
.filter(Boolean);
if (pastedFiles.length > 0) {
await addImageAttachmentsFromFiles(pastedFiles);
}
});
if (imageInputEl) {
imageInputEl.addEventListener('change', async () => {
const files = imageInputEl.files || [];
if (files.length > 0) {
await addImageAttachmentsFromFiles(files);
}
});
}
if (imageButtonEl && imageInputEl) {
imageButtonEl.addEventListener('click', (event) => {
event.stopPropagation();
imageInputEl.click();
});
}
if (imageClearBtn) {
imageClearBtn.addEventListener('click', (event) => {
event.stopPropagation();
clearImageAttachment();
});
}
if (clearBtn) {
clearBtn.addEventListener('click', (event) => {
event.stopPropagation();
inputEl.value = '';
clearPersonalNoteForUseCase(useCaseId);
clearImageAttachment();
markSaved();
});
}
if (imagePreviewEl) {
imagePreviewEl.addEventListener('click', (event) => {
const removeBtn = event.target.closest('.note-image-remove-btn');
if (!removeBtn) {
return;
}
event.stopPropagation();
const imageIndex = Number(removeBtn.dataset.imageIndex);
if (Number.isInteger(imageIndex)) {
removeImageAttachmentAtIndex(imageIndex);
}
});
}
const detailsEl = container.querySelector('.inline-notes-dropdown');
if (detailsEl) {
detailsEl.addEventListener('click', (event) => {
event.stopPropagation();
});
}
}
function bindNotesEditor(useCaseId) {
const inputEl = document.getElementById('user-note-input');
const saveBtn = document.getElementById('save-note-btn');
const imageInputEl = document.getElementById('user-note-image-input');
const imageButtonEl = document.getElementById('user-note-image-btn');
const imageClearBtn = document.getElementById('user-note-image-clear-btn');
const imagePreviewEl = document.getElementById('user-note-image-preview');
const imageStatusEl = document.getElementById('user-note-image-status');
if (!inputEl || !saveBtn) {
return;
}
let currentImageAttachments = [];
const renderImagePreview = () => {
if (!imagePreviewEl) {
return;
}
if (currentImageAttachments.length === 0) {
imagePreviewEl.innerHTML = '';
imagePreviewEl.classList.remove('visible');
if (imageClearBtn) {
imageClearBtn.classList.add('hidden');
}
return;
}
const summaryText = currentImageAttachments.length === 1
? (currentImageAttachments[0].name ? escapeHtml(currentImageAttachments[0].name) : 'Attached image')
: `${currentImageAttachments.length} attached images`;
imagePreviewEl.innerHTML = `
<div class="note-image-preview-grid">
${currentImageAttachments.map((attachment, index) => {
const caption = attachment.name ? escapeHtml(attachment.name) : `Image ${index + 1}`;
return `
<div class="note-image-preview-card">
<button class="note-image-remove-btn" type="button" data-image-index="${index}" aria-label="Remove ${caption}">×</button>
<img src="${escapeHtml(attachment.dataUrl)}" alt="${caption}">
<div class="note-image-preview-meta">
<span>${caption}</span>
</div>
</div>
`;
}).join('')}
<div class="note-image-preview-summary">
<span>${summaryText}</span>
</div>
</div>
`;
imagePreviewEl.classList.add('visible');
if (imageClearBtn) {
imageClearBtn.classList.remove('hidden');
}
};
const saveCurrentNote = () => {
const text = inputEl.value.trim();
if (!text && currentImageAttachments.length === 0) {
return;
}
saveNoteForUseCase(useCaseId, text, currentImageAttachments);
inputEl.value = '';
currentImageAttachments = [];
if (imageInputEl) {
imageInputEl.value = '';
}
renderImagePreview();
renderNotesList(useCaseId);
if (imageStatusEl) {
imageStatusEl.textContent = 'Saved';
}
};
const addImageAttachmentsFromFiles = async (files) => {
const imageFiles = Array.from(files || []).filter(isImageFile);
if (imageFiles.length === 0) {
return;
}
for (const file of imageFiles) {
if (file.size > 2 * 1024 * 1024) {
if (imageStatusEl) {
imageStatusEl.textContent = 'One of the images is too large. Use images under 2 MB.';
}
continue;
}
if (imageStatusEl) {
imageStatusEl.textContent = 'Saving image...';
}
const dataUrl = await fileToDataUrl(file);
currentImageAttachments.push({
dataUrl,
name: file.name,
type: file.type,
size: file.size
});
}
renderImagePreview();
if (imageStatusEl) {
imageStatusEl.textContent = 'Image(s) added';
}
};
const removeImageAttachmentAtIndex = (imageIndex) => {
currentImageAttachments = currentImageAttachments.filter((_, index) => index !== imageIndex);
renderImagePreview();
};
renderImagePreview();
saveBtn.addEventListener('click', () => {
saveCurrentNote();
});
inputEl.addEventListener('paste', async (event) => {
const clipboardItems = Array.from(event.clipboardData?.items || []);
const pastedFiles = clipboardItems
.filter((item) => item.kind === 'file' && isImageFile(item.getAsFile()))
.map((item) => item.getAsFile())
.filter(Boolean);
if (pastedFiles.length === 0) {
return;
}
event.preventDefault();
await addImageAttachmentsFromFiles(pastedFiles);
});
if (imageInputEl) {
imageInputEl.addEventListener('change', async () => {
const files = imageInputEl.files || [];
if (files.length > 0) {
await addImageAttachmentsFromFiles(files);
}
});
}
if (imageButtonEl && imageInputEl) {
imageButtonEl.addEventListener('click', (event) => {
event.stopPropagation();
imageInputEl.click();
});
}
if (imageClearBtn) {
imageClearBtn.addEventListener('click', (event) => {
event.stopPropagation();
currentImageAttachments = [];
if (imageInputEl) {
imageInputEl.value = '';
}
renderImagePreview();
});
}
if (imagePreviewEl) {
imagePreviewEl.addEventListener('click', (event) => {
const removeBtn = event.target.closest('.note-image-remove-btn');
if (!removeBtn) {
return;
}
event.stopPropagation();
const imageIndex = Number(removeBtn.dataset.imageIndex);
if (Number.isInteger(imageIndex)) {
removeImageAttachmentAtIndex(imageIndex);
}
});
}
renderNotesList(useCaseId);
}
function setUserBadge() {
if (!currentUser) {
currentUserLabel.textContent = '';
if (adminPanelBtn) {
adminPanelBtn.classList.add('hidden');
}
return;
}
const groupLabel = groupLabelMap[currentUser.groupId] || 'No Group';
currentUserLabel.textContent = `${currentUser.name}${currentUser.department}${groupLabel}${currentUser.email}`;
showAdminPanelButton();
}
function showApp() {
loginOverlay.classList.add('hidden');
appShell.classList.remove('hidden');
}
function showLogin() {
appShell.classList.add('hidden');
loginOverlay.classList.remove('hidden');
}
function setCurrentUserByEmail(email) {
const users = readStorageObject(USERS_STORAGE_KEY);
const normalizedEmail = normalizeEmail(email);
currentUser = users[normalizedEmail] || null;
if (currentUser) {
try {
localStorage.setItem(ACTIVE_USER_STORAGE_KEY, normalizedEmail);
} catch (error) {
// Ignore storage failures; keep in-memory session active.
}
setUserBadge();
}
}
function persistUserProfile(name, department, email, groupId) {
const users = readStorageObject(USERS_STORAGE_KEY);
const normalizedEmail = normalizeEmail(email);
users[normalizedEmail] = {
name: String(name).trim(),
department: String(department).trim(),
email: normalizedEmail,
groupId: String(groupId).trim(),
lastLoginAt: new Date().toISOString()
};
const writeSucceeded = writeStorageObject(USERS_STORAGE_KEY, users);
if (writeSucceeded) {
setCurrentUserByEmail(normalizedEmail);
return;
}
// Fallback for quota/full storage: allow login for current session.
currentUser = users[normalizedEmail];
setUserBadge();
}
function renderAdminUsersList() {
if (!adminUsersList) {
return;
}
const admins = getAdminUsers();
if (admins.length === 0) {
adminUsersList.innerHTML = '<p class="note-empty">No additional admins added yet.</p>';
return;
}
adminUsersList.innerHTML = admins
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
.map((admin) => `
<div class="admin-list-item">
<p>${escapeHtml(admin.name || admin.email)}</p>
<span>${escapeHtml(admin.department || 'Department')}${escapeHtml(admin.email)}</span>
</div>
`)
.join('');
}
function renderAdminPostUseCaseOptions() {
if (!adminPostUseCaseInput) {
return;
}
adminPostUseCaseInput.innerHTML = allUseCases
.map((useCase) => `<option value="${escapeHtml(useCase.id)}">${escapeHtml(useCase.id)} - ${escapeHtml(useCase.title)}</option>`)
.join('');
}
function renderAdminPostImagePreview() {
if (!adminPostImagePreview) {
return;
}
if (adminPostDraftImages.length === 0) {
adminPostImagePreview.innerHTML = '';
adminPostImagePreview.classList.remove('visible');
if (adminPostImageClearBtn) {
adminPostImageClearBtn.classList.add('hidden');
}
return;
}
adminPostImagePreview.innerHTML = `
<div class="note-image-preview-grid">
${adminPostDraftImages.map((image, index) => {
const caption = image.name ? escapeHtml(image.name) : `Image ${index + 1}`;
return `
<div class="note-image-preview-card">
<button class="note-image-remove-btn" type="button" data-image-index="${index}" aria-label="Remove ${caption}">×</button>
<img src="${escapeHtml(image.dataUrl)}" alt="${caption}">
<div class="note-image-preview-meta"><span>${caption}</span></div>
</div>
`;
}).join('')}
</div>
`;
adminPostImagePreview.classList.add('visible');
if (adminPostImageClearBtn) {
adminPostImageClearBtn.classList.remove('hidden');
}
}
async function addAdminPostImages(files) {
const imageFiles = Array.from(files || []).filter(isImageFile);
if (imageFiles.length === 0) {
return;
}
for (const file of imageFiles) {
if (file.size > 2 * 1024 * 1024) {
if (adminPostImageStatus) {
adminPostImageStatus.textContent = 'One image is too large. Use files under 2 MB.';
}
continue;
}
const dataUrl = await fileToDataUrl(file);
adminPostDraftImages.push({
dataUrl,
name: file.name,
type: file.type,
size: file.size
});
}
if (adminPostImageStatus) {
adminPostImageStatus.textContent = 'Image(s) added';
}
renderAdminPostImagePreview();
}
function removeAdminPostImageAt(index) {
adminPostDraftImages = adminPostDraftImages.filter((_, i) => i !== index);
renderAdminPostImagePreview();
}
function resetAdminPostComposer() {
adminPostDraftImages = [];
adminEditingPostId = null;
if (adminPostTextInput) {
adminPostTextInput.value = '';
}
if (adminPostImageInput) {
adminPostImageInput.value = '';
}
if (adminPostImageStatus) {
adminPostImageStatus.textContent = 'Paste images or choose files.';
}
if (adminPostGroupInput) {
adminPostGroupInput.value = 'group-1';
}
if (adminPostUseCaseInput && adminPostUseCaseInput.options.length > 0) {
adminPostUseCaseInput.selectedIndex = 0;
}
if (adminPostForm) {
const submitBtn = adminPostForm.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.textContent = 'Publish to Group';
}
}
renderAdminPostImagePreview();
}
function beginEditAdminPost(postId) {
const post = getAdminPosts().find((item) => item.id === postId);
if (!post) {
return;
}
adminEditingPostId = post.id;
if (adminPostGroupInput) {
adminPostGroupInput.value = post.targetGroupId;
}
if (adminPostUseCaseInput) {
adminPostUseCaseInput.value = post.useCaseId;
}
if (adminPostTextInput) {
adminPostTextInput.value = post.text || '';
}
adminPostDraftImages = normalizeImageAttachments(post.images || []);
if (adminPostImageStatus) {
adminPostImageStatus.textContent = 'Editing published post';
}
if (adminPostForm) {
const submitBtn = adminPostForm.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.textContent = 'Update Group Note';
}
}
renderAdminPostImagePreview();
renderAdminPostsList();
}
function deleteAdminPost(postId) {
const posts = getAdminPosts();
const nextPosts = posts.filter((post) => post.id !== postId);
saveAdminPosts(nextPosts);
if (adminEditingPostId === postId) {
resetAdminPostComposer();
}
}
function publishAdminPost(targetGroupId, useCaseId, text) {
if (!currentUser) {
return;
}
const posts = getAdminPosts();
posts.unshift({
id: generateId('admin_post'),
targetGroupId,
useCaseId,
text: String(text || '').trim(),
images: normalizeImageAttachments(adminPostDraftImages),
createdAt: new Date().toISOString(),
createdByName: currentUser.name,
createdByEmail: normalizeEmail(currentUser.email),
department: currentUser.department
});
saveAdminPosts(posts);
}
function getUserProfilesForAdmin() {
const users = readStorageObject(USERS_STORAGE_KEY);
return Object.keys(users)
.map((email) => users[email])
.filter((profile) => profile && profile.email)
.sort((a, b) => new Date(b.lastLoginAt || 0).getTime() - new Date(a.lastLoginAt || 0).getTime());
}
function renderAdminLoginsList() {
if (!adminLoginsList) {
return;
}
const users = getUserProfilesForAdmin();
if (users.length === 0) {
adminLoginsList.innerHTML = '<p class="note-empty">No user logins yet.</p>';
return;
}
adminLoginsList.innerHTML = users
.map((user) => {
const email = normalizeEmail(user.email);
const selectedClass = adminSelectedUserEmail === email ? ' admin-list-item-selected' : '';
const ts = user.lastLoginAt ? new Date(user.lastLoginAt).toLocaleString() : 'No login time';
return `
<button class="admin-list-item admin-user-item${selectedClass}" type="button" data-user-email="${escapeHtml(email)}">
<p>${escapeHtml(user.name || email)}</p>
<span>${escapeHtml(user.department || '')}${escapeHtml(getGroupLabel(user.groupId))}</span>
<span>${escapeHtml(ts)}</span>
</button>
`;
})
.join('');
}
function getPersonalDraftRowsForUser(email) {
const allDrafts = readStorageObject(PERSONAL_NOTES_STORAGE_KEY);
const userDrafts = allDrafts[normalizeEmail(email)] || {};
return Object.keys(userDrafts)
.map((useCaseId) => {
const entry = userDrafts[useCaseId];
if (!entry) {
return null;
}
const normalized = typeof entry === 'string'
? { text: entry, images: [], updatedAt: '' }
: {
text: entry.text || '',
images: getStoredNoteImages(entry),
updatedAt: entry.updatedAt || ''
};
if (!normalized.text && normalized.images.length === 0) {
return null;
}
return {
useCaseId,
text: normalized.text,
images: normalized.images,
updatedAt: normalized.updatedAt
};
})
.filter(Boolean)
.sort((a, b) => new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime());
}
function getSubmittedRowsForUser(email) {
const notes = readStorageObject(NOTES_STORAGE_KEY);
const perUseCase = notes[normalizeEmail(email)] || {};
const rows = [];
Object.keys(perUseCase).forEach((useCaseId) => {
const entries = Array.isArray(perUseCase[useCaseId]) ? perUseCase[useCaseId] : [];
entries.forEach((entry) => {
rows.push({
useCaseId,
text: entry.text || '',
images: getStoredNoteImages(entry),
createdAt: entry.createdAt || ''
});
});
});
return rows.sort((a, b) => new Date(b.createdAt || 0).getTime() - new Date(a.createdAt || 0).getTime());
}
function renderSelectedUserData() {
if (!adminUserData) {
return;
}
if (!adminSelectedUserEmail) {
adminUserData.innerHTML = '<p class="note-empty">Select a user in Logins to view their data.</p>';
return;
}
const draftRows = getPersonalDraftRowsForUser(adminSelectedUserEmail);
const submittedRows = getSubmittedRowsForUser(adminSelectedUserEmail);
adminUserData.innerHTML = `
<div class="admin-user-data-grid">
<div class="admin-user-data-card">
<h4>Autosaved In-Progress Notes</h4>
${draftRows.length === 0 ? '<p class="note-empty">No autosaved content.</p>' : draftRows.map((row) => `
<div class="admin-list-item">
<p>${escapeHtml(row.useCaseId)}</p>
${row.text ? `<span>${escapeHtml(row.text)}</span>` : '<span>-</span>'}
${renderNoteImagesBlock(row.images)}
</div>
`).join('')}
</div>
<div class="admin-user-data-card">
<h4>Submitted Notes</h4>
${submittedRows.length === 0 ? '<p class="note-empty">No submitted notes.</p>' : submittedRows.map((row) => {
const ts = row.createdAt ? new Date(row.createdAt).toLocaleString() : '-';
return `
<div class="admin-list-item">
<p>${escapeHtml(row.useCaseId)}${escapeHtml(ts)}</p>
${row.text ? `<span>${escapeHtml(row.text)}</span>` : '<span>-</span>'}
${renderNoteImagesBlock(row.images)}
</div>
`;
}).join('')}
</div>
</div>
`;
}
function downloadSelectedUserResponsesPdf() {
if (!adminSelectedUserEmail) {
if (adminUserData) {
adminUserData.innerHTML = '<p class="note-empty">Select a user in Logins before downloading.</p>';
}
return;
}
const jspdfNamespace = window.jspdf;
if (!jspdfNamespace || !jspdfNamespace.jsPDF) {
return;
}
const users = readStorageObject(USERS_STORAGE_KEY);
const profile = users[adminSelectedUserEmail] || {};
const draftRows = getPersonalDraftRowsForUser(adminSelectedUserEmail);
const submittedRows = getSubmittedRowsForUser(adminSelectedUserEmail);
const doc = new jspdfNamespace.jsPDF({ unit: 'pt', format: 'a4' });
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 40;
const maxWidth = pageWidth - margin * 2;
let y = margin;
const ensureSpace = (required = 24) => {
if (y + required <= pageHeight - margin) {
return;
}
doc.addPage();
y = margin;
};
const addHeading = (text) => {
ensureSpace(28);
doc.setFont('helvetica', 'bold');
doc.setFontSize(14);
doc.text(text, margin, y);
y += 18;
};
const addLine = (text, fontSize = 10) => {
const safeText = String(text || '-');
const lines = doc.splitTextToSize(safeText, maxWidth);
doc.setFont('helvetica', 'normal');
doc.setFontSize(fontSize);
lines.forEach((line) => {
ensureSpace(14);
doc.text(line, margin, y);
y += 13;
});
};
const addImages = (images) => {
const entries = Array.isArray(images) ? images : [];
if (entries.length === 0) {
addLine('Images: 0');
return;
}
addLine(`Images: ${entries.length}`);
entries.forEach((image, idx) => {
const dataUrl = String(image?.dataUrl || '');
if (!dataUrl) {
return;
}
const label = image?.name ? String(image.name) : `Image ${idx + 1}`;
addLine(`- ${label}`);
try {
const match = /^data:image\/(png|jpeg|jpg);/i.exec(dataUrl);
const format = match && match[1] && match[1].toLowerCase().startsWith('png') ? 'PNG' : 'JPEG';
const props = doc.getImageProperties(dataUrl);
const maxImgW = maxWidth;
const maxImgH = 220;
let drawW = maxImgW;
let drawH = (drawW * props.height) / props.width;
if (drawH > maxImgH) {
drawH = maxImgH;
drawW = (drawH * props.width) / props.height;
}
ensureSpace(drawH + 14);
doc.addImage(dataUrl, format, margin, y, drawW, drawH);
y += drawH + 10;
} catch (error) {
addLine('[Image could not be embedded in PDF]');
}
});
};
addHeading('LVMWD AI Usecases - User Responses');
addLine(`Generated: ${new Date().toLocaleString()}`);
addLine(`Name: ${profile.name || adminSelectedUserEmail}`);
addLine(`Department: ${profile.department || '-'}`);
addLine(`Group: ${getGroupLabel(profile.groupId || '')}`);
addLine(`Email: ${adminSelectedUserEmail}`);
y += 8;
addHeading('Autosaved In-Progress Notes');
if (draftRows.length === 0) {
addLine('No autosaved content.');
} else {
draftRows.forEach((row, index) => {
addLine(`${index + 1}. Use Case: ${row.useCaseId}`);
addLine(`Updated: ${row.updatedAt ? new Date(row.updatedAt).toLocaleString() : '-'}`);
addLine(`Note: ${row.text || '-'}`);
addImages(row.images || []);
y += 6;
});
}
y += 8;
addHeading('Submitted Notes');
if (submittedRows.length === 0) {
addLine('No submitted notes.');
} else {
submittedRows.forEach((row, index) => {
addLine(`${index + 1}. Use Case: ${row.useCaseId}`);
addLine(`Submitted: ${row.createdAt ? new Date(row.createdAt).toLocaleString() : '-'}`);
addLine(`Note: ${row.text || '-'}`);
addImages(row.images || []);
y += 6;
});
}
const fileBase = (profile.name || adminSelectedUserEmail || 'responses').replace(/[^a-zA-Z0-9_-]+/g, '_');
doc.save(`${fileBase}_responses.pdf`);
}
function renderAdminPostsList() {
if (!adminPostsList) {
return;
}
const posts = getAdminPosts();
if (posts.length === 0) {
adminPostsList.innerHTML = '<p class="note-empty">No published group notes yet.</p>';
return;
}
adminPostsList.innerHTML = posts
.map((post) => {
const ts = new Date(post.createdAt).toLocaleString();
const activeClass = adminEditingPostId === post.id ? ' admin-list-item-selected' : '';
return `
<div class="admin-list-item${activeClass}">
<p>${escapeHtml(post.useCaseId)}${escapeHtml(getGroupLabel(post.targetGroupId))}</p>
<span>${escapeHtml(post.text || '')}</span>
${renderNoteImagesBlock(post.images || [])}
<span>${escapeHtml(post.createdByName || 'Admin')}${escapeHtml(ts)}</span>
<div class="admin-inline-actions">
<button class="note-image-btn" type="button" data-admin-action="edit" data-post-id="${escapeHtml(post.id)}">Edit</button>
<button class="note-image-clear-btn" type="button" data-admin-action="delete" data-post-id="${escapeHtml(post.id)}">Delete</button>
</div>
</div>
`;
})
.join('');
}
function getAdminSubmissionRows() {
const users = readStorageObject(USERS_STORAGE_KEY);
const notes = readStorageObject(NOTES_STORAGE_KEY);
const rows = [];
Object.keys(notes).forEach((emailKey) => {
const perUseCase = notes[emailKey] || {};
Object.keys(perUseCase).forEach((useCaseId) => {
const entries = Array.isArray(perUseCase[useCaseId]) ? perUseCase[useCaseId] : [];
entries.forEach((note) => {
const userProfile = users[emailKey] || {};
rows.push({
timestamp: note.createdAt || '',
name: note.authorName || userProfile.name || emailKey,
department: note.department || userProfile.department || '',
groupId: note.groupId || userProfile.groupId || '',
useCaseId,
noteText: note.text || '',
images: getStoredNoteImages(note)
});
});
});
});
return rows.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
}
function renderAdminSubmissionsTable() {
if (!adminSubmissionsTable) {
return;
}
const rows = getAdminSubmissionRows();
if (rows.length === 0) {
adminSubmissionsTable.innerHTML = '<p class="note-empty">No user notes submitted yet.</p>';
return;
}
adminSubmissionsTable.innerHTML = `
<table class="admin-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Name</th>
<th>Department</th>
<th>Group</th>
<th>Use Case</th>
<th>Notes / Images</th>
</tr>
</thead>
<tbody>
${rows.map((row) => {
const ts = row.timestamp ? new Date(row.timestamp).toLocaleString() : '-';
const images = renderNoteImagesBlock(row.images);
return `
<tr>
<td>${escapeHtml(ts)}</td>
<td>${escapeHtml(row.name)}</td>
<td>${escapeHtml(row.department)}</td>
<td>${escapeHtml(getGroupLabel(row.groupId))}</td>
<td>${escapeHtml(row.useCaseId)}</td>
<td>
${row.noteText ? `<p>${escapeHtml(row.noteText)}</p>` : '<p>-</p>'}
${images}
</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}
function showAdminPanelButton() {
if (!adminPanelBtn) {
return;
}
if (isCurrentUserAdmin()) {
adminPanelBtn.classList.remove('hidden');
} else {
adminPanelBtn.classList.add('hidden');
}
}
function openAdminView() {
if (!isCurrentUserAdmin() || !adminView) {
return;
}
categoriesView.classList.remove('active');
useCasesView.classList.remove('active');
detailView.classList.remove('active');
categoriesView.style.display = 'none';
useCasesView.style.display = 'none';
detailView.style.display = 'none';
adminView.style.display = 'block';
adminView.classList.add('active');
renderAdminUsersList();
renderAdminLoginsList();
renderSelectedUserData();
renderAdminPostsList();
renderAdminSubmissionsTable();
}
// Modal Functions
function openDetailModal(useCaseId, useCase, categoryColor) {
const detail = getUseCaseDetail(useCaseId, useCase.title, useCase.desc);
const contextBlock = detail.context ? `
<div class="section-card info-card">
<div class="section-icon"><i class="fa-solid fa-info-circle"></i></div>
<h3>Context & Challenge</h3>
<p>${detail.context}</p>
${detail.challenges ? `<p class="sub-text"><strong>Key Challenges:</strong> ${detail.challenges}</p>` : ''}
</div>
` : '';
const expandedBlock = detail.expandedText ? `
<div class="expanded-description">
<h3>Detailed Overview</h3>
<p>${detail.expandedText}</p>
</div>
` : '';
const outcomesBlock = detail.expectedOutcomes ? `
<div class="section-card outcomes-card">
<div class="section-icon"><i class="fa-solid fa-target"></i></div>
<h3>Expected Outcomes</h3>
<p>${detail.expectedOutcomes}</p>
</div>
` : '';
const applicationsBlock = detail.industryApplications && detail.industryApplications.length > 0 ? `
<div class="section-card applications-card">
<div class="section-icon"><i class="fa-solid fa-industry"></i></div>
<h3>Applications & Use Cases</h3>
<ul>
${detail.industryApplications.map(app => `<li>${app}</li>`).join('')}
</ul>
</div>
` : '';
const notesBlock = currentUser ? `
<details class="user-notes-dropdown">
<summary>
<span><i class="fa-solid fa-chevron-right"></i> Add Notes / Suggestions</span>
<small>for this use case</small>
</summary>
<div class="user-notes-panel">
<p class="notes-user-line">Signed in as <strong>${currentUser.name}</strong> from <strong>${currentUser.department}</strong></p>
<textarea id="user-note-input" class="user-note-input" placeholder="Add your suggestion or note for this use case..."></textarea>
<div class="note-image-tools">
<button id="user-note-image-btn" class="note-image-btn" type="button">Add Images</button>
<button id="user-note-image-clear-btn" class="note-image-clear-btn hidden" type="button">Remove Images</button>
<input id="user-note-image-input" class="hidden-file-input" type="file" accept="image/*" multiple>
<p id="user-note-image-status" class="note-image-status">You can paste images here or choose files.</p>
</div>
<div id="user-note-image-preview" class="note-image-preview"></div>
<div class="notes-actions">
<button id="save-note-btn" class="note-save-btn" type="button">Save Note</button>
</div>
<div id="user-notes-list" class="user-notes-list"></div>
</div>
</details>
` : '';
const adminBlock = renderAdminPostBlock(useCaseId);
modalDetailContent.innerHTML = `
<div class="detail-use-case">
<div class="detail-header">
<div class="detail-id-title">
<span class="detail-id-badge">${useCaseId}</span>
<div>
<h2>${useCase.title}</h2>
</div>
</div>
</div>
<div class="detail-description">
<h3>Overview</h3>
<p>${detail.overview || useCase.desc}</p>
</div>
${adminBlock}
${expandedBlock}
<div class="detail-sections">
<div class="section-card">
<div class="section-icon"><i class="fa-solid fa-lightbulb"></i></div>
<h3>Key Benefits</h3>
<ul>
${(detail.keyBenefits || detail.impact || ['Enhanced capabilities']).map(benefit => `<li>${benefit}</li>`).join('')}
</ul>
</div>
<div class="section-card">
<div class="section-icon"><i class="fa-solid fa-cogs"></i></div>
<h3>How It Works</h3>
<p>${detail.implementation || 'AI systems analyze data and patterns to provide intelligent recommendations and automate processes for improved outcomes.'}</p>
</div>
<div class="section-card">
<div class="section-icon"><i class="fa-solid fa-chart-line"></i></div>
<h3>Impact Areas</h3>
<ul>
${(detail.impact || ['Efficiency', 'Quality', 'Cost']).map(impact => `<li>${impact}</li>`).join('')}
</ul>
</div>
</div>
${contextBlock}
${outcomesBlock}
${applicationsBlock}
${notesBlock}
</div>
`;
bindNotesEditor(useCaseId);
detailModal.classList.add('visible');
document.body.style.overflow = 'hidden';
}
function closeDetailModal() {
detailModal.classList.remove('visible');
document.body.style.overflow = 'auto';
}
modalClose.addEventListener('click', closeDetailModal);
// Close modal when clicking outside of modal content
detailModal.addEventListener('click', (e) => {
if (e.target === detailModal) {
closeDetailModal();
}
});
// Close modal with Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && detailModal.classList.contains('visible')) {
closeDetailModal();
}
});
// Initialize App
async function init() {
await initializeSupabaseDataLayer();
renderAdminPostUseCaseOptions();
const activeEmail = localStorage.getItem(ACTIVE_USER_STORAGE_KEY);
if (activeEmail) {
setCurrentUserByEmail(activeEmail);
}
if (currentUser && currentUser.groupId) {
ensureCurrentUserAdminSeed();
showApp();
renderGroups();
} else {
if (currentUser && !currentUser.groupId) {
localStorage.removeItem(ACTIVE_USER_STORAGE_KEY);
currentUser = null;
}
showLogin();
}
}
function renderGroups() {
categoriesGrid.innerHTML = '';
const accessibleGroups = getAccessibleGroups();
accessibleGroups.forEach((group) => {
const card = document.createElement('div');
card.className = 'glass-card';
card.style.borderColor = group.color;
card.innerHTML = `
<div class="category-icon" style="color: ${group.color}">
<i class="fa-solid ${group.icon}"></i>
</div>
<h3 style="color: ${group.color}">${group.title}</h3>
<p>${group.description}</p>
<div class="meta">${getGroupUseCaseCount(group)} Total Use Cases</div>
`;
card.addEventListener('click', () => showGroup(group));
categoriesGrid.appendChild(card);
});
}
function showGroup(group) {
if (!currentUser) {
return;
}
const isManager = currentUser.groupId === 'all-groups';
if (!isManager && currentUser.groupId !== group.id) {
return;
}
// Hide Categories, Show Use Cases
categoriesView.classList.remove('active');
// Add brief delay to allow exit animation (if implemented) or just immediate transition
setTimeout(() => {
categoriesView.style.display = 'none';
useCasesView.style.display = 'block';
// Brief delay before adding active class to trigger CSS animation
setTimeout(() => {
useCasesView.classList.add('active');
renderGroupUseCases(group);
}, 50);
}, 400);
}
function renderUseCaseCard(useCase, color, index) {
const card = document.createElement('div');
card.className = 'use-case-card clickable';
card.style.animationDelay = `${index * 0.05}s`;
card.style.borderTopColor = color;
const inlineNotesBlock = currentUser
? `
<details class="user-notes-dropdown inline-notes-dropdown">
<summary>
<span><i class="fa-solid fa-chevron-right"></i> Add Notes / Suggestions</span>
<small>${useCase.id}</small>
</summary>
<div class="user-notes-panel inline-notes-panel">
<button class="inline-note-clear-btn" type="button" aria-label="Clear note">×</button>
<p class="notes-user-line">Signed in as <strong>${escapeHtml(currentUser.name)}</strong> from <strong>${escapeHtml(currentUser.department)}</strong></p>
<textarea class="user-note-input inline-note-input" placeholder="Add your suggestion for ${useCase.id}..."></textarea>
<div class="note-image-tools inline-note-image-tools">
<button class="inline-note-image-btn note-image-btn" type="button">Add Images</button>
<button class="inline-note-image-clear-btn note-image-clear-btn hidden" type="button">Remove Images</button>
<input class="inline-note-image-input hidden-file-input" type="file" accept="image/*" multiple>
<p class="note-image-status inline-note-image-status">Paste images or choose files.</p>
</div>
<div class="inline-note-image-preview note-image-preview"></div>
<p class="inline-note-status">Auto-saved</p>
</div>
</details>
`
: '';
const adminBlock = renderAdminPostBlock(useCase.id);
card.innerHTML = `
<h4 style="color: ${color}">
<span class="id-badge" style="color: ${color}; border-color: ${color}40;">${useCase.id}</span>
${useCase.title}
</h4>
<p>${useCase.desc}</p>
${adminBlock}
${inlineNotesBlock}
`;
if (currentUser) {
bindInlineNotesEditor(card, useCase.id);
}
return card;
}
function renderGroupUseCases(group) {
// Update Header
activeCategoryIcon.className = `fa-solid ${group.icon}`;
activeCategoryIcon.style.color = group.color;
activeCategoryTitle.textContent = group.title;
activeCategoryTitle.style.color = group.color;
activeCategoryCount.textContent = `${getGroupUseCaseCount(group)} Total Use Cases`;
// Store current category for reference
currentCategory = group;
// Clear previous use cases
useCasesContainer.innerHTML = '';
group.sections.forEach((section) => {
const sectionBlock = document.createElement('details');
sectionBlock.className = 'group-section';
sectionBlock.innerHTML = `
<summary class="group-section-header">
<h3>${section.title}</h3>
<div class="group-section-meta">
<p>${section.useCases.length} Use Cases</p>
<i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
</div>
</summary>
<div class="group-use-cases-grid"></div>
`;
const sectionGrid = sectionBlock.querySelector('.group-use-cases-grid');
section.useCases.forEach((useCase, index) => {
sectionGrid.appendChild(renderUseCaseCard(useCase, group.color, index));
});
useCasesContainer.appendChild(sectionBlock);
});
}
// Back Button Navigation
backBtn.addEventListener('click', () => {
useCasesView.classList.remove('active');
setTimeout(() => {
useCasesView.style.display = 'none';
categoriesView.style.display = 'block';
setTimeout(() => {
categoriesView.classList.add('active');
}, 50);
}, 400);
});
function handleLoginSubmit() {
if (!loginForm) {
return;
}
if (!loginForm.checkValidity()) {
loginForm.reportValidity();
return;
}
const name = loginNameInput.value.trim();
const department = loginDepartmentInput.value.trim();
const email = normalizeEmail(loginEmailInput.value);
const groupId = (loginGroupInput.value || '').trim();
if (!name || !department || !email || !groupId) {
return;
}
persistUserProfile(name, department, email, groupId);
ensureCurrentUserAdminSeed();
renderAdminPostUseCaseOptions();
showApp();
renderGroups();
}
function closeAdminViewToGroups() {
if (!adminView) {
return;
}
adminView.classList.remove('active');
adminView.style.display = 'none';
categoriesView.style.display = 'block';
categoriesView.classList.add('active');
}
if (loginForm) {
loginForm.addEventListener('submit', (e) => {
e.preventDefault();
handleLoginSubmit();
});
}
if (loginSubmitBtn) {
loginSubmitBtn.addEventListener('click', (e) => {
e.preventDefault();
handleLoginSubmit();
});
}
if (adminPanelBtn) {
adminPanelBtn.addEventListener('click', () => {
openAdminView();
});
}
if (adminBackBtn) {
adminBackBtn.addEventListener('click', () => {
closeAdminViewToGroups();
});
}
if (addAdminForm) {
addAdminForm.addEventListener('submit', (e) => {
e.preventDefault();
if (!isCurrentUserAdmin()) {
return;
}
const nameInput = document.getElementById('new-admin-name');
const deptInput = document.getElementById('new-admin-department');
const emailInput = document.getElementById('new-admin-email');
const email = normalizeEmail(emailInput?.value);
const name = String(nameInput?.value || '').trim();
const department = String(deptInput?.value || '').trim();
if (!email || !name || !department) {
return;
}
const admins = getAdminUsers();
const exists = admins.some((admin) => normalizeEmail(admin.email) === email);
if (!exists) {
admins.push({
id: generateId('admin'),
name,
department,
email,
createdAt: new Date().toISOString(),
addedBy: normalizeEmail(currentUser?.email || '')
});
saveAdminUsers(admins);
}
addAdminForm.reset();
renderAdminUsersList();
renderAdminLoginsList();
});
}
if (adminPostImageBtn && adminPostImageInput) {
adminPostImageBtn.addEventListener('click', () => {
adminPostImageInput.click();
});
}
if (adminPostImageInput) {
adminPostImageInput.addEventListener('change', async () => {
const files = adminPostImageInput.files || [];
if (files.length > 0) {
await addAdminPostImages(files);
}
});
}
if (adminPostImageClearBtn) {
adminPostImageClearBtn.addEventListener('click', () => {
adminPostDraftImages = [];
if (adminPostImageInput) {
adminPostImageInput.value = '';
}
renderAdminPostImagePreview();
});
}
if (adminPostImagePreview) {
adminPostImagePreview.addEventListener('click', (event) => {
const removeBtn = event.target.closest('.note-image-remove-btn');
if (!removeBtn) {
return;
}
const imageIndex = Number(removeBtn.dataset.imageIndex);
if (Number.isInteger(imageIndex)) {
removeAdminPostImageAt(imageIndex);
}
});
}
if (adminLoginsList) {
adminLoginsList.addEventListener('click', (event) => {
const userBtn = event.target.closest('.admin-user-item');
if (!userBtn) {
return;
}
const email = normalizeEmail(userBtn.dataset.userEmail || '');
adminSelectedUserEmail = email || null;
renderAdminLoginsList();
renderSelectedUserData();
});
}
if (adminPostsList) {
adminPostsList.addEventListener('click', (event) => {
const actionBtn = event.target.closest('[data-admin-action]');
if (!actionBtn) {
return;
}
const postId = String(actionBtn.dataset.postId || '');
const action = String(actionBtn.dataset.adminAction || '');
if (!postId) {
return;
}
if (action === 'edit') {
beginEditAdminPost(postId);
return;
}
if (action === 'delete') {
deleteAdminPost(postId);
renderAdminPostsList();
renderAdminSubmissionsTable();
if (currentCategory) {
renderGroupUseCases(currentCategory);
}
}
});
}
if (adminPostTextInput) {
adminPostTextInput.addEventListener('paste', async (event) => {
const items = Array.from(event.clipboardData?.items || []);
const pastedFiles = items
.filter((item) => item.kind === 'file' && isImageFile(item.getAsFile()))
.map((item) => item.getAsFile())
.filter(Boolean);
if (pastedFiles.length === 0) {
return;
}
event.preventDefault();
await addAdminPostImages(pastedFiles);
});
}
if (adminPostForm) {
adminPostForm.addEventListener('submit', (e) => {
e.preventDefault();
if (!isCurrentUserAdmin()) {
return;
}
const groupId = String(adminPostGroupInput?.value || '').trim();
const useCaseId = String(adminPostUseCaseInput?.value || '').trim();
const text = String(adminPostTextInput?.value || '').trim();
const hasImages = adminPostDraftImages.length > 0;
if (!groupId || !useCaseId || (!text && !hasImages)) {
if (adminPostImageStatus) {
adminPostImageStatus.textContent = 'Add a note or at least one image before publishing.';
}
return;
}
if (adminEditingPostId) {
const posts = getAdminPosts();
const targetPost = posts.find((post) => post.id === adminEditingPostId);
if (targetPost) {
targetPost.targetGroupId = groupId;
targetPost.useCaseId = useCaseId;
targetPost.text = text;
targetPost.images = normalizeImageAttachments(adminPostDraftImages);
targetPost.updatedAt = new Date().toISOString();
saveAdminPosts(posts);
}
} else {
publishAdminPost(groupId, useCaseId, text);
}
resetAdminPostComposer();
renderAdminPostsList();
renderAdminSubmissionsTable();
if (currentCategory) {
renderGroupUseCases(currentCategory);
}
});
}
if (adminDownloadPdfBtn) {
adminDownloadPdfBtn.addEventListener('click', () => {
downloadSelectedUserResponsesPdf();
});
}
logoutBtn.addEventListener('click', () => {
localStorage.removeItem(ACTIVE_USER_STORAGE_KEY);
currentUser = null;
adminPostDraftImages = [];
setUserBadge();
showLogin();
categoriesView.classList.add('active');
categoriesView.style.display = 'block';
useCasesView.classList.remove('active');
useCasesView.style.display = 'none';
if (adminView) {
adminView.classList.remove('active');
adminView.style.display = 'none';
}
closeDetailModal();
});
// Run Init
document.addEventListener('DOMContentLoaded', () => {
init();
});
window.addEventListener('beforeunload', () => {
flushCloudWrites();
});