- Login page with animated floating stat cards - Executive dashboard with Chart.js KPIs and activity feed - Full asset registry (list, search, filter, bulk actions, QR) - Asset detail page with 6 tabs (financial, maintenance, history…) - 3-step asset creation wizard with category-specific fields - Inventory: stock overview, GRN, location tree, physical audit - Procurement: PR → approval → PO → GRN → asset lifecycle - Maintenance: Kanban board, PM schedule, AMC contracts - Reports: depreciation schedule, utilization, compliance gauge - User management: roles, permission matrix, session control - Settings: 8 config sections (org, depreciation, security, integrations) - Premium dark UI with CSS variables, Chart.js 4, toast system
369 lines
20 KiB
HTML
369 lines
20 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||
<title>Maintenance | 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">
|
||
<aside class="sidebar">
|
||
<div class="sidebar-logo"><div class="logo-icon">📦</div><div><div class="logo-title">AMS</div><div class="logo-sub">Asset Management</div></div></div>
|
||
<nav class="sidebar-nav">
|
||
<div class="nav-section-label">Overview</div>
|
||
<a href="dashboard.html" class="nav-item"><span class="nav-icon">📊</span> Dashboard</a>
|
||
<div class="nav-section-label">Assets</div>
|
||
<a href="assets.html" class="nav-item"><span class="nav-icon">📦</span> All Assets</a>
|
||
<a href="asset-create.html" class="nav-item"><span class="nav-icon">➕</span> Add Asset</a>
|
||
<div class="nav-section-label">Supply Chain</div>
|
||
<a href="inventory.html" class="nav-item"><span class="nav-icon">🏪</span> Inventory</a>
|
||
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement</a>
|
||
<div class="nav-section-label">Operations</div>
|
||
<a href="maintenance.html" class="nav-item active"><span class="nav-icon">🔧</span> Maintenance</a>
|
||
<a href="reports.html" class="nav-item"><span class="nav-icon">📈</span> Reports</a>
|
||
<div class="nav-section-label">Administration</div>
|
||
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users</a>
|
||
<a href="settings.html" class="nav-item"><span class="nav-icon">⚙️</span> Settings</a>
|
||
</nav>
|
||
<div class="sidebar-footer"><div class="user-card"><div class="user-av">AS</div><div style="flex:1;min-width:0"><div class="user-name">Arjun Sharma</div><div class="user-role">Asset Manager</div></div></div></div>
|
||
</aside>
|
||
|
||
<div class="main-wrapper">
|
||
<header class="topbar">
|
||
<div class="topbar-left"><div class="topbar-title">Maintenance Management</div></div>
|
||
<div class="topbar-actions">
|
||
<button class="btn btn-primary btn-sm" onclick="openModal('ticketModal')">+ Raise Ticket</button>
|
||
<a href="index.html" class="icon-btn">🚪</a>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="content">
|
||
<!-- Stats -->
|
||
<div class="grid-4 mb-4" id="maintStats"></div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="tab-container">
|
||
<div class="tabs" data-group="maint">
|
||
<button class="tab-btn active" data-tab="tab-kanban" data-group="maint">📋 Kanban Board</button>
|
||
<button class="tab-btn" data-tab="tab-list" data-group="maint">📄 All Tickets</button>
|
||
<button class="tab-btn" data-tab="tab-pm" data-group="maint">🗓️ PM Schedule <span class="badge badge-warning" style="margin-left:4px;font-size:9px">2 overdue</span></button>
|
||
<button class="tab-btn" data-tab="tab-amc" data-group="maint">📋 AMC & Warranty</button>
|
||
</div>
|
||
|
||
<!-- Kanban -->
|
||
<div class="tab-content active" id="tab-kanban" data-group="maint">
|
||
<div class="kanban-board" id="kanbanBoard"></div>
|
||
</div>
|
||
|
||
<!-- All Tickets List -->
|
||
<div class="tab-content" id="tab-list" data-group="maint">
|
||
<div class="filters-row mb-3">
|
||
<div class="search-wrap" style="max-width:280px"><span style="color:var(--text-muted)">🔍</span><input type="text" id="ticketSearch" placeholder="Search tickets…"></div>
|
||
<select class="filter-sel" id="ticketPriorityFilter"><option value="">All Priorities</option><option>Critical</option><option>High</option><option>Medium</option><option>Low</option></select>
|
||
<select class="filter-sel" id="ticketStatusFilter"><option value="">All Statuses</option><option>Open</option><option>In Progress</option><option>Pending Parts</option><option>Resolved</option></select>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table class="data-table">
|
||
<thead><tr><th>Ticket ID</th><th>Issue</th><th>Asset</th><th>Priority</th><th>Status</th><th>Dept</th><th>Assigned To</th><th>Created</th><th>Actions</th></tr></thead>
|
||
<tbody id="ticketTbody"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PM Schedule -->
|
||
<div class="tab-content" id="tab-pm" data-group="maint">
|
||
<div class="alert alert-warning mb-4">
|
||
<div class="alert-icon">⚠️</div>
|
||
<div><div class="alert-title">2 Preventive Maintenance tasks overdue!</div>
|
||
<div class="alert-text">Daikin AC (overdue 13 days) and Ricoh Photocopier (overdue 38 days). Schedule service immediately.</div></div>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table class="data-table">
|
||
<thead><tr><th>PM ID</th><th>Asset</th><th>Frequency</th><th>Last Done</th><th>Next Due</th><th>Status</th><th>Assigned To</th><th>Actions</th></tr></thead>
|
||
<tbody id="pmTbody"></tbody>
|
||
</table>
|
||
</div>
|
||
<div style="margin-top:16px">
|
||
<button class="btn btn-primary" onclick="openModal('pmModal')">+ Schedule Preventive Maintenance</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AMC Tab -->
|
||
<div class="tab-content" id="tab-amc" data-group="maint">
|
||
<div class="alert alert-warning mb-4">
|
||
<div class="alert-icon">⏰</div>
|
||
<div><div class="alert-title">1 AMC contract expiring within 30 days</div><div class="alert-text">Cisco Network Infrastructure AMC expires June 30, 2025. Renewal must be initiated immediately.</div></div>
|
||
<button class="btn btn-warning btn-sm" style="margin-left:auto;flex-shrink:0;color:var(--warning);background:var(--warning-bg);border-color:rgba(245,158,11,.2)" onclick="openModal('amcModal')">Renew Now →</button>
|
||
</div>
|
||
<div class="table-wrapper">
|
||
<table class="data-table">
|
||
<thead><tr><th>Contract ID</th><th>Vendor</th><th>Scope</th><th>Value</th><th>Valid From</th><th>Valid To</th><th>Status</th><th>Next Service</th><th>Actions</th></tr></thead>
|
||
<tbody id="amcTbody"></tbody>
|
||
</table>
|
||
</div>
|
||
<div style="margin-top:14px">
|
||
<button class="btn btn-primary" onclick="openModal('amcModal')">+ Add AMC Contract</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ticket Modal -->
|
||
<div class="modal-overlay" id="ticketModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header"><span class="modal-title">Raise Maintenance Ticket</span><button class="modal-close">✕</button></div>
|
||
<div class="modal-body">
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">Asset <span class="req">*</span></label>
|
||
<select class="form-select" id="ticketAsset"><option value="">Search asset…</option>
|
||
${window.AMS ? '' : ''}
|
||
</select>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">Category <span class="req">*</span></label>
|
||
<select class="form-select"><option>Hardware Failure</option><option>Software Issue</option><option>Performance Issue</option><option>Electrical Issue</option><option>Mechanical Issue</option><option>Network Issue</option><option>Consumable Replacement</option><option>Preventive Check</option></select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">Issue Title <span class="req">*</span></label><input class="form-input" placeholder="Brief description of the issue"></div>
|
||
<div class="form-group"><label class="form-label">Detailed Description</label><textarea class="form-textarea" rows="3" placeholder="Steps to reproduce, error messages, symptoms…"></textarea></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">Priority <span class="req">*</span></label>
|
||
<select class="form-select">
|
||
<option>Critical – System down, data loss risk</option>
|
||
<option>High – Core workflow broken</option>
|
||
<option selected>Medium – Significant but workaround exists</option>
|
||
<option>Low – Cosmetic / minor issue</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">Assign To</label>
|
||
<select class="form-select"><option>Kavya Nair (IT)</option><option>External Vendor</option><option>Dell Support</option><option>Daikin AMC Team</option><option>Ricoh Service</option><option>Auto-assign</option></select>
|
||
</div>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">Attachments (photos, logs)</label><input type="file" class="form-input" multiple accept=".pdf,.jpg,.png,.log"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-ghost" onclick="closeModal('ticketModal')">Cancel</button>
|
||
<button class="btn btn-primary" onclick="saveTicket()">Create Ticket</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PM Modal -->
|
||
<div class="modal-overlay" id="pmModal">
|
||
<div class="modal">
|
||
<div class="modal-header"><span class="modal-title">Schedule PM</span><button class="modal-close">✕</button></div>
|
||
<div class="modal-body">
|
||
<div class="form-group"><label class="form-label">Asset <span class="req">*</span></label><select class="form-select"><option>Daikin 1.5T Split AC (AST-2025-005)</option><option>Dell PowerEdge R740 (AST-2025-004)</option><option>Honda City (AST-2025-014)</option><option>APC Smart UPS 3KVA (AST-2025-012)</option></select></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">Frequency</label><select class="form-select"><option>Monthly</option><option>Quarterly</option><option>Half-Yearly</option><option>Yearly</option></select></div>
|
||
<div class="form-group"><label class="form-label">Assigned To</label><input class="form-input" placeholder="Technician or vendor"></div>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">First PM Date <span class="req">*</span></label><input type="date" class="form-input"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-ghost" onclick="closeModal('pmModal')">Cancel</button>
|
||
<button class="btn btn-primary" onclick="savePM()">Schedule PM</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- AMC Modal -->
|
||
<div class="modal-overlay" id="amcModal">
|
||
<div class="modal modal-lg">
|
||
<div class="modal-header"><span class="modal-title">AMC Contract</span><button class="modal-close">✕</button></div>
|
||
<div class="modal-body">
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">Vendor <span class="req">*</span></label><select class="form-select"><option>Daikin Aircon India</option><option>Dell India Pvt. Ltd.</option><option>Cisco Systems India</option><option>Godrej Security</option></select></div>
|
||
<div class="form-group"><label class="form-label">Contract Value (₹) <span class="req">*</span></label><input type="number" class="form-input" placeholder="Annual value"></div>
|
||
</div>
|
||
<div class="form-group"><label class="form-label">Scope of Work</label><textarea class="form-textarea" rows="2" placeholder="What assets/services are covered"></textarea></div>
|
||
<div class="form-row">
|
||
<div class="form-group"><label class="form-label">Valid From</label><input type="date" class="form-input"></div>
|
||
<div class="form-group"><label class="form-label">Valid To</label><input type="date" class="form-input"></div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-ghost" onclick="closeModal('amcModal')">Cancel</button>
|
||
<button class="btn btn-primary" onclick="saveAMC()">Save Contract</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
<script src="js/data.js"></script>
|
||
<script src="js/app.js"></script>
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
renderStats();
|
||
renderKanban();
|
||
renderTicketList();
|
||
renderPMSchedule();
|
||
renderAMC();
|
||
initTabs();
|
||
|
||
// Populate asset select in ticket modal
|
||
const sel = document.getElementById('ticketAsset');
|
||
AMS.assets.forEach(a => { const o=document.createElement('option'); o.value=a.id; o.textContent=`${a.name} (${a.id})`; sel.appendChild(o); });
|
||
document.getElementById('ticketSearch').addEventListener('input', filterTickets);
|
||
document.getElementById('ticketPriorityFilter').addEventListener('change', filterTickets);
|
||
document.getElementById('ticketStatusFilter').addEventListener('change', filterTickets);
|
||
});
|
||
|
||
function renderStats() {
|
||
const t = AMS.tickets;
|
||
document.getElementById('maintStats').innerHTML = [
|
||
{ label:'Open Tickets', icon:'🔴', val:t.filter(x=>x.status==='Open').length, color:'var(--danger)' },
|
||
{ label:'In Progress', icon:'🔵', val:t.filter(x=>x.status==='In Progress').length, color:'var(--info)' },
|
||
{ label:'Pending Parts', icon:'⏳', val:t.filter(x=>x.status==='Pending Parts').length, color:'var(--warning)' },
|
||
{ label:'Resolved (30d)', icon:'✅', val:t.filter(x=>x.status==='Resolved').length, color:'var(--success)' }
|
||
].map(s=>`<div class="stat-card" style="--sc-color:${s.color}"><div class="stat-icon" style="background:${s.color}18;color:${s.color}">${s.icon}</div><div class="stat-label">${s.label}</div><div class="stat-value" style="color:${s.color}">${s.val}</div></div>`).join('');
|
||
}
|
||
|
||
const kanbanCols = [
|
||
{ id:'Open', title:'Open', color:'var(--danger)', count:0 },
|
||
{ id:'In Progress', title:'In Progress', color:'var(--info)', count:0 },
|
||
{ id:'Pending Parts', title:'Pending Parts', color:'var(--warning)', count:0 },
|
||
{ id:'Resolved', title:'Resolved', color:'var(--success)', count:0 },
|
||
{ id:'Closed', title:'Closed', color:'var(--text-muted)', count:0 }
|
||
];
|
||
|
||
function renderKanban() {
|
||
const board = document.getElementById('kanbanBoard');
|
||
board.innerHTML = kanbanCols.map(col => {
|
||
const colTickets = AMS.tickets.filter(t => t.status === col.id);
|
||
const cards = colTickets.map(t => `
|
||
<div class="kanban-card" onclick="openTicketDetail('${t.id}')">
|
||
<div class="kanban-card-title">${t.title || t.name}</div>
|
||
<div style="font-size:11px;color:var(--text-muted);margin-bottom:8px">${t.assetName}</div>
|
||
<div class="kanban-meta">
|
||
<span class="badge ${statusBadge(t.priority)}" style="font-size:9px">${t.priority}</span>
|
||
<span style="font-size:10.5px;color:var(--text-muted)">${t.assignedTo}</span>
|
||
</div>
|
||
<div style="font-size:10px;color:var(--text-muted);margin-top:6px">🗓️ ${fmtDate(t.created)}</div>
|
||
</div>`).join('') || `<div class="empty-state" style="padding:20px"><div class="empty-icon" style="font-size:28px">✅</div><div style="font-size:12px;color:var(--text-muted)">No tickets</div></div>`;
|
||
return `
|
||
<div class="kanban-col">
|
||
<div class="kanban-col-header">
|
||
<span class="kanban-col-title"><span style="width:8px;height:8px;border-radius:50%;background:${col.color};display:inline-block;flex-shrink:0"></span>${col.title}</span>
|
||
<span class="kanban-count">${colTickets.length}</span>
|
||
</div>
|
||
<div class="kanban-cards">${cards}</div>
|
||
${col.id==='Open'?`<div style="padding:8px 10px 10px"><button class="btn btn-ghost btn-sm w-full" onclick="openModal('ticketModal')">+ Add Ticket</button></div>`:''}
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderTicketList(data) {
|
||
const tickets = data || AMS.tickets;
|
||
document.getElementById('ticketTbody').innerHTML = tickets.map(t => `
|
||
<tr onclick="openTicketDetail('${t.id}')">
|
||
<td><code style="font-size:11.5px;color:var(--primary-light)">${t.id}</code></td>
|
||
<td><div style="font-weight:600;color:var(--text-primary)">${t.title||t.name}</div><div style="font-size:11px;color:var(--text-muted)">${t.category}</div></td>
|
||
<td><div style="font-size:12.5px">${t.assetName}</div><div style="font-size:11px;color:var(--text-muted)">${t.asset}</div></td>
|
||
<td><span class="badge ${statusBadge(t.priority)}">${t.priority}</span></td>
|
||
<td><span class="badge ${statusBadge(t.status)}">${t.status}</span></td>
|
||
<td>${t.dept}</td>
|
||
<td>${t.assignedTo}</td>
|
||
<td>${fmtDate(t.created)}</td>
|
||
<td onclick="event.stopPropagation()">
|
||
<div class="flex gap-1">
|
||
<button class="btn btn-ghost btn-sm" onclick="updateStatus('${t.id}')">Update</button>
|
||
<button class="btn btn-ghost btn-sm" onclick="resolveTicket('${t.id}')">✓ Resolve</button>
|
||
</div>
|
||
</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
function filterTickets() {
|
||
const q = document.getElementById('ticketSearch').value.toLowerCase();
|
||
const pr = document.getElementById('ticketPriorityFilter').value;
|
||
const st = document.getElementById('ticketStatusFilter').value;
|
||
const filtered = AMS.tickets.filter(t =>
|
||
(!q || (t.title||t.name||'').toLowerCase().includes(q) || t.assetName.toLowerCase().includes(q)) &&
|
||
(!pr || t.priority === pr) &&
|
||
(!st || t.status === st)
|
||
);
|
||
renderTicketList(filtered);
|
||
}
|
||
|
||
function renderPMSchedule() {
|
||
document.getElementById('pmTbody').innerHTML = AMS.pmSchedule.map(pm => `
|
||
<tr>
|
||
<td><code style="font-size:11px;color:var(--primary-light)">${pm.id}</code></td>
|
||
<td><div style="font-weight:600">${pm.asset}</div><div style="font-size:11px;color:var(--text-muted)">${pm.assetId}</div></td>
|
||
<td>${pm.freq}</td>
|
||
<td>${fmtDate(pm.lastDone)}</td>
|
||
<td style="font-weight:600;color:${pm.status==='Overdue'?'var(--danger)':pm.status==='Due Today'?'var(--warning)':'var(--text-primary)'}">${fmtDate(pm.nextDue)}</td>
|
||
<td><span class="badge ${statusBadge(pm.status)}">${pm.status}</span></td>
|
||
<td>${pm.assignee}</td>
|
||
<td>
|
||
<button class="btn btn-secondary btn-sm" onclick="markPMDone('${pm.id}')">✓ Mark Done</button>
|
||
<button class="btn btn-ghost btn-sm" onclick="showToast('Reschedule','PM rescheduling dialog','info')">Reschedule</button>
|
||
</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
function renderAMC() {
|
||
document.getElementById('amcTbody').innerHTML = AMS.amcContracts.map(c => `
|
||
<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;max-width:200px;color:var(--text-secondary)">${c.scope}</td>
|
||
<td style="font-weight:700">${fmt(c.value)}</td>
|
||
<td>${fmtDate(c.start)}</td>
|
||
<td style="font-weight:600;color:${c.status==='Expiring'?'var(--warning)':'var(--text-primary)'}">${fmtDate(c.end)}</td>
|
||
<td><span class="badge ${c.status==='Expiring'?'badge-warning':'badge-success'}">${c.status}</span></td>
|
||
<td>${fmtDate(c.nextService)}</td>
|
||
<td>
|
||
${c.status==='Expiring'?`<button class="btn btn-warning btn-sm" style="color:var(--warning);background:var(--warning-bg)" onclick="renewAMC('${c.id}')">Renew</button>`:
|
||
`<button class="btn btn-ghost btn-sm" onclick="showToast('View','Opening AMC document','info')">View</button>`}
|
||
</td>
|
||
</tr>`).join('');
|
||
}
|
||
|
||
function openTicketDetail(id) {
|
||
const t = AMS.tickets.find(x => x.id === id);
|
||
if (!t) return;
|
||
showToast(t.id, `${t.title||t.name} — ${t.status}`, 'info');
|
||
}
|
||
|
||
function updateStatus(id) {
|
||
const t = AMS.tickets.find(x=>x.id===id);
|
||
if (!t) return;
|
||
const statuses = ['Open','In Progress','Pending Parts','Resolved','Closed'];
|
||
const curr = statuses.indexOf(t.status);
|
||
t.status = statuses[Math.min(curr+1, statuses.length-1)];
|
||
renderKanban(); renderTicketList();
|
||
showToast('Status Updated',`${id} → ${t.status}`,'success');
|
||
}
|
||
|
||
function resolveTicket(id) {
|
||
const t = AMS.tickets.find(x=>x.id===id);
|
||
if(t) t.status='Resolved';
|
||
renderKanban(); renderTicketList();
|
||
showToast('Ticket Resolved',`${id} marked as resolved`,'success');
|
||
}
|
||
|
||
function markPMDone(id) {
|
||
const pm = AMS.pmSchedule.find(x=>x.id===id);
|
||
if(pm) { pm.lastDone='2025-05-28'; pm.status='Upcoming'; }
|
||
renderPMSchedule();
|
||
showToast('PM Completed',`${id} marked as done. Next due auto-calculated.`,'success');
|
||
}
|
||
|
||
function renewAMC(id) {
|
||
const c = AMS.amcContracts.find(x=>x.id===id);
|
||
if(c) c.status='Active';
|
||
renderAMC();
|
||
showToast('AMC Renewed','Contract renewed for another year','success');
|
||
}
|
||
|
||
function saveTicket() { closeModal('ticketModal'); showToast('Ticket Created','Assigned to technician, email notification sent','success'); }
|
||
function savePM() { closeModal('pmModal'); showToast('PM Scheduled','Preventive maintenance schedule created','success'); }
|
||
function saveAMC() { closeModal('amcModal'); showToast('AMC Added','Contract added and team notified','success'); }
|
||
</script>
|
||
</body>
|
||
</html>
|