314 lines
12 KiB
HTML
314 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>Renewals & Reminders | AMS</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="css/styles.css">
|
|
</head>
|
|
<body>
|
|
<div class="app-layout">
|
|
|
|
<!-- SIDEBAR -->
|
|
<aside class="sidebar" id="appSidebar"></aside>
|
|
|
|
<!-- MAIN -->
|
|
<div class="main-wrapper">
|
|
<header class="topbar">
|
|
<div class="topbar-left"><div class="topbar-title">Renewals & Expirations <span class="topbar-sub">— V1 Alert Center</span></div></div>
|
|
<div class="topbar-actions">
|
|
<a href="assets.html" class="btn btn-secondary btn-sm">← Back to Assets</a>
|
|
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="content animate-fadein">
|
|
<!-- Stats row -->
|
|
<div class="grid-4 mb-4" id="statsRow">
|
|
<div class="stat-card" style="--sc-color:var(--primary)">
|
|
<div class="stat-icon" style="background:var(--primary-glow);color:var(--primary)">🔑</div>
|
|
<div class="stat-label">Software Expirations</div>
|
|
<div class="stat-value" id="countSoftware">0</div>
|
|
</div>
|
|
<div class="stat-card" style="--sc-color:var(--warning)">
|
|
<div class="stat-icon" style="background:var(--warning-bg);color:var(--warning)">🔌</div>
|
|
<div class="stat-label">AMC Contracts Expiring</div>
|
|
<div class="stat-value" id="countAMC">0</div>
|
|
</div>
|
|
<div class="stat-card" style="--sc-color:var(--info)">
|
|
<div class="stat-icon" style="background:var(--info-bg);color:var(--info)">🛡️</div>
|
|
<div class="stat-label">Warranty Near End</div>
|
|
<div class="stat-value" id="countWarranty">0</div>
|
|
</div>
|
|
<div class="stat-card" style="--sc-color:var(--danger)">
|
|
<div class="stat-icon" style="background:var(--danger-bg);color:var(--danger)">⏰</div>
|
|
<div class="stat-label">Overdue Maintenance</div>
|
|
<div class="stat-value" id="countPM">0</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tab-container">
|
|
<div class="tabs" data-group="renew">
|
|
<button class="tab-btn active" data-tab="tab-software" data-group="renew">🔑 Software Licenses</button>
|
|
<button class="tab-btn" data-tab="tab-amc" data-group="renew">🔌 AMC Agreements</button>
|
|
<button class="tab-btn" data-tab="tab-warranty" data-group="renew">🛡️ Hardware Warranties</button>
|
|
</div>
|
|
|
|
<!-- Software Licenses Tab -->
|
|
<div class="tab-content active" id="tab-software" data-group="renew">
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Software Asset</th>
|
|
<th>Category</th>
|
|
<th>License Key</th>
|
|
<th>Cost (Annual)</th>
|
|
<th>Expiry Date</th>
|
|
<th>Status</th>
|
|
<th>Project</th>
|
|
<th style="width:120px">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="softwareTbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AMC Tab -->
|
|
<div class="tab-content" id="tab-amc" data-group="renew">
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Contract ID</th>
|
|
<th>Vendor</th>
|
|
<th>Scope of Cover</th>
|
|
<th>Annual Value</th>
|
|
<th>Valid From</th>
|
|
<th>Expiry Date</th>
|
|
<th>Status</th>
|
|
<th style="width:120px">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="amcTbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Warranties Tab -->
|
|
<div class="tab-content" id="tab-warranty" data-group="renew">
|
|
<div class="table-wrapper">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Asset Name</th>
|
|
<th>Serial Number</th>
|
|
<th>Category</th>
|
|
<th>Purchase Cost</th>
|
|
<th>Warranty Expiry</th>
|
|
<th>Days Left</th>
|
|
<th>Assignee</th>
|
|
<th style="width:120px">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="warrantyTbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast-container" id="toastContainer"></div>
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<script src="js/data.js"></script>
|
|
<script src="js/sidebar.js"></script>
|
|
<script src="js/app.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
renderRenewals();
|
|
initTabs();
|
|
});
|
|
|
|
function calculateDaysLeft(dateStr) {
|
|
if (!dateStr || dateStr === 'N/A') return Infinity;
|
|
const diff = new Date(dateStr) - new Date();
|
|
return Math.ceil(diff / (1000 * 60 * 60 * 24));
|
|
}
|
|
|
|
function renderRenewals() {
|
|
if (!window.AMS) return;
|
|
|
|
// Filter Software Licenses (Digital assets near renewal)
|
|
const software = AMS.assets.filter(a => a.type === 'digital');
|
|
// Filter AMCs
|
|
const amcs = AMS.amcContracts;
|
|
// Filter Warranties
|
|
const warranties = AMS.assets.filter(a => a.type === 'physical' && a.warranty !== 'N/A');
|
|
// Filter Overdue PMs
|
|
const overduePMs = AMS.pmSchedule.filter(pm => pm.status === 'Overdue');
|
|
|
|
// Set top counter stats
|
|
document.getElementById('countSoftware').textContent = software.length;
|
|
document.getElementById('countAMC').textContent = amcs.length;
|
|
document.getElementById('countWarranty').textContent = warranties.filter(w => calculateDaysLeft(w.warranty) < 180).length;
|
|
document.getElementById('countPM').textContent = overduePMs.length;
|
|
|
|
// Render Software Tbody
|
|
const softTbody = document.getElementById('softwareTbody');
|
|
if (software.length === 0) {
|
|
softTbody.innerHTML = `<tr><td colspan="8"><div class="empty-state"><div class="empty-icon">🔑</div><div class="empty-title">No digital software assets found</div></div></td></tr>`;
|
|
} else {
|
|
softTbody.innerHTML = software.map(s => {
|
|
const days = calculateDaysLeft(s.warranty);
|
|
const isExpiring = days < 60;
|
|
const statusBadge = isExpiring ? 'badge-danger' : 'badge-success';
|
|
const statusText = isExpiring ? `Expiring in ${days}d` : 'Active';
|
|
return `
|
|
<tr>
|
|
<td><div style="font-weight:600">${s.name}</div><div style="font-size:11px;color:var(--text-muted)">${s.id}</div></td>
|
|
<td><span class="badge badge-neutral" style="font-size:10px">${s.cat}</span></td>
|
|
<td><code style="font-size:11px;color:var(--primary-light)">${s.licenseKey || 'N/A'}</code></td>
|
|
<td style="font-weight:600">${fmt(s.cost)}</td>
|
|
<td style="color:${isExpiring ? 'var(--danger)' : 'var(--text-primary)'};font-weight:600">${fmtDate(s.warranty)}</td>
|
|
<td><span class="badge ${statusBadge}">${statusText}</span></td>
|
|
<td><span class="badge badge-primary" style="font-size:10.5px">📁 ${s.project}</span></td>
|
|
<td>
|
|
<button class="btn btn-primary btn-sm" onclick="renewSoftware('${s.id}')">🔄 Renew</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Render AMC Tbody
|
|
const amcTbody = document.getElementById('amcTbody');
|
|
if (amcs.length === 0) {
|
|
amcTbody.innerHTML = `<tr><td colspan="8"><div class="empty-state"><div class="empty-icon">🔌</div><div class="empty-title">No active AMC contracts</div></div></td></tr>`;
|
|
} else {
|
|
amcTbody.innerHTML = amcs.map(c => {
|
|
const days = calculateDaysLeft(c.end);
|
|
const isExpiring = days < 60;
|
|
const statusBadge = isExpiring ? 'badge-danger' : 'badge-success';
|
|
const statusText = isExpiring ? `Expiring in ${days}d` : 'Active';
|
|
return `
|
|
<tr>
|
|
<td><code style="font-size:11px;color:var(--primary-light)">${c.id}</code></td>
|
|
<td style="font-weight:600">${c.vendor}</td>
|
|
<td style="font-size:12px;color:var(--text-secondary)">${c.scope}</td>
|
|
<td style="font-weight:600">${fmt(c.value)}</td>
|
|
<td>${fmtDate(c.start)}</td>
|
|
<td style="color:${isExpiring ? 'var(--danger)' : 'var(--text-primary)'};font-weight:600">${fmtDate(c.end)}</td>
|
|
<td><span class="badge ${statusBadge}">${statusText}</span></td>
|
|
<td>
|
|
<button class="btn btn-warning btn-sm" onclick="renewAMC('${c.id}')" style="color:#000">🔄 Renew</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// Render Warranties Tbody
|
|
const warrTbody = document.getElementById('warrantyTbody');
|
|
const criticalWarranties = warranties.filter(w => calculateDaysLeft(w.warranty) < 365); // showing those expiring in a year
|
|
if (criticalWarranties.length === 0) {
|
|
warrTbody.innerHTML = `<tr><td colspan="8"><div class="empty-state"><div class="empty-icon">🛡️</div><div class="empty-title">No physical assets near warranty expiry</div></div></td></tr>`;
|
|
} else {
|
|
warrTbody.innerHTML = criticalWarranties.map(w => {
|
|
const days = calculateDaysLeft(w.warranty);
|
|
const isCritical = days < 90;
|
|
const color = isCritical ? 'var(--danger)' : 'var(--text-secondary)';
|
|
return `
|
|
<tr>
|
|
<td><div style="font-weight:600">${w.name}</div><div style="font-size:11px;color:var(--text-muted)">${w.id}</div></td>
|
|
<td><code style="font-size:11px">${w.serial}</code></td>
|
|
<td><span class="badge badge-neutral" style="font-size:10px">${w.cat}</span></td>
|
|
<td style="font-weight:600">${fmt(w.cost)}</td>
|
|
<td style="color:${color};font-weight:600">${fmtDate(w.warranty)}</td>
|
|
<td style="color:${color};font-weight:600">${days <= 0 ? 'Expired' : `${days} days`}</td>
|
|
<td>${w.assignee}</td>
|
|
<td>
|
|
<button class="btn btn-secondary btn-sm" onclick="extendWarranty('${w.id}')">🛡️ Extend</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
}
|
|
|
|
function renewSoftware(id) {
|
|
const s = AMS.assets.find(x => x.id === id);
|
|
if (s) {
|
|
const oldW = new Date(s.warranty);
|
|
oldW.setFullYear(oldW.getFullYear() + 1);
|
|
s.warranty = oldW.toISOString().split('T')[0];
|
|
|
|
AMS.activityFeed.unshift({
|
|
user: AMS.currentUser.name,
|
|
av: AMS.currentUser.avatar,
|
|
color: '#A855F7',
|
|
action: 'renewed software license',
|
|
target: s.name,
|
|
detail: `by +1 year (new renewal: ${fmtDate(s.warranty)})`,
|
|
time: 'Just now'
|
|
});
|
|
|
|
AMS.save();
|
|
showToast('License Renewed', `${s.name} extended by 1 year.`, 'success');
|
|
renderRenewals();
|
|
}
|
|
}
|
|
|
|
function renewAMC(id) {
|
|
const c = AMS.amcContracts.find(x => x.id === id);
|
|
if (c) {
|
|
const oldE = new Date(c.end);
|
|
oldE.setFullYear(oldE.getFullYear() + 1);
|
|
c.end = oldE.toISOString().split('T')[0];
|
|
c.status = 'Active';
|
|
|
|
AMS.activityFeed.unshift({
|
|
user: AMS.currentUser.name,
|
|
av: AMS.currentUser.avatar,
|
|
color: '#F59E0B',
|
|
action: 'renewed AMC contract',
|
|
target: c.vendor,
|
|
detail: `for ${c.scope} (+1 year)`,
|
|
time: 'Just now'
|
|
});
|
|
|
|
AMS.save();
|
|
showToast('Contract Renewed', `AMC with ${c.vendor} extended by 1 year.`, 'success');
|
|
renderRenewals();
|
|
}
|
|
}
|
|
|
|
function extendWarranty(id) {
|
|
const w = AMS.assets.find(x => x.id === id);
|
|
if (w) {
|
|
const oldW = new Date(w.warranty);
|
|
oldW.setFullYear(oldW.getFullYear() + 1);
|
|
w.warranty = oldW.toISOString().split('T')[0];
|
|
|
|
AMS.activityFeed.unshift({
|
|
user: AMS.currentUser.name,
|
|
av: AMS.currentUser.avatar,
|
|
color: '#10B981',
|
|
action: 'extended hardware warranty',
|
|
target: w.name,
|
|
detail: `by +1 year (new expiry: ${fmtDate(w.warranty)})`,
|
|
time: 'Just now'
|
|
});
|
|
|
|
AMS.save();
|
|
showToast('Warranty Extended', `${w.name} warranty extended by 1 year.`, 'success');
|
|
renderRenewals();
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|