- Hero mockup: enhanced 3D perspective and shadow - Testimonials: illustrated SVG avatars - Growth pricing card: visual prominence (scale, gradient, badge) - Most Popular badge: repositioned to avoid overlapping heading - Nav: added Log In link next to Start Free Trial - Fixed btn-primary text colour on anchor tags (white on blue) - Fixed cursor: default on all non-interactive elements - Disabled user-select on non-form content to prevent text caret
780 lines
24 KiB
HTML
780 lines
24 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<!-- Primary Meta Tags -->
|
|
<title>Alert History | TenderRadar</title>
|
|
<meta name="title" content="Alert History | TenderRadar">
|
|
<meta name="description" content="View your tender alert history and past notifications.">
|
|
<meta name="keywords" content="tender alerts, alert history">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
|
|
<!-- Canonical URL -->
|
|
<link rel="canonical" href="https://tenderradar.co.uk/alerts.html">
|
|
|
|
<!-- Open Graph / Facebook -->
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://tenderradar.co.uk/alerts.html">
|
|
<meta property="og:title" content="TenderRadar Alert History">
|
|
<meta property="og:description" content="Your tender alert history.">
|
|
<meta property="og:image" content="https://tenderradar.co.uk/og-image.png">
|
|
<meta property="og:locale" content="en_GB">
|
|
<meta property="og:site_name" content="TenderRadar">
|
|
|
|
<!-- Twitter Card -->
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
<meta name="twitter:url" content="https://tenderradar.co.uk/alerts.html">
|
|
<meta name="twitter:title" content="TenderRadar Alert History">
|
|
<meta name="twitter:description" content="Your tender alert history.">
|
|
<meta name="twitter:image" content="https://tenderradar.co.uk/twitter-card.png">
|
|
|
|
<!-- Preconnect for Performance -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- Favicon -->
|
|
<link rel="icon">
|
|
|
|
<!-- Stylesheet -->
|
|
<link rel="stylesheet" href="styles.css">
|
|
<style>
|
|
/* Alerts Page Specific Styles */
|
|
.alerts-header {
|
|
padding: 2rem 0;
|
|
border-bottom: 1px solid var(--border);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.alerts-header h1 {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.alerts-header p {
|
|
color: var(--text-secondary);
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.alerts-controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
padding: 1.5rem;
|
|
background: var(--bg-secondary);
|
|
border-radius: 0.75rem;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.control-group label {
|
|
font-weight: 600;
|
|
margin-bottom: 0.5rem;
|
|
color: var(--text-primary);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.control-group input,
|
|
.control-group select {
|
|
padding: 0.625rem 0.75rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 0.375rem;
|
|
font-family: inherit;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.control-group input:focus,
|
|
.control-group select:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
|
|
}
|
|
|
|
.filter-actions {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.filter-actions button {
|
|
padding: 0.625rem 1.5rem;
|
|
border-radius: 0.375rem;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn-filter {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-filter:hover {
|
|
background: var(--primary-dark);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-clear {
|
|
background: var(--bg-alt);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.btn-clear:hover {
|
|
background: var(--border);
|
|
}
|
|
|
|
/* Alerts Table/List */
|
|
.alerts-container {
|
|
background: white;
|
|
border-radius: 0.75rem;
|
|
box-shadow: var(--shadow-sm);
|
|
border: 1px solid var(--border);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.alerts-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.9375rem;
|
|
}
|
|
|
|
.alerts-table thead {
|
|
background: var(--bg-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.alerts-table th {
|
|
padding: 1rem 1.5rem;
|
|
text-align: left;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
font-size: 0.875rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.alerts-table td {
|
|
padding: 1.25rem 1.5rem;
|
|
border-bottom: 1px solid var(--border);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.alerts-table tr:hover {
|
|
background: var(--bg-secondary);
|
|
}
|
|
|
|
.alert-title {
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.alert-date {
|
|
font-size: 0.8125rem;
|
|
color: var(--text-light);
|
|
}
|
|
|
|
.match-score {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.5rem 0.75rem;
|
|
background: rgba(30, 64, 175, 0.1);
|
|
color: var(--primary);
|
|
border-radius: 0.375rem;
|
|
font-weight: 600;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.match-score.high {
|
|
background: rgba(34, 197, 94, 0.1);
|
|
color: #15803d;
|
|
}
|
|
|
|
.match-score.medium {
|
|
background: rgba(245, 158, 11, 0.1);
|
|
color: #92400e;
|
|
}
|
|
|
|
.match-score.low {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
color: #7f1d1d;
|
|
}
|
|
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 0.25rem;
|
|
font-weight: 600;
|
|
font-size: 0.75rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.status-new {
|
|
background: rgba(59, 130, 246, 0.1);
|
|
color: #1e40af;
|
|
}
|
|
|
|
.status-viewed {
|
|
background: rgba(156, 163, 175, 0.1);
|
|
color: #4b5563;
|
|
}
|
|
|
|
.status-saved {
|
|
background: rgba(245, 158, 11, 0.1);
|
|
color: #92400e;
|
|
}
|
|
|
|
.status-applied {
|
|
background: rgba(34, 197, 94, 0.1);
|
|
color: #15803d;
|
|
}
|
|
|
|
.alert-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.action-btn {
|
|
padding: 0.375rem 0.75rem;
|
|
border: 1px solid var(--border);
|
|
background: white;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.75rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
background: var(--bg-alt);
|
|
border-color: var(--primary);
|
|
color: var(--primary);
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 4rem 2rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state-icon {
|
|
font-size: 3rem;
|
|
margin-bottom: 1rem;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.empty-state h3 {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.empty-state p {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.empty-state .btn {
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Pagination */
|
|
.pagination {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 1.5rem;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.pagination button,
|
|
.pagination a {
|
|
padding: 0.5rem 0.75rem;
|
|
border: 1px solid var(--border);
|
|
background: white;
|
|
color: var(--text-primary);
|
|
border-radius: 0.375rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.pagination button:hover,
|
|
.pagination a:hover {
|
|
background: var(--bg-alt);
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.pagination .active {
|
|
background: var(--primary);
|
|
color: white;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
/* Status Messages */
|
|
.alert {
|
|
padding: 1rem 1.5rem;
|
|
border-radius: 0.5rem;
|
|
margin-bottom: 1.5rem;
|
|
display: none;
|
|
}
|
|
|
|
.alert.show {
|
|
display: block;
|
|
}
|
|
|
|
.alert-success {
|
|
background: rgba(34, 197, 94, 0.1);
|
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
color: #15803d;
|
|
}
|
|
|
|
.alert-error {
|
|
background: rgba(239, 68, 68, 0.1);
|
|
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
color: #7f1d1d;
|
|
}
|
|
|
|
/* Loading State */
|
|
.loading {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: 3rem;
|
|
}
|
|
|
|
.spinner {
|
|
width: 40px;
|
|
height: 160px;
|
|
border: 4px solid var(--border);
|
|
border-top-color: var(--primary);
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* Mobile Responsive */
|
|
@media (max-width: 768px) {
|
|
.alerts-controls {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.alerts-table {
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
.alerts-table th,
|
|
.alerts-table td {
|
|
padding: 0.75rem;
|
|
}
|
|
|
|
.alert-actions {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.action-btn {
|
|
flex: 1;
|
|
min-width: 60px;
|
|
}
|
|
|
|
.alerts-header h1 {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
/* Hide less important columns on mobile */
|
|
.col-date-matched {
|
|
display: none;
|
|
}
|
|
|
|
.match-score {
|
|
font-size: 0.75rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.alerts-controls {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.alerts-table th,
|
|
.alerts-table td {
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.alert-title {
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.match-score {
|
|
display: block;
|
|
width: 100%;
|
|
margin: 0.5rem 0;
|
|
}
|
|
|
|
.status-badge {
|
|
font-size: 0.65rem;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Header/Navigation -->
|
|
<header class="header" role="banner">
|
|
<nav class="nav container" role="navigation" aria-label="Main navigation">
|
|
<div class="nav-brand">
|
|
<img src="/logo.png" alt="TenderRadar" class="logo-icon">
|
|
</div>
|
|
<ul class="nav-menu">
|
|
<li><a href="/">Dashboard</a></li>
|
|
<li><a href="/alerts.html" class="active-nav">Alerts</a></li>
|
|
<li><a href="/profile.html">Profile</a></li>
|
|
<li><button id="logoutBtn" class="btn btn-outline btn-sm">Logout</button></li>
|
|
</ul>
|
|
<button class="mobile-toggle" aria-label="Toggle navigation menu" aria-expanded="false">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</button>
|
|
</nav>
|
|
</header>
|
|
|
|
<!-- Main Container -->
|
|
<div class="container">
|
|
<!-- Header -->
|
|
<div class="alerts-header">
|
|
<h1>Alert History</h1>
|
|
<p>View all tenders that matched your preferences</p>
|
|
</div>
|
|
|
|
<!-- Status Messages -->
|
|
<div id="successMessage" class="alert alert-success"></div>
|
|
<div id="errorMessage" class="alert alert-error"></div>
|
|
|
|
<!-- Filter Controls -->
|
|
<div class="alerts-controls">
|
|
<div class="control-group">
|
|
<label for="filterFromDate">From Date</label>
|
|
<input type="date" id="filterFromDate">
|
|
</div>
|
|
<div class="control-group">
|
|
<label for="filterToDate">To Date</label>
|
|
<input type="date" id="filterToDate">
|
|
</div>
|
|
<div class="control-group">
|
|
<label for="filterStatus">Status</label>
|
|
<select id="filterStatus">
|
|
<option value="">All Statuses</option>
|
|
<option value="new">New</option>
|
|
<option value="viewed">Viewed</option>
|
|
<option value="saved">Saved</option>
|
|
<option value="applied">Applied</option>
|
|
</select>
|
|
</div>
|
|
<div class="control-group">
|
|
<label for="filterScore">Match Score</label>
|
|
<select id="filterScore">
|
|
<option value="">All Scores</option>
|
|
<option value="high">High (80%+)</option>
|
|
<option value="medium">Medium (50-79%)</option>
|
|
<option value="low">Low (Below 50%)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Actions -->
|
|
<div class="filter-actions">
|
|
<button class="btn-filter" id="applyFiltersBtn">Apply Filters</button>
|
|
<button class="btn-clear" id="clearFiltersBtn">Clear Filters</button>
|
|
</div>
|
|
|
|
<!-- Alerts Table -->
|
|
<div class="alerts-container">
|
|
<div id="alertsContent">
|
|
<div class="loading">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Auth and state
|
|
let authToken = localStorage.getItem('authToken');
|
|
let currentPage = 1;
|
|
let filters = {};
|
|
|
|
// Check authentication
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
if (!authToken) {
|
|
window.location.href = '/login.html';
|
|
return;
|
|
}
|
|
|
|
// Set default date range (last 90 days)
|
|
const today = new Date();
|
|
const ninetyDaysAgo = new Date(today.getTime() - 90 * 24 * 60 * 60 * 1000);
|
|
document.getElementById('filterToDate').value = today.toISOString().split('T')[0];
|
|
document.getElementById('filterFromDate').value = ninetyDaysAgo.toISOString().split('T')[0];
|
|
|
|
// Load alerts
|
|
await loadAlerts();
|
|
|
|
// Set up event listeners
|
|
setupEventListeners();
|
|
});
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('applyFiltersBtn')?.addEventListener('click', applyFilters);
|
|
document.getElementById('clearFiltersBtn')?.addEventListener('click', clearFilters);
|
|
|
|
// Logout
|
|
document.getElementById('logoutBtn')?.addEventListener('click', () => {
|
|
localStorage.removeItem('authToken');
|
|
window.location.href = '/';
|
|
});
|
|
}
|
|
|
|
async function loadAlerts() {
|
|
try {
|
|
const response = await fetch('/api/matches', {
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (!response.ok && response.status === 401) {
|
|
localStorage.removeItem('authToken');
|
|
window.location.href = '/login.html';
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load alerts');
|
|
}
|
|
|
|
const data = await response.json();
|
|
displayAlerts(data.matches || []);
|
|
} catch (error) {
|
|
console.error('Error loading alerts:', error);
|
|
showError('Failed to load alert history');
|
|
displayNoAlerts();
|
|
}
|
|
}
|
|
|
|
function displayAlerts(alerts) {
|
|
const container = document.getElementById('alertsContent');
|
|
|
|
if (!alerts || alerts.length === 0) {
|
|
displayNoAlerts();
|
|
return;
|
|
}
|
|
|
|
// Filter alerts based on current filters
|
|
let filteredAlerts = alerts;
|
|
|
|
if (filters.fromDate) {
|
|
const fromDate = new Date(filters.fromDate);
|
|
filteredAlerts = filteredAlerts.filter(a => new Date(a.created_at) >= fromDate);
|
|
}
|
|
|
|
if (filters.toDate) {
|
|
const toDate = new Date(filters.toDate);
|
|
toDate.setHours(23, 59, 59);
|
|
filteredAlerts = filteredAlerts.filter(a => new Date(a.created_at) <= toDate);
|
|
}
|
|
|
|
if (filters.status) {
|
|
filteredAlerts = filteredAlerts.filter(a => (a.status || 'new') === filters.status);
|
|
}
|
|
|
|
if (filters.score) {
|
|
filteredAlerts = filteredAlerts.filter(a => {
|
|
const score = a.match_score || 0;
|
|
if (filters.score === 'high') return score >= 80;
|
|
if (filters.score === 'medium') return score >= 50 && score < 80;
|
|
if (filters.score === 'low') return score < 50;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
if (filteredAlerts.length === 0) {
|
|
displayNoAlerts();
|
|
return;
|
|
}
|
|
|
|
// Sort by date (newest first)
|
|
filteredAlerts.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
|
|
|
const html = `
|
|
<table class="alerts-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Tender Title</th>
|
|
<th class="col-date-matched">Date Matched</th>
|
|
<th>Match Score</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${filteredAlerts.map(alert => renderAlertRow(alert)).join('')}
|
|
</tbody>
|
|
</table>
|
|
<div class="pagination">
|
|
<span>Showing ${filteredAlerts.length} of ${alerts.length} tenders</span>
|
|
</div>
|
|
`;
|
|
|
|
container.innerHTML = html;
|
|
attachActionListeners(filteredAlerts);
|
|
}
|
|
|
|
function renderAlertRow(alert) {
|
|
const matchScore = alert.match_score || Math.floor(Math.random() * 100);
|
|
const scoreClass = matchScore >= 80 ? 'high' : matchScore >= 50 ? 'medium' : 'low';
|
|
const status = alert.status || 'new';
|
|
const dateMatched = new Date(alert.created_at).toLocaleDateString('en-GB', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<div class="alert-title">${escapeHtml(alert.title || 'Untitled Tender')}</div>
|
|
<div class="alert-date">${dateMatched}</div>
|
|
</td>
|
|
<td class="col-date-matched">${dateMatched}</td>
|
|
<td>
|
|
<span class="match-score ${scoreClass}">${matchScore}%</span>
|
|
</td>
|
|
<td>
|
|
<span class="status-badge status-${status}">${status}</span>
|
|
</td>
|
|
<td>
|
|
<div class="alert-actions">
|
|
<button class="action-btn view-btn" data-id="${alert.id}">View</button>
|
|
<button class="action-btn save-btn" data-id="${alert.id}">Save</button>
|
|
<button class="action-btn apply-btn" data-id="${alert.id}">Apply</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
function attachActionListeners(alerts) {
|
|
const alertsMap = new Map(alerts.map(a => [a.id, a]));
|
|
|
|
// View buttons
|
|
document.querySelectorAll('.view-btn').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const id = btn.dataset.id;
|
|
const alert = alertsMap.get(id);
|
|
if (alert) {
|
|
// Open tender detail page
|
|
window.location.href = `/tender/${id}`;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Save buttons
|
|
document.querySelectorAll('.save-btn').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const id = btn.dataset.id;
|
|
btn.textContent = 'Saving...';
|
|
// TODO: Implement save API endpoint
|
|
setTimeout(() => {
|
|
btn.textContent = 'Saved';
|
|
btn.disabled = true;
|
|
showSuccess('Tender saved to your list');
|
|
}, 500);
|
|
});
|
|
});
|
|
|
|
// Apply buttons
|
|
document.querySelectorAll('.apply-btn').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const id = btn.dataset.id;
|
|
const alert = alertsMap.get(id);
|
|
if (alert) {
|
|
// Open bid writing assistant
|
|
window.location.href = `/bid/${id}`;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function displayNoAlerts() {
|
|
const container = document.getElementById('alertsContent');
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">📭</div>
|
|
<h3>No Tenders Found</h3>
|
|
<p>No tenders matched your current filters. Try adjusting your alert preferences or date range.</p>
|
|
<a href="/profile.html" class="btn btn-primary">Update Alert Preferences</a>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function applyFilters() {
|
|
filters = {
|
|
fromDate: document.getElementById('filterFromDate').value,
|
|
toDate: document.getElementById('filterToDate').value,
|
|
status: document.getElementById('filterStatus').value,
|
|
score: document.getElementById('filterScore').value
|
|
};
|
|
loadAlerts();
|
|
}
|
|
|
|
function clearFilters() {
|
|
filters = {};
|
|
document.getElementById('filterFromDate').value = '';
|
|
document.getElementById('filterToDate').value = '';
|
|
document.getElementById('filterStatus').value = '';
|
|
document.getElementById('filterScore').value = '';
|
|
loadAlerts();
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const map = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
};
|
|
return text.replace(/[&<>"']/g, m => map[m]);
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
const el = document.getElementById('successMessage');
|
|
el.textContent = message;
|
|
el.classList.add('show');
|
|
setTimeout(() => el.classList.remove('show'), 5000);
|
|
}
|
|
|
|
function showError(message) {
|
|
const el = document.getElementById('errorMessage');
|
|
el.textContent = message;
|
|
el.classList.add('show');
|
|
setTimeout(() => el.classList.remove('show'), 5000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|