marco.asseto.prototype/maintenance.html

421 lines
22 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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" id="appSidebar"></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" title="Logout"><i data-lucide="log-out"></i></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 &amp; 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)"><i data-lucide="search" style="width:14px;height:14px"></i></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></select>
</div>
<div class="form-group"><label class="form-label">Category <span class="req">*</span></label>
<select class="form-select" id="ticketCategory"><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" id="ticketTitle" 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" id="ticketPriority">
<option value="Critical">Critical System down, data loss risk</option>
<option value="High">High Core workflow broken</option>
<option value="Medium" selected>Medium Significant but workaround exists</option>
<option value="Low">Low Cosmetic / minor issue</option>
</select>
</div>
<div class="form-group"><label class="form-label">Assign To</label>
<select class="form-select" id="ticketAssignee"><option>Kavya Nair</option><option>External Vendor</option><option>Dell Support</option><option>Daikin AMC Team</option><option>Ricoh Service</option><option>Unassigned</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" id="pmAsset"><option value="AST-2025-005">Daikin 1.5T Split AC (AST-2025-005)</option><option value="AST-2025-004">Dell PowerEdge R740 (AST-2025-004)</option><option value="AST-2025-014">Honda City (AST-2025-014)</option><option value="AST-2025-012">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" id="pmFreq"><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" id="pmAssignee" 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" id="pmDate"></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" id="amcVendor"><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" id="amcValue" placeholder="Annual value"></div>
</div>
<div class="form-group"><label class="form-label">Scope of Work</label><textarea class="form-textarea" id="amcScope" 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" id="amcStart"></div>
<div class="form-group"><label class="form-label">Valid To</label><input type="date" class="form-input" id="amcEnd"></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="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', () => {
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">${escapeHtml(t.title || t.name)}</div>
<div style="font-size:11px;color:var(--text-muted);margin-bottom:8px">${escapeHtml(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)">${escapeHtml(t.id)}</code></td>
<td><div style="font-weight:600;color:var(--text-primary)">${escapeHtml(t.title||t.name)}</div><div style="font-size:11px;color:var(--text-muted)">${escapeHtml(t.category)}</div></td>
<td><div style="font-size:12.5px">${escapeHtml(t.assetName)}</div><div style="font-size:11px;color:var(--text-muted)">${escapeHtml(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)];
AMS.save();
renderStats(); 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';
AMS.save();
renderStats(); renderKanban(); renderTicketList();
showToast('Ticket Resolved',`${id} marked as resolved`,'success');
}
function todayStr() { return new Date().toISOString().slice(0,10); }
function markPMDone(id) {
const pm = AMS.pmSchedule.find(x=>x.id===id);
if(pm) { pm.lastDone=todayStr(); pm.status='Upcoming'; }
AMS.save();
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';
AMS.save();
renderAMC();
showToast('AMC Renewed','Contract renewed for another year','success');
}
function nextSeqId(arr, prefix) {
const n = arr.reduce((m,x)=>{ const v=parseInt(String(x.id).split('-').pop(),10); return isNaN(v)?m:Math.max(m,v); },0)+1;
return `${prefix}-${String(n).padStart(3,'0')}`;
}
function saveTicket() {
const title = document.getElementById('ticketTitle').value.trim();
const assetId = document.getElementById('ticketAsset').value;
if(!title || !assetId) { showToast('Missing Details','Asset and issue title are required','error'); return; }
const asset = AMS.assets.find(a=>a.id===assetId);
AMS.tickets.unshift({
id: nextSeqId(AMS.tickets,'TKT'),
title,
asset: assetId,
assetName: asset ? asset.name : assetId,
priority: document.getElementById('ticketPriority').value,
status: 'Open',
assignedTo: document.getElementById('ticketAssignee').value,
created: todayStr(),
dept: asset ? asset.dept : '',
category: document.getElementById('ticketCategory').value
});
AMS.save();
renderStats(); renderKanban(); renderTicketList();
closeModal('ticketModal');
document.getElementById('ticketTitle').value = '';
showToast('Ticket Created','Assigned to technician, email notification sent','success');
}
function savePM() {
const assetId = document.getElementById('pmAsset').value;
const date = document.getElementById('pmDate').value;
if(!date) { showToast('Missing Details','First PM date is required','error'); return; }
const asset = AMS.assets.find(a=>a.id===assetId);
AMS.pmSchedule.push({
id: nextSeqId(AMS.pmSchedule,'PM'),
asset: asset ? asset.name : assetId,
assetId,
freq: document.getElementById('pmFreq').value,
lastDone: '—',
nextDue: date,
status: 'Upcoming',
assignee: document.getElementById('pmAssignee').value.trim() || 'Unassigned'
});
AMS.save();
renderPMSchedule();
closeModal('pmModal');
showToast('PM Scheduled','Preventive maintenance schedule created','success');
}
function saveAMC() {
const value = parseInt(document.getElementById('amcValue').value,10);
if(!value) { showToast('Missing Details','Contract value is required','error'); return; }
AMS.amcContracts.push({
id: nextSeqId(AMS.amcContracts,'AMC'),
vendor: document.getElementById('amcVendor').value,
scope: document.getElementById('amcScope').value.trim(),
value,
start: document.getElementById('amcStart').value || todayStr(),
end: document.getElementById('amcEnd').value || '',
status: 'Active',
nextService: document.getElementById('amcStart').value || todayStr()
});
AMS.save();
renderAMC();
closeModal('amcModal');
showToast('AMC Added','Contract added and team notified','success');
}
</script>
</body>
</html>