chore: update project dependencies and internal vendor libraries

This commit is contained in:
Vaibhav Surve 2026-06-05 16:56:54 +05:30
parent c35b02391e
commit 7bab488ce8
18 changed files with 4068 additions and 1650 deletions

Binary file not shown.

View File

@ -9,26 +9,7 @@
<body>
<div class="app-layout">
<!-- SIDEBAR -->
<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 active"><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"><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>
<aside class="sidebar" id="appSidebar"></aside>
<!-- MAIN -->
<div class="main-wrapper">
@ -39,7 +20,7 @@
</div>
<div class="topbar-actions">
<button class="btn btn-secondary btn-sm" onclick="saveDraft()">💾 Save Draft</button>
<a href="index.html" class="icon-btn">🚪</a>
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
</div>
</header>
@ -64,10 +45,20 @@
<div class="card-header"><span class="card-title">📋 Step 1: Basic Information</span></div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<label class="form-label">Asset Type <span class="req">*</span></label>
<select class="form-select" id="assetType" onchange="handleTypeChange()">
<option value="physical">Physical Asset (Hardware, Furniture, Vehicle, etc.)</option>
<option value="digital">Digital Asset (Software License, Cloud Subscription)</option>
<option value="other">Other Asset (Lease, Agreement, Patent, etc.)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Asset Name <span class="req">*</span></label>
<input type="text" class="form-input" id="assetName" placeholder="e.g. Dell Latitude 5540 Laptop" oninput="updatePreview()">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Category <span class="req">*</span></label>
<select class="form-select" id="assetCategory" onchange="renderCustomFields()">
@ -78,8 +69,16 @@
<option>Vehicles</option><option>HVAC</option><option>Power Equipment</option><option>Storage</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Status <span class="req">*</span></label>
<select class="form-select" id="assetStatus">
<option>Active</option><option>Idle</option><option>Under Maintenance</option>
</select>
</div>
</div>
<div class="form-row">
<!-- Physical Hardware Fields -->
<div class="form-row" id="physicalFieldsRow">
<div class="form-group">
<label class="form-label">Serial Number</label>
<input type="text" class="form-input" id="serialNo" placeholder="Manufacturer serial number">
@ -89,22 +88,37 @@
<input type="text" class="form-input" id="modelNo" placeholder="Model identifier">
</div>
</div>
<!-- Digital Software Fields -->
<div class="form-row" id="digitalFieldsRow" style="display:none">
<div class="form-group" id="licenseKeyGroup">
<label class="form-label">License Key / Subscription ID</label>
<input type="text" class="form-input" id="licenseKey" placeholder="e.g. MS365-ABCD-1234">
</div>
<div class="form-group" id="seatsGroup">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div>
<label class="form-label">Total Seats</label>
<input type="number" class="form-input" id="seatsTotal" value="1" min="1">
</div>
<div>
<label class="form-label">Allocated Seats</label>
<input type="number" class="form-input" id="seatsAlloc" value="0" min="0">
</div>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<div class="form-group" id="brandGroup">
<label class="form-label">Brand / Manufacturer</label>
<input type="text" class="form-input" id="brand" placeholder="e.g. Dell, HP, Apple">
</div>
<div class="form-group">
<label class="form-label">Status <span class="req">*</span></label>
<select class="form-select" id="assetStatus">
<option>Active</option><option>Idle</option><option>Under Maintenance</option>
</select>
<label class="form-label">Description</label>
<input type="text" class="form-input" id="description" placeholder="Optional description or notes…">
</div>
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea class="form-textarea" placeholder="Optional description or notes about this asset…" rows="2"></textarea>
</div>
<!-- Custom Fields -->
<div id="customFieldsSection" style="display:none">
@ -209,7 +223,7 @@
</div>
</div>
<div class="form-row">
<div class="form-group">
<div class="form-group" id="assigneeGroup">
<label class="form-label">Assign To (Employee)</label>
<select class="form-select" id="assetAssignee">
<option value="">Unassigned (Pool Asset)</option>
@ -218,9 +232,15 @@
<option>Sneha Patel Marketing</option><option>Deepak Joshi Admin</option>
</select>
</div>
<div class="form-group" id="projectGroup" style="display:none">
<label class="form-label">Associated Project</label>
<select class="form-select" id="assetProject">
<option value="">Unassigned / No Project</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Assignment Date</label>
<input type="date" class="form-input" value="2025-05-28">
<input type="date" class="form-input" value="2026-06-04">
</div>
</div>
<div class="form-group">
@ -284,7 +304,9 @@
</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>
const steps = [
@ -308,6 +330,29 @@ const customFieldDefs = {
document.addEventListener('DOMContentLoaded', () => {
renderStepNav();
// Populate projects select dynamically from AMS
const projSel = document.getElementById('assetProject');
if (projSel && window.AMS && AMS.projects) {
AMS.projects.forEach(p => {
const o = document.createElement('option');
o.value = p.name;
o.textContent = `${p.name} (${p.dept})`;
projSel.appendChild(o);
});
}
// Populate assignee select dynamically from AMS.users
const assignSel = document.getElementById('assetAssignee');
if (assignSel && window.AMS && AMS.users) {
assignSel.innerHTML = '<option value="">Unassigned (Pool Asset)</option>';
AMS.users.forEach(u => {
const o = document.createElement('option');
o.value = u.name;
o.textContent = `${u.name} ${u.role} (${u.dept})`;
assignSel.appendChild(o);
});
}
});
function renderStepNav() {
@ -323,6 +368,50 @@ function scrollToStep(n) {
if (el) el.scrollIntoView({behavior:'smooth',block:'start'});
}
function handleTypeChange() {
const type = document.getElementById('assetType').value;
const catSel = document.getElementById('assetCategory');
// Dynamic categories based on asset type
let catsHtml = '';
if (type === 'physical') {
catsHtml = `
<option value="">Select category…</option>
<option>Laptops</option><option>Desktops</option><option>Servers</option>
<option>Printers</option><option>Networking</option><option>Mobile Devices</option>
<option>AV Equipment</option><option>Displays</option><option>Furniture</option>
<option>Vehicles</option><option>HVAC</option><option>Power Equipment</option><option>Storage</option>
`;
document.getElementById('physicalFieldsRow').style.display = '';
document.getElementById('digitalFieldsRow').style.display = 'none';
document.getElementById('assigneeGroup').style.display = '';
document.getElementById('projectGroup').style.display = 'none';
document.getElementById('assetLoc').closest('.form-group').style.display = '';
} else if (type === 'digital') {
catsHtml = `
<option value="">Select category…</option>
<option>Software Licenses</option><option>Cloud Subscriptions</option>
`;
document.getElementById('physicalFieldsRow').style.display = 'none';
document.getElementById('digitalFieldsRow').style.display = '';
document.getElementById('assigneeGroup').style.display = 'none';
document.getElementById('projectGroup').style.display = '';
document.getElementById('assetLoc').closest('.form-group').style.display = 'none';
} else {
catsHtml = `
<option value="">Select category…</option>
<option>Service Agreements</option><option>Leases</option><option>Patents/IP</option>
`;
document.getElementById('physicalFieldsRow').style.display = 'none';
document.getElementById('digitalFieldsRow').style.display = 'none';
document.getElementById('assigneeGroup').style.display = '';
document.getElementById('projectGroup').style.display = '';
document.getElementById('assetLoc').closest('.form-group').style.display = 'none';
}
catSel.innerHTML = catsHtml;
renderCustomFields();
}
function renderCustomFields() {
const cat = document.getElementById('assetCategory').value;
const section = document.getElementById('customFieldsSection');
@ -340,9 +429,20 @@ function renderCustomFields() {
</div>`).join('');
}
function nextAssetId() {
// Derive from the highest existing sequence number so IDs never collide
const maxNum = AMS.assets.reduce((m, a) => {
const n = parseInt(String(a.id).split('-').pop(), 10);
return isNaN(n) ? m : Math.max(m, n);
}, 0);
return `AST-2025-${String(maxNum + 1).padStart(3, '0')}`;
}
function updatePreview() {
const n = document.getElementById('assetName').value;
if(n) document.getElementById('previewId').textContent = 'AST-2025-021';
if(n && window.AMS) {
document.getElementById('previewId').textContent = nextAssetId();
}
}
function calcDepreciation() {
@ -373,24 +473,112 @@ function saveDraft() {
}
function submitAsset() {
const name = document.getElementById('assetName').value;
const name = document.getElementById('assetName').value.trim();
const type = document.getElementById('assetType').value;
const cat = document.getElementById('assetCategory').value;
const cost = document.getElementById('purchaseCost').value;
if (!name) { showToast('Validation Error','Please enter the asset name','error'); document.getElementById('assetName').focus(); return; }
if (!cat) { showToast('Validation Error','Please select a category','error'); document.getElementById('assetCategory').focus(); return; }
if (!cost) { showToast('Validation Error','Please enter the purchase cost','error'); document.getElementById('purchaseCost').focus(); return; }
const cost = parseFloat(document.getElementById('purchaseCost').value);
const vendor = document.getElementById('assetVendor').value;
const pdate = document.getElementById('purchaseDate').value;
const dept = document.getElementById('assetDept').value;
const depRraw = document.getElementById('depRate').value;
const depR = parseFloat(depRraw);
// Simulate save
const fail = (msg, id) => { showToast('Validation Error', msg, 'error'); const el = document.getElementById(id); if (el) el.focus(); };
// Step 1
if (!name) { fail('Please enter the asset name','assetName'); return; }
if (!cat) { fail('Please select a category','assetCategory'); return; }
// Step 2 — financials
if (isNaN(cost) || cost <= 0) { fail('Please enter a valid purchase cost greater than 0','purchaseCost'); return; }
if (!vendor || vendor === 'new') { fail('Please select a vendor / supplier','assetVendor'); return; }
if (!pdate) { fail('Please select the purchase date','purchaseDate'); return; }
if (isNaN(depR) || depR < 1 || depR > 100) { fail('Depreciation rate must be between 1 and 100','depRate'); return; }
// Step 3 — location & assignment
if (!dept) { fail('Please select a department','assetDept'); return; }
// Digital seat integrity
if (type === 'digital') {
const tot = parseInt(document.getElementById('seatsTotal').value, 10);
const alloc = parseInt(document.getElementById('seatsAlloc').value, 10);
if (isNaN(tot) || tot < 1) { fail('Total seats must be at least 1','seatsTotal'); return; }
if (isNaN(alloc) || alloc < 0) { fail('Allocated seats cannot be negative','seatsAlloc'); return; }
if (alloc > tot) { fail('Allocated seats cannot exceed total seats','seatsAlloc'); return; }
}
// Generate ID
const nextId = nextAssetId();
// Build new asset record
const newAsset = {
id: nextId,
name: name,
cat: cat,
type: type,
status: document.getElementById('assetStatus').value || 'Active',
dept: document.getElementById('assetDept').value || 'IT',
purchase: document.getElementById('purchaseDate').value || new Date().toISOString().split('T')[0],
cost: cost,
value: cost,
vendor: document.getElementById('assetVendor').value || 'N/A',
warranty: document.getElementById('warrantyDate').value || 'N/A',
depM: document.getElementById('depMethod').value || 'SLM',
depR: parseFloat(document.getElementById('depRate').value) || 20,
icon: type === 'digital' ? (cat.includes('Subscription') ? '☁️' : '🔑') : (cat === 'Vehicles' ? '🚗' : '💻')
};
if (type === 'physical') {
newAsset.serial = document.getElementById('serialNo').value || 'N/A';
newAsset.loc = document.getElementById('assetLoc').value || 'IT Dept Floor 2';
newAsset.assignee = document.getElementById('assetAssignee').value || 'Unassigned';
} else if (type === 'digital') {
newAsset.serial = 'Digital-License';
newAsset.loc = 'Cloud Environment';
newAsset.assignee = 'IT Team';
newAsset.licenseKey = document.getElementById('licenseKey').value || 'N/A';
newAsset.seatsTotal = parseInt(document.getElementById('seatsTotal').value) || 1;
newAsset.seatsAlloc = parseInt(document.getElementById('seatsAlloc').value) || 0;
newAsset.project = document.getElementById('assetProject').value || 'Unassigned / No Project';
} else {
newAsset.serial = 'N/A';
newAsset.loc = 'Corporate HQ';
newAsset.assignee = document.getElementById('assetAssignee').value || 'Unassigned';
newAsset.project = document.getElementById('assetProject').value || 'Unassigned / No Project';
}
// Save to datastore
AMS.assets.push(newAsset);
// Track activity
AMS.activityFeed.unshift({
user: AMS.currentUser.name,
av: AMS.currentUser.avatar,
color: '#6366F1',
action: 'created asset',
target: newAsset.name,
detail: `(${newAsset.id})`,
time: 'Just now'
});
// Adjust stats
AMS.stats.total++;
if (newAsset.status === 'Active') AMS.stats.active++;
else if (newAsset.status === 'Under Maintenance') AMS.stats.maintenance++;
else if (newAsset.status === 'Idle') AMS.stats.idle++;
AMS.stats.totalValue += cost;
AMS.stats.netValue += cost;
AMS.save();
// Show Success Modal
document.querySelector('#successModal .modal').innerHTML = `
<div style="text-align:center;padding:36px 28px">
<div style="font-size:52px;margin-bottom:16px">🎉</div>
<div style="font-size:20px;font-weight:800;margin-bottom:8px">Asset Created!</div>
<div style="font-size:13px;color:var(--text-muted);margin-bottom:6px">Asset ID: <code style="color:var(--primary-light);font-size:14px">AST-2025-021</code></div>
<div style="font-size:13px;color:var(--text-muted);margin-bottom:6px">Asset ID: <code style="color:var(--primary-light);font-size:14px">${nextId}</code></div>
<div style="font-size:12px;color:var(--text-muted);margin-bottom:6px"><strong style="color:var(--text-secondary)">${name}</strong> — ${cat}</div>
<div style="font-size:12.5px;color:var(--text-muted);margin-bottom:24px">QR code generated. Assignee acknowledgment email sent.</div>
<div class="flex gap-3" style="justify-content:center">
<a href="assets.html" class="btn btn-secondary">← Asset List</a>
<a href="asset-detail.html?id=AST-2025-001" class="btn btn-primary">View Asset →</a>
<a href="asset-detail.html?id=${nextId}" class="btn btn-primary">View Asset →</a>
</div>
</div>`;
openModal('successModal');

View File

@ -10,26 +10,7 @@
<body>
<div class="app-layout">
<!-- SIDEBAR -->
<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 active"><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"><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>
<aside class="sidebar" id="appSidebar"></aside>
<!-- MAIN -->
<div class="main-wrapper">
@ -43,7 +24,7 @@
<button class="btn btn-secondary btn-sm" onclick="openModal('assignModal')">👤 Assign</button>
<button class="btn btn-secondary btn-sm" onclick="openModal('transferModal')">↔ Transfer</button>
<button class="btn btn-danger btn-sm" onclick="confirmDispose()">🗑️ Dispose</button>
<a href="index.html" class="icon-btn">🚪</a>
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
</div>
</header>
@ -153,13 +134,18 @@
<div class="modal-body">
<div class="form-row">
<div class="form-group"><label class="form-label">Asset Name <span class="req">*</span></label><input class="form-input" id="editName"></div>
<div class="form-group"><label class="form-label">Serial Number</label><input class="form-input" id="editSerial"></div>
<div class="form-group"><label class="form-label">Serial Number / License Key</label><input class="form-input" id="editSerial"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Category</label><select class="form-select" id="editCat"><option>Laptops</option><option>Desktops</option><option>Printers</option><option>Networking</option><option>Vehicles</option><option>Furniture</option><option>HVAC</option></select></div>
<div class="form-group"><label class="form-label">Category</label><select class="form-select" id="editCat"><option>Laptops</option><option>Desktops</option><option>Servers</option><option>Printers</option><option>Networking</option><option>Vehicles</option><option>Furniture</option><option>HVAC</option><option>Software Licenses</option><option>Cloud Subscriptions</option></select></div>
<div class="form-group"><label class="form-label">Status</label><select class="form-select" id="editStatus"><option>Active</option><option>Idle</option><option>Under Maintenance</option><option>Disposed</option></select></div>
</div>
<div class="form-group"><label class="form-label">Change Reason (for audit trail) <span class="req">*</span></label><textarea class="form-textarea" rows="2" placeholder="Why is this being updated?"></textarea></div>
<!-- Digital-only fields inside Edit Modal -->
<div class="form-row" id="editDigitalRow" style="display:none">
<div class="form-group"><label class="form-label">Total Seats</label><input type="number" class="form-input" id="editSeatsTotal"></div>
<div class="form-group"><label class="form-label">Allocated Seats</label><input type="number" class="form-input" id="editSeatsAlloc"></div>
</div>
<div class="form-group"><label class="form-label">Change Reason (for audit trail) <span class="req">*</span></label><textarea class="form-textarea" id="editReason" rows="2" placeholder="Why is this being updated?"></textarea></div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('editModal')">Cancel</button>
@ -173,15 +159,28 @@
<div class="modal">
<div class="modal-header"><span class="modal-title">Assign Asset</span><button class="modal-close"></button></div>
<div class="modal-body">
<div class="form-group"><label class="form-label">Employee <span class="req">*</span></label>
<select class="form-select"><option value="">Select…</option><option>Priya Kumar IT</option><option>Rahul Mehta Finance</option><option>Anita Singh HR</option><option>Vikram Reddy Operations</option><option>Sneha Patel Marketing</option></select>
<div class="form-group">
<label class="form-label">Assign Type</label>
<div class="flex gap-4" style="margin-bottom:8px">
<label style="cursor:pointer; font-size:13px"><input type="radio" name="assignType" value="user" checked onchange="toggleAssignType()"> Employee</label>
<label style="cursor:pointer; font-size:13px"><input type="radio" name="assignType" value="project" onchange="toggleAssignType()"> Project</label>
</div>
</div>
<div class="form-group" id="assignUserGroup">
<label class="form-label">Employee <span class="req">*</span></label>
<select class="form-select" id="assignUserSelect">
<option value="">Select Employee…</option>
</select>
</div>
<div class="form-group" id="assignProjectGroup" style="display:none">
<label class="form-label">Project <span class="req">*</span></label>
<select class="form-select" id="assignProjectSelect">
<option value="">Select Project…</option>
</select>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">From Date</label><input type="date" class="form-input" value="2025-05-28"></div>
<div class="form-group"><label class="form-label">Return By (optional)</label><input type="date" class="form-input"></div>
</div>
<div class="form-group"><label class="form-label">Acknowledgment</label>
<select class="form-select"><option>Yes Send email confirmation</option><option>No Direct assignment</option></select>
<div class="form-group"><label class="form-label">From Date</label><input type="date" class="form-input" id="assignFromDate" value="2026-06-04"></div>
<div class="form-group"><label class="form-label">Return By (optional)</label><input type="date" class="form-input" id="assignReturnDate"></div>
</div>
</div>
<div class="modal-footer">
@ -224,7 +223,9 @@
</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>
const params = new URLSearchParams(window.location.search);
@ -241,29 +242,86 @@ document.addEventListener('DOMContentLoaded', () => {
renderDocuments();
renderHistory();
initTabs();
// Populate Edit Modal fields
document.getElementById('editName').value = asset.name;
document.getElementById('editSerial').value = asset.serial || asset.licenseKey || '';
document.getElementById('editCat').value = asset.cat;
document.getElementById('editStatus').value = asset.status;
if (asset.type === 'digital') {
document.getElementById('editDigitalRow').style.display = '';
document.getElementById('editSeatsTotal').value = asset.seatsTotal || 1;
document.getElementById('editSeatsAlloc').value = asset.seatsAlloc || 0;
}
// Populate Assign modal employee dropdown
const userSelect = document.getElementById('assignUserSelect');
if (userSelect && AMS.users) {
userSelect.innerHTML = '<option value="">Select Employee…</option>';
AMS.users.forEach(u => {
const o = document.createElement('option');
o.value = u.name;
o.textContent = `${u.name} ${u.role} (${u.dept})`;
if (asset.assignee === u.name) o.selected = true;
userSelect.appendChild(o);
});
}
// Populate Assign modal project dropdown
const projSelect = document.getElementById('assignProjectSelect');
if (projSelect && AMS.projects) {
projSelect.innerHTML = '<option value="">Select Project…</option>';
AMS.projects.forEach(p => {
const o = document.createElement('option');
o.value = p.name;
o.textContent = `${p.name} (${p.dept})`;
if (asset.project === p.name) o.selected = true;
projSelect.appendChild(o);
});
}
});
function toggleAssignType() {
const type = document.querySelector('input[name="assignType"]:checked').value;
if (type === 'user') {
document.getElementById('assignUserGroup').style.display = '';
document.getElementById('assignProjectGroup').style.display = 'none';
} else {
document.getElementById('assignUserGroup').style.display = 'none';
document.getElementById('assignProjectGroup').style.display = '';
}
}
function renderHeader() {
document.getElementById('assetHeaderTitle').textContent = asset.name;
const depPct = Math.round((asset.cost - asset.value)/asset.cost*100);
const isDigital = asset.type === 'digital';
const depPct = isDigital ? 100 : Math.round((asset.cost - asset.value)/asset.cost*100);
document.getElementById('assetHeader').innerHTML = `
<div class="card-body">
<div class="flex items-center gap-4 flex-wrap">
<div style="width:64px;height:64px;border-radius:16px;background:var(--primary-glow);display:flex;align-items:center;justify-content:center;font-size:30px;border:1px solid var(--border-accent);flex-shrink:0">${asset.icon}</div>
<div style="width:64px;height:64px;border-radius:16px;background:var(--primary-glow);display:flex;align-items:center;justify-content:center;font-size:30px;border:1px solid var(--border-accent);flex-shrink:0">${asset.icon || '📦'}</div>
<div style="flex:1;min-width:0">
<div style="font-size:20px;font-weight:800;margin-bottom:4px">${asset.name}</div>
<div style="font-size:20px;font-weight:800;margin-bottom:4px">${escapeHtml(asset.name)}</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
<span class="badge badge-neutral" style="font-size:11px">${asset.id}</span>
<span class="badge badge-neutral" style="font-size:11px">${escapeHtml(asset.id)}</span>
<span class="badge ${statusBadge(asset.status)}"><span class="badge-dot-ind" style="background:${statusDot(asset.status)}"></span>${asset.status}</span>
<span class="badge badge-neutral" style="font-size:11px">${asset.cat}</span>
<span style="font-size:12px;color:var(--text-muted)">${asset.serial}</span>
<span class="badge badge-neutral" style="font-size:11px">${escapeHtml(asset.cat)}</span>
<span style="font-size:12px;color:var(--text-muted)">${isDigital ? 'Software License' : escapeHtml(asset.serial)}</span>
</div>
</div>
<div class="flex gap-5 flex-wrap">
<div style="text-align:right">
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px">Purchase Cost</div>
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px">${isDigital ? 'Cost (Annual)' : 'Purchase Cost'}</div>
<div style="font-size:18px;font-weight:800">${fmt(asset.cost)}</div>
</div>
${isDigital ? `
<div style="text-align:right">
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px">Allocated Seats</div>
<div style="font-size:18px;font-weight:800;color:var(--primary-light)">${asset.seatsAlloc || 0} / ${asset.seatsTotal || 1}</div>
</div>
` : `
<div style="text-align:right">
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px">Net Book Value</div>
<div style="font-size:18px;font-weight:800;color:${depPct>60?'var(--warning)':'var(--success)'}">${fmt(asset.value)}</div>
@ -272,52 +330,71 @@ function renderHeader() {
<div style="font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px">Depreciated</div>
<div style="font-size:18px;font-weight:800;color:var(--danger)">${depPct}%</div>
</div>
`}
</div>
</div>
<div style="margin-top:16px;background:var(--bg-surface);border-radius:var(--radius-md);overflow:hidden;height:6px">
<div style="height:100%;width:${100-depPct}%;background:linear-gradient(90deg,var(--primary),var(--cyan));transition:width .6s ease;border-radius:var(--radius-md)"></div>
<div style="height:100%;width:${isDigital ? (100 * (asset.seatsAlloc || 0)/(asset.seatsTotal || 1)) : (100-depPct)}%;background:linear-gradient(90deg,var(--primary),var(--cyan));transition:width .6s ease;border-radius:var(--radius-md)"></div>
</div>
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--text-muted);margin-top:5px">
<span>Net Value: ${fmt(asset.value)}</span>
<span>Fully Depreciated by: ~${new Date(new Date(asset.purchase).getFullYear() + Math.ceil(asset.cost/asset.value),0,1).getFullYear()}</span>
<span>${isDigital ? `Seat Utilization: ${Math.round((asset.seatsAlloc||0)/(asset.seatsTotal||1)*100)}%` : `Net Value: ${fmt(asset.value)}`}</span>
<span>${isDigital ? `Billing Cycle: Annual Renewal` : `Fully Depreciated by: ~${new Date(new Date(asset.purchase).getFullYear() + Math.ceil(asset.cost/asset.value),0,1).getFullYear()}`}</span>
<span>Purchased: ${fmtDate(asset.purchase)}</span>
</div>
</div>`;
}
function renderOverview() {
const isDigital = asset.type === 'digital';
document.getElementById('assetInfoGrid').innerHTML = `
<div class="info-grid">
<div class="info-item"><div class="info-label">Asset ID</div><div class="info-value">${asset.id}</div></div>
<div class="info-item"><div class="info-label">Serial Number</div><div class="info-value">${asset.serial}</div></div>
<div class="info-item"><div class="info-label">Category</div><div class="info-value">${asset.cat}</div></div>
<div class="info-item"><div class="info-label">Asset ID</div><div class="info-value">${escapeHtml(asset.id)}</div></div>
<div class="info-item"><div class="info-label">${isDigital ? 'Subscription Type' : 'Serial Number'}</div><div class="info-value">${isDigital ? 'Digital Subscription' : escapeHtml(asset.serial)}</div></div>
<div class="info-item"><div class="info-label">Category</div><div class="info-value">${escapeHtml(asset.cat)}</div></div>
<div class="info-item"><div class="info-label">Status</div><div class="info-value"><span class="badge ${statusBadge(asset.status)}">${asset.status}</span></div></div>
<div class="info-item"><div class="info-label">Vendor / Supplier</div><div class="info-value">${asset.vendor}</div></div>
<div class="info-item"><div class="info-label">Vendor / Supplier</div><div class="info-value">${escapeHtml(asset.vendor)}</div></div>
<div class="info-item"><div class="info-label">Purchase Date</div><div class="info-value">${fmtDate(asset.purchase)}</div></div>
<div class="info-item"><div class="info-label">Warranty Expiry</div><div class="info-value">${fmtDate(asset.warranty)}</div></div>
<div class="info-item"><div class="info-label">Depreciation Method</div><div class="info-value">${asset.depM} @ ${asset.depR}% p.a.</div></div>
<div class="info-item"><div class="info-label">${isDigital ? 'Renewal Date' : 'Warranty Expiry'}</div><div class="info-value">${fmtDate(asset.warranty)}</div></div>
<div class="info-item"><div class="info-label">Depreciation Method</div><div class="info-value">${isDigital ? 'N/A' : `${asset.depM} @ ${asset.depR}% p.a.`}</div></div>
</div>`;
document.getElementById('assetLocationGrid').innerHTML = `
<div class="info-grid">
<div class="info-item" style="grid-column:span 2"><div class="info-label">Current Location</div><div class="info-value">🏢 ${asset.loc}</div></div>
<div class="info-item"><div class="info-label">Department</div><div class="info-value">${asset.dept}</div></div>
<div class="info-item"><div class="info-label">Assigned To</div><div class="info-value">${asset.assignee}</div></div>
</div>
<hr class="divider">
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Custody Chain</div>
<div class="timeline">
<div class="timeline-item">
<div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--success-bg);color:var(--success)">👤</div><div class="tl-line"></div></div>
<div class="tl-content"><div class="tl-title">${asset.assignee} (Current)</div><div class="tl-desc">${asset.dept} · ${asset.loc}</div><div class="tl-time">From ${fmtDate(asset.purchase)}</div></div>
if (isDigital) {
document.getElementById('assetLocationGrid').innerHTML = `
<div class="info-grid">
<div class="info-item" style="grid-column:span 2"><div class="info-label">Deployment Environment</div><div class="info-value">☁️ ${escapeHtml(asset.loc || 'Cloud hosted')}</div></div>
<div class="info-item"><div class="info-label">Assigned Project</div><div class="info-value"><span class="badge badge-primary">${escapeHtml(asset.project || 'Unassigned')}</span></div></div>
<div class="info-item"><div class="info-label">License Owner</div><div class="info-value">${escapeHtml(asset.assignee || 'IT Team')}</div></div>
</div>
<div class="timeline-item">
<div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--bg-elevated);color:var(--text-muted)">🏪</div></div>
<div class="tl-content"><div class="tl-title">Warehouse (Initial Stock)</div><div class="tl-desc">Goods Receipt Note created</div><div class="tl-time">${fmtDate(asset.purchase)}</div></div>
<hr class="divider">
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">License Status</div>
<div style="padding:12px;background:var(--bg-surface);border-radius:var(--radius-md);border:1px solid var(--border)">
<div style="font-weight:600;font-size:13px;margin-bottom:4px">Key Details</div>
<code style="display:block;padding:6px;background:var(--bg-elevated);border-radius:4px;font-size:12px;color:var(--primary-light);margin-bottom:8px">${asset.licenseKey || 'N/A'}</code>
<div style="font-size:12px;color:var(--text-secondary)">Total Seats: <strong>${asset.seatsTotal || 1}</strong> &nbsp;·&nbsp; Allocated: <strong>${asset.seatsAlloc || 0}</strong></div>
</div>`;
} else {
document.getElementById('assetLocationGrid').innerHTML = `
<div class="info-grid">
<div class="info-item" style="grid-column:span 2"><div class="info-label">Current Location</div><div class="info-value">🏢 ${asset.loc}</div></div>
<div class="info-item"><div class="info-label">Department</div><div class="info-value">${asset.dept}</div></div>
<div class="info-item"><div class="info-label">Assigned To</div><div class="info-value">${asset.assignee}</div></div>
</div>
</div>`;
<hr class="divider">
<div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">Custody Chain</div>
<div class="timeline">
<div class="timeline-item">
<div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--success-bg);color:var(--success)">👤</div><div class="tl-line"></div></div>
<div class="tl-content"><div class="tl-title">${asset.assignee} (Current)</div><div class="tl-desc">${asset.dept} · ${asset.loc}</div><div class="tl-time">From ${fmtDate(asset.purchase)}</div></div>
</div>
<div class="timeline-item">
<div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--bg-elevated);color:var(--text-muted)">🏪</div></div>
<div class="tl-content"><div class="tl-title">Warehouse (Initial Stock)</div><div class="tl-desc">Goods Receipt Note created</div><div class="tl-time">${fmtDate(asset.purchase)}</div></div>
</div>
</div>`;
}
// QR code
// QR Code
const cells = generateQRPattern(asset.id);
document.getElementById('assetQR').innerHTML = `
<div class="qr-box" style="margin:0 auto">
@ -329,34 +406,63 @@ function renderOverview() {
</div>
<div style="margin-top:12px;font-size:12px;color:var(--text-muted)">Scan to view asset details</div>`;
// Custom fields by category
const fieldsByCategory = {
'Laptops': [['Processor','Intel Core i7 13th Gen'],['RAM','16 GB DDR5'],['Storage','512 GB NVMe SSD'],['OS','Windows 11 Pro'],['Screen Size','14 inch FHD']],
'Servers': [['CPU','2× Xeon Gold 5218'],['RAM','128 GB ECC'],['Storage','4× 1.8 TB SAS'],['RAID','RAID 10'],['OS','Ubuntu Server 22.04']],
'Vehicles': [['Registration No.',asset.serial],['Fuel Type','Petrol / Electric'],['Engine CC','1498 CC'],['Insurance Expiry','2026-01-15'],['Next Service','50,000 KM']],
'HVAC': [['Capacity','1.5 Ton'],['Type','Inverter Split'],['Refrigerant','R-32'],['Star Rating','5 Star'],['Installation Date',fmtDate(asset.purchase)]],
'Printers': [['Print Speed','40 ppm'],['Print Technology','Laser'],['Paper Sizes','A4, A5, Letter'],['Connectivity','USB, Network, Wi-Fi'],['Monthly Duty Cycle','80,000 pages']]
};
const fields = fieldsByCategory[asset.cat] || [['Brand',asset.vendor],['Part Number','N/A'],['Model Year','2024'],['Color','Black / Silver']];
document.getElementById('assetCustomFields').innerHTML = `
<div class="info-grid">
${fields.map(([k,v])=>`<div class="info-item"><div class="info-label">${k}</div><div class="info-value">${v}</div></div>`).join('')}
</div>`;
// Custom fields
let fieldsHtml = '';
if (isDigital) {
fieldsHtml = `
<div class="info-item"><div class="info-label">Platform Type</div><div class="info-value">SaaS Cloud</div></div>
<div class="info-item"><div class="info-label">Billing Cycle</div><div class="info-value">${asset.billingCycle || 'Annual'}</div></div>
<div class="info-item"><div class="info-label">Associated Project</div><div class="info-value">${asset.project || 'Unassigned'}</div></div>
<div class="info-item"><div class="info-label">Access Provider</div><div class="info-value">${asset.vendor}</div></div>
`;
} else {
const fieldsByCategory = {
'Laptops': [['Processor','Intel Core i7 13th Gen'],['RAM','16 GB DDR5'],['Storage','512 GB NVMe SSD'],['OS','Windows 11 Pro'],['Screen Size','14 inch FHD']],
'Servers': [['CPU','2× Xeon Gold 5218'],['RAM','128 GB ECC'],['Storage','4× 1.8 TB SAS'],['RAID','RAID 10'],['OS','Ubuntu Server 22.04']],
'Vehicles': [['Registration No.',asset.serial],['Fuel Type','Petrol / Electric'],['Engine CC','1498 CC'],['Insurance Expiry','2026-01-15'],['Next Service','50,000 KM']],
'HVAC': [['Capacity','1.5 Ton'],['Type','Inverter Split'],['Refrigerant','R-32'],['Star Rating','5 Star'],['Installation Date',fmtDate(asset.purchase)]],
'Printers': [['Print Speed','40 ppm'],['Print Technology','Laser'],['Paper Sizes','A4, A5, Letter'],['Connectivity','USB, Network, Wi-Fi'],['Monthly Duty Cycle','80,000 pages']]
};
const fields = fieldsByCategory[asset.cat] || [['Brand',asset.vendor],['Part Number','N/A'],['Model Year','2024'],['Color','Black / Silver']];
fieldsHtml = fields.map(([k,v])=>`<div class="info-item"><div class="info-label">${k}</div><div class="info-value">${v}</div></div>`).join('');
}
document.getElementById('assetCustomFields').innerHTML = `<div class="info-grid">${fieldsHtml}</div>`;
}
function renderFinancial() {
const isDigital = asset.type === 'digital';
if (isDigital) {
document.getElementById('tab-financial').innerHTML = `
<div class="card">
<div class="card-header"><span class="card-title">💰 Software Subscription Cost Breakdown</span></div>
<div class="card-body">
<div class="info-grid mb-4">
<div class="info-item"><div class="info-label">Subscription Cost</div><div class="info-value" style="font-size:18px;font-weight:800">${fmt(asset.cost)} / year</div></div>
<div class="info-item"><div class="info-label">Total Seats Purchased</div><div class="info-value">${asset.seatsTotal || 1}</div></div>
<div class="info-item"><div class="info-label">Allocated Seats</div><div class="info-value">${asset.seatsAlloc || 0}</div></div>
<div class="info-item"><div class="info-label">Cost per Seat (avg)</div><div class="info-value">${fmt(Math.round(asset.cost / (asset.seatsTotal || 1)))} / year</div></div>
</div>
<hr class="divider">
<div style="font-size:12.5px;color:var(--text-secondary)">
<strong style="color:var(--text-primary)">V1 Note:</strong> Software subscriptions are expensed as operational costs (OPEX) and are 100% written off annually. Depreciation methods (SLM/WDV) do not apply to SaaS licenses.
</div>
</div>
</div>`;
return;
}
const depPct = Math.round((asset.cost - asset.value)/asset.cost*100);
const years = Math.floor((new Date() - new Date(asset.purchase))/(365.25*24*3600*1000));
const years = Math.max(1, Math.floor((new Date() - new Date(asset.purchase))/(365.25*24*3600*1000)));
const annualDep = asset.depM === 'SLM' ? Math.round(asset.cost * asset.depR/100) : Math.round((asset.cost - asset.value) / Math.max(1,years));
document.getElementById('depreciationInfo').innerHTML = `
<div class="dep-bar-wrap">
<div class="dep-nums">
<div class="dep-bar-wrap" style="margin-bottom:16px">
<div class="dep-nums" style="display:flex;justify-content:space-between;margin-bottom:6px">
<span>Net Book Value: <strong>${fmt(asset.value)}</strong></span>
<span>Total Dep.: <strong style="color:var(--danger)">${fmt(asset.cost - asset.value)}</strong></span>
</div>
<div class="progress-wrap"><div class="progress-fill" style="width:${100-depPct}%"></div></div>
<div class="dep-nums" style="margin-top:6px;margin-bottom:0">
<div class="progress-wrap" style="background:var(--bg-surface);border-radius:4px;height:8px;overflow:hidden"><div class="progress-fill" style="width:${100-depPct}%;height:100%;background:var(--success)"></div></div>
<div class="dep-nums" style="margin-top:6px;margin-bottom:0;display:flex;justify-content:space-between">
<span style="font-size:11px">${100-depPct}% remaining value</span>
<span style="font-size:11px">${depPct}% depreciated</span>
</div>
@ -413,14 +519,19 @@ function renderFinancial() {
}
function renderAssignments() {
const isProject = asset.project && asset.project !== 'N/A' && asset.project !== 'Unassigned / No Project';
document.getElementById('assignmentHistory').innerHTML = `
<div class="timeline">
<div class="timeline-item">
<div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--success-bg);color:var(--success)"></div><div class="tl-line"></div></div>
<div class="tl-content"><div class="tl-title">${asset.assignee} (Current)</div><div class="tl-desc">${asset.dept} Department · ${asset.loc}</div><div class="tl-time">Since ${fmtDate(asset.purchase)} · Acknowledged via email</div></div>
<div class="tl-content">
<div class="tl-title">${isProject ? `Project: ${asset.project}` : `Employee: ${asset.assignee}`} (Current)</div>
<div class="tl-desc">${asset.dept} Department · ${asset.loc}</div>
<div class="tl-time">Since ${fmtDate(asset.purchase)} · Saved in records</div>
</div>
</div>
<div class="timeline-item">
<div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--bg-elevated);color:var(--text-muted)">🏪</div><div class="tl-line"></div></div>
<div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--bg-elevated);color:var(--text-muted)">🏪</div></div>
<div class="tl-content"><div class="tl-title">Warehouse (Initial)</div><div class="tl-desc">Asset received via GRN • PO-2024-038</div><div class="tl-time">${fmtDate(asset.purchase)}</div></div>
</div>
</div>`;
@ -450,8 +561,7 @@ function renderDocuments() {
const docs = [
{ name:'Purchase Invoice', type:'PDF', size:'240 KB', date:'2024-01-15', icon:'📄' },
{ name:'Delivery Challan', type:'PDF', size:'85 KB', date:'2024-01-16', icon:'📋' },
{ name:'Warranty Card', type:'PDF', size:'120 KB', date:'2024-01-15', icon:'🛡️' },
{ name:'Asset Photo', type:'JPG', size:'1.2 MB', date:'2024-01-18', icon:'📷' }
{ name:'Warranty Card', type:'PDF', size:'120 KB', date:'2024-01-15', icon:'🛡️' }
];
document.getElementById('assetDocs').innerHTML = docs.map(d=>`
<div class="flex items-center gap-3 mb-3" style="padding:12px;background:var(--bg-surface);border-radius:var(--radius-md);border:1px solid var(--border)">
@ -464,31 +574,126 @@ function renderDocuments() {
function renderHistory() {
const events = [
{ icon:'📦', bg:'var(--primary-glow)', color:'var(--primary-light)', title:'Asset Created', desc:`Imported via GRN · PO: PO-2024-038 · Vendor: ${asset.vendor}`, time:fmtDate(asset.purchase) },
{ icon:'📱', bg:'var(--info-bg)', color:'var(--info)', title:'QR Code Generated', desc:'Printed on A4 label sheet (8-up format)', time:fmtDate(asset.purchase) },
{ icon:'👤', bg:'var(--success-bg)', color:'var(--success)', title:'Assigned to '+asset.assignee, desc:`Dept: ${asset.dept} · Acknowledgment received`, time:fmtDate(asset.purchase) },
{ icon:'📝', bg:'var(--bg-elevated)', color:'var(--text-muted)', title:'Field Updated: Location', desc:'Previous: Warehouse → Current: '+asset.loc, time:'2024-02-01' }
{ icon:'📦', bg:'var(--primary-glow)', color:'var(--primary-light)', title:'Asset Created', desc:`Imported via GRN · Vendor: ${asset.vendor}`, time:fmtDate(asset.purchase) },
{ icon:'📱', bg:'var(--info-bg)', color:'var(--info)', title:'QR Code Generated', desc:'System assigned unique ID: '+asset.id, time:fmtDate(asset.purchase) }
];
if (asset.project && asset.project !== 'N/A' && asset.project !== 'Unassigned / No Project') {
events.push({ icon:'👤', bg:'var(--success-bg)', color:'var(--success)', title:'Assigned to Project', desc:`Project: ${asset.project}`, time:'Just now' });
} else {
events.push({ icon:'👤', bg:'var(--success-bg)', color:'var(--success)', title:'Assigned to '+asset.assignee, desc:`Dept: ${asset.dept} · Location: ${asset.loc}`, time:'Just now' });
}
document.getElementById('auditTrail').innerHTML = `
<div class="timeline">
${events.map(e=>`
<div class="timeline-item">
<div class="tl-icon-wrap"><div class="tl-icon" style="background:${e.bg};color:${e.color}">${e.icon}</div><div class="tl-line"></div></div>
<div class="tl-content"><div class="tl-title">${e.title}</div><div class="tl-desc">${e.desc}</div><div class="tl-time">By Arjun Sharma · ${e.time}</div></div>
<div class="tl-content"><div class="tl-title">${e.title}</div><div class="tl-desc">${e.desc}</div><div class="tl-time">By ${AMS.currentUser.name} · ${e.time}</div></div>
</div>`).join('')}
</div>`;
}
function saveEdit() { closeModal('editModal'); showToast('Asset Updated','Changes saved with audit trail entry','success'); }
function doAssign() { closeModal('assignModal'); showToast('Assigned','Acknowledgment email sent','success'); }
function doTransfer() { closeModal('transferModal'); showToast('Transfer Initiated','Pending Department Head approval','info'); }
function saveEdit() {
const target = AMS.assets.find(a => a.id === asset.id);
if (!target) return;
target.name = document.getElementById('editName').value;
target.cat = document.getElementById('editCat').value;
target.status = document.getElementById('editStatus').value;
if (asset.type === 'digital') {
target.licenseKey = document.getElementById('editSerial').value;
target.seatsTotal = parseInt(document.getElementById('editSeatsTotal').value) || 1;
target.seatsAlloc = parseInt(document.getElementById('editSeatsAlloc').value) || 0;
} else {
target.serial = document.getElementById('editSerial').value;
}
// Track activity feed
AMS.activityFeed.unshift({
user: AMS.currentUser.name,
av: AMS.currentUser.avatar,
color: '#3B82F6',
action: 'updated asset',
target: target.name,
detail: `(${target.id}) — Reason: ${document.getElementById('editReason').value || 'Details correction'}`,
time: 'Just now'
});
AMS.save();
closeModal('editModal');
showToast('Asset Updated', 'Changes saved successfully', 'success');
setTimeout(() => window.location.reload(), 800);
}
function doAssign() {
const target = AMS.assets.find(a => a.id === asset.id);
if (!target) return;
const assignType = document.querySelector('input[name="assignType"]:checked').value;
if (assignType === 'user') {
const user = document.getElementById('assignUserSelect').value;
if (!user) { showToast('Error', 'Please select an employee', 'error'); return; }
target.assignee = user;
target.project = 'Unassigned / No Project';
} else {
const proj = document.getElementById('assignProjectSelect').value;
if (!proj) { showToast('Error', 'Please select a project', 'error'); return; }
target.project = proj;
target.assignee = 'Shared (Project Asset)';
}
// Add audit trace
AMS.activityFeed.unshift({
user: AMS.currentUser.name,
av: AMS.currentUser.avatar,
color: '#10B981',
action: 're-assigned asset',
target: target.name,
detail: `to ${assignType === 'user' ? target.assignee : target.project}`,
time: 'Just now'
});
AMS.save();
closeModal('assignModal');
showToast('Asset Assigned', `Assigned successfully`, 'success');
setTimeout(() => window.location.reload(), 800);
}
function doTransfer() {
closeModal('transferModal');
showToast('Transfer Request', 'Approval request sent to Department Head', 'info');
}
function confirmDispose() {
confirmAction('Dispose Asset',`Mark "${asset.name}" as disposed? This action is permanent and will generate a write-off entry.`,()=>{
showToast('Asset Disposed','Write-off entry created. Finance notified.','success');
setTimeout(()=>window.location.href='assets.html',1500);
confirmAction('Dispose Asset', `Mark "${asset.name}" as disposed? This action is permanent and will write off this asset.`, () => {
const target = AMS.assets.find(a => a.id === asset.id);
if (target) {
target.status = 'Disposed';
// Update statistics
AMS.stats.active = AMS.assets.filter(a => a.status === 'Active').length;
AMS.stats.maintenance = AMS.assets.filter(a => a.status === 'Under Maintenance').length;
AMS.stats.idle = AMS.assets.filter(a => a.status === 'Idle').length;
AMS.stats.disposed = AMS.assets.filter(a => a.status === 'Disposed').length;
AMS.activityFeed.unshift({
user: AMS.currentUser.name,
av: AMS.currentUser.avatar,
color: '#EF4444',
action: 'disposed asset',
target: target.name,
detail: `(${target.id})`,
time: 'Just now'
});
AMS.save();
showToast('Asset Disposed', 'Asset status set to Disposed', 'success');
setTimeout(() => window.location.href = 'assets.html', 1200);
}
});
}
function printQR() { showToast('Print','QR label sent to printer','info'); }
function printQR() { showToast('Print', 'QR label sent to printer', 'info'); }
</script>
</body>
</html>

View File

@ -10,36 +10,17 @@
<div class="app-layout">
<!-- SIDEBAR -->
<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 active"><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 <span class="nav-badge">8</span></a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><span class="nav-icon">🔧</span> Maintenance <span class="nav-badge">12</span></a>
<a href="reports.html" class="nav-item"><span class="nav-icon">📈</span> Reports &amp; Audit</a>
<div class="nav-section-label">Administration</div>
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users &amp; Roles</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>
<aside class="sidebar" id="appSidebar"></aside>
<!-- MAIN -->
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-left"><div class="topbar-title">All Assets <span class="topbar-sub" id="totalCount"></span></div></div>
<div class="topbar-search"><span style="color:var(--text-muted)">🔍</span><input type="text" placeholder="Search assets…" id="assetSearch" data-search-table="assetTable"></div>
<div class="topbar-search"><span style="color:var(--text-muted)"><i data-lucide="search" style="width:14px;height:14px"></i></span><input type="text" placeholder="Search assets…" id="assetSearch" data-search-table="assetTable"></div>
<div class="topbar-actions">
<a href="asset-create.html" class="btn btn-primary btn-sm">+ Add Asset</a>
<button class="btn btn-secondary btn-sm" onclick="exportAssets()">📥 Export</button>
<a href="index.html" class="icon-btn">🚪</a>
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
</div>
</header>
@ -49,10 +30,22 @@
<!-- Filters -->
<div class="filters-row" id="filtersRow">
<div class="search-wrap" style="max-width:300px">
<div class="search-wrap" style="max-width:240px">
<span style="color:var(--text-muted);font-size:13px">🔍</span>
<input type="text" id="tableSearch" placeholder="Filter list…">
</div>
<select class="filter-sel" id="typeFilter">
<option value="">All Types</option>
<option value="physical">Physical Assets</option>
<option value="digital">Digital Assets</option>
<option value="other">Other Assets</option>
</select>
<select class="filter-sel" id="assignFilter">
<option value="">All Assignments</option>
<option value="user">Assigned to User</option>
<option value="project">Assigned to Project</option>
<option value="unassigned">Unassigned</option>
</select>
<select class="filter-sel" id="statusFilter">
<option value="">All Statuses</option>
<option>Active</option><option>Idle</option>
@ -64,6 +57,7 @@
<option>Printers</option><option>Networking</option><option>Mobile Devices</option>
<option>AV Equipment</option><option>Displays</option><option>Furniture</option>
<option>Vehicles</option><option>HVAC</option><option>Power Equipment</option>
<option>Software Licenses</option><option>Cloud Subscriptions</option>
</select>
<select class="filter-sel" id="deptFilter">
<option value="">All Departments</option>
@ -71,7 +65,7 @@
<option>Operations</option><option>Marketing</option><option>Admin</option>
</select>
<button class="btn btn-ghost btn-sm" onclick="clearFilters()">✕ Clear</button>
<div style="margin-left:auto;font-size:12.5px;color:var(--text-muted)">Showing <strong id="visCount" style="color:var(--text-primary)">20</strong> of 1,247 assets</div>
<div style="margin-left:auto;font-size:12.5px;color:var(--text-muted)">Showing <strong id="visCount" style="color:var(--text-primary)">0</strong> assets</div>
</div>
<!-- Bulk Bar -->
@ -221,7 +215,9 @@
</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>
let currentPage = 1;
@ -231,9 +227,11 @@ let filteredAssets = [];
document.addEventListener('DOMContentLoaded', () => {
renderStats();
renderTable();
applyFilters();
initCheckboxes('assetTable','assetTableBulk');
document.getElementById('tableSearch').addEventListener('input', applyFilters);
document.getElementById('typeFilter').addEventListener('change', applyFilters);
document.getElementById('assignFilter').addEventListener('change', applyFilters);
document.getElementById('statusFilter').addEventListener('change', applyFilters);
document.getElementById('catFilter').addEventListener('change', applyFilters);
document.getElementById('deptFilter').addEventListener('change', applyFilters);
@ -265,21 +263,43 @@ function filterByStatus(s) {
function applyFilters() {
const q = document.getElementById('tableSearch').value.toLowerCase();
const ty = document.getElementById('typeFilter').value.toLowerCase();
const as = document.getElementById('assignFilter').value.toLowerCase();
const st = document.getElementById('statusFilter').value.toLowerCase();
const ct = document.getElementById('catFilter').value.toLowerCase();
const dt = document.getElementById('deptFilter').value.toLowerCase();
filteredAssets = AMS.assets.filter(a =>
(!q || a.name.toLowerCase().includes(q) || a.id.toLowerCase().includes(q) || a.serial.toLowerCase().includes(q)) &&
(!st || a.status.toLowerCase() === st) &&
(!ct || a.cat.toLowerCase() === ct) &&
(!dt || a.dept.toLowerCase() === dt)
);
filteredAssets = AMS.assets.filter(a => {
const matchesSearch = !q || a.name.toLowerCase().includes(q) || a.id.toLowerCase().includes(q) || (a.serial && a.serial.toLowerCase().includes(q));
const matchesType = !ty || (a.type && a.type.toLowerCase() === ty);
const matchesStatus = !st || a.status.toLowerCase() === st;
const matchesCat = !ct || a.cat.toLowerCase() === ct;
const matchesDept = !dt || a.dept.toLowerCase() === dt;
let matchesAssign = true;
if (as === 'user') {
matchesAssign = a.assignee && a.assignee !== 'Unassigned' && (!a.project || a.project === 'N/A' || a.project === 'Unassigned / No Project');
} else if (as === 'project') {
matchesAssign = a.project && a.project !== 'N/A' && a.project !== 'Unassigned / No Project';
} else if (as === 'unassigned') {
matchesAssign = !a.assignee || a.assignee === 'Unassigned';
}
// Role-based restrictions: Employees can only view assets assigned to them
const isEmployee = AMS.currentUser.role === 'Employee';
const matchesRole = !isEmployee || a.assignee === AMS.currentUser.name;
return matchesSearch && matchesType && matchesAssign && matchesStatus && matchesCat && matchesDept && matchesRole;
});
currentPage = 1;
renderTable();
}
function clearFilters() {
document.getElementById('tableSearch').value = '';
document.getElementById('typeFilter').value = '';
document.getElementById('assignFilter').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('catFilter').value = '';
document.getElementById('deptFilter').value = '';
@ -287,8 +307,10 @@ function clearFilters() {
}
function renderTable() {
if (!filteredAssets.length && !document.getElementById('tableSearch').value) {
filteredAssets = [...AMS.assets];
if (!filteredAssets.length && !document.getElementById('tableSearch').value && !document.getElementById('typeFilter').value && !document.getElementById('assignFilter').value && !document.getElementById('statusFilter').value && !document.getElementById('catFilter').value && !document.getElementById('deptFilter').value) {
// If no filters are active and no search, check employee role list
const isEmployee = AMS.currentUser.role === 'Employee';
filteredAssets = isEmployee ? AMS.assets.filter(a => a.assignee === AMS.currentUser.name) : [...AMS.assets];
}
document.getElementById('visCount').textContent = filteredAssets.length;
const start = (currentPage-1)*perPage;
@ -300,30 +322,37 @@ function renderTable() {
}
const warningDate = new Date(); warningDate.setDate(warningDate.getDate()+90);
tbody.innerHTML = page.map(a => {
const isDigital = a.type === 'digital';
const warrantyExpiring = a.warranty !== 'N/A' && new Date(a.warranty) < warningDate;
const depPct = Math.round((a.cost - a.value)/a.cost*100);
const depPct = isDigital ? 100 : Math.round((a.cost - a.value)/a.cost*100);
const assigneeText = (a.project && a.project !== 'N/A' && a.project !== 'Unassigned / No Project')
? `<span class="badge badge-primary" style="font-size:10.5px;padding:3px 6px">📁 ${escapeHtml(a.project)}</span>`
: escapeHtml(a.assignee || 'Unassigned');
const valueText = isDigital
? `<div style="font-weight:600;color:var(--primary-light)">${a.seatsAlloc}/${a.seatsTotal} seats</div><div style="font-size:10px;color:var(--text-muted)">allocated</div>`
: `<div style="font-weight:600;color:${depPct>60?'var(--warning)':'var(--text-primary)'}">${fmt(a.value)}</div><div style="font-size:10px;color:var(--text-muted)">${depPct}% depreciated</div>`;
return `<tr onclick="goToDetail('${a.id}')">
<td onclick="event.stopPropagation()"><input type="checkbox" class="row-check"></td>
<td><div class="asset-cell">
<div class="asset-ic">${a.icon}</div>
<div><div class="asset-n">${a.name}</div><div class="asset-id">${a.id} · ${a.serial}</div></div>
<div class="asset-ic">${a.icon || '📦'}</div>
<div><div class="asset-n">${escapeHtml(a.name)}</div><div class="asset-id">${escapeHtml(a.id)} · ${isDigital ? 'Software License' : escapeHtml(a.serial)}</div></div>
</div></td>
<td><span class="badge badge-neutral" style="font-size:10px">${a.cat}</span></td>
<td><span class="badge badge-neutral" style="font-size:10px">${escapeHtml(a.cat)}</span></td>
<td><span class="badge ${statusBadge(a.status)}"><span class="badge-dot-ind" style="background:${statusDot(a.status)}"></span>${a.status}</span></td>
<td>${a.dept}</td>
<td>${a.assignee}</td>
<td>${escapeHtml(a.dept)}</td>
<td>${assigneeText}</td>
<td style="font-weight:600">${fmt(a.cost)}</td>
<td>
<div style="font-weight:600;color:${depPct>60?'var(--warning)':'var(--text-primary)'}">${fmt(a.value)}</div>
<div style="font-size:10px;color:var(--text-muted)">${depPct}% depreciated</div>
</td>
<td><span style="color:${warrantyExpiring?'var(--warning)':'var(--text-muted)';};font-size:12px">${a.warranty==='N/A'?'—':fmtDate(a.warranty)}</span></td>
<td>${valueText}</td>
<td><span style="color:${warrantyExpiring?'var(--warning)':'var(--text-muted)'};font-size:12px">${a.warranty==='N/A'?'—':fmtDate(a.warranty)}</span></td>
<td onclick="event.stopPropagation()">
<div class="flex gap-1">
<button class="btn btn-icon btn-ghost" title="View" onclick="goToDetail('${a.id}')">👁️</button>
<button class="btn btn-icon btn-ghost" title="QR Code" onclick="showQR('${a.id}')">📱</button>
<button class="btn btn-icon btn-ghost" title="Assign" onclick="openAssignModal('${a.id}','${a.name}','${a.dept}')">👤</button>
<button class="btn btn-icon btn-ghost" title="Transfer" onclick="openTransferModal('${a.id}','${a.loc}')"></button>
<button class="btn btn-icon btn-ghost" title="View" onclick="goToDetail('${a.id}')"><i data-lucide="eye" style="width:14px;height:14px"></i></button>
<button class="btn btn-icon btn-ghost" title="QR Code" onclick="showQR('${a.id}')"><i data-lucide="qr-code" style="width:14px;height:14px"></i></button>
<button class="btn btn-icon btn-ghost" title="Assign" onclick="openAssignModal('${escapeJs(a.id)}','${escapeJs(a.name)}','${escapeJs(a.dept)}')"><i data-lucide="user-check" style="width:14px;height:14px"></i></button>
<button class="btn btn-icon btn-ghost" title="Transfer" onclick="openTransferModal('${a.id}','${a.loc}')"><i data-lucide="arrow-left-right" style="width:14px;height:14px"></i></button>
</div>
</td>
</tr>`;

File diff suppressed because it is too large Load Diff

View File

@ -12,38 +12,7 @@
<div class="app-layout">
<!-- ════ SIDEBAR ════ -->
<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 active"><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 <span class="nav-badge" style="background:var(--warning)">5</span></a>
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement <span class="nav-badge">8</span></a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><span class="nav-icon">🔧</span> Maintenance <span class="nav-badge">12</span></a>
<a href="reports.html" class="nav-item"><span class="nav-icon">📈</span> Reports &amp; Audit</a>
<div class="nav-section-label">Administration</div>
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users &amp; Roles</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>
<span style="color:var(--text-muted)"></span>
</div>
</div>
</aside>
<aside class="sidebar" id="appSidebar"></aside>
<!-- ════ MAIN ════ -->
<div class="main-wrapper">
@ -54,19 +23,19 @@
<div class="topbar-title">Dashboard <span class="topbar-sub">Welcome back, Arjun 👋</span></div>
</div>
<div class="topbar-search">
<span style="color:var(--text-muted)">🔍</span>
<span style="color:var(--text-muted)"><i data-lucide="search" style="width:14px;height:14px"></i></span>
<input type="text" placeholder="Search assets, tickets…" id="globalSearch">
</div>
<div class="topbar-actions">
<div style="position:relative">
<button class="icon-btn" id="notifBell">🔔<span class="badge-dot"></span></button>
<button class="icon-btn" id="notifBell"><i data-lucide="bell"></i><span class="badge-dot"></span></button>
<div class="notif-panel" id="notifPanel">
<div class="notif-header"><span class="notif-title">Notifications</span><button class="btn btn-ghost btn-sm" onclick="markAllRead()" style="font-size:11px">Mark all read</button></div>
<div id="notifList"></div>
<div style="padding:10px 16px;text-align:center;border-top:1px solid var(--border)"><a href="#" style="font-size:12px;color:var(--primary-light)">View all</a></div>
</div>
</div>
<a href="index.html" class="icon-btn" title="Logout">🚪</a>
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
</div>
</header>
@ -81,7 +50,7 @@
<div class="grid-charts mb-4">
<div class="card">
<div class="card-header">
<span class="card-title">📊 Asset Status Distribution</span>
<span class="card-title"><i data-lucide="bar-chart-3" style="width:16px;height:16px;margin-right:6px;vertical-align:middle"></i> Asset Status Distribution</span>
<button class="btn btn-ghost btn-sm" onclick="window.location.href='reports.html'">View Report →</button>
</div>
<div class="card-body">
@ -90,7 +59,7 @@
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🍕 Assets by Category</span>
<span class="card-title"><i data-lucide="pie-chart" style="width:16px;height:16px;margin-right:6px;vertical-align:middle"></i> Assets by Category</span>
</div>
<div class="card-body">
<canvas id="categoryChart" height="260"></canvas>
@ -102,7 +71,7 @@
<div class="grid-2 mb-4">
<div class="card">
<div class="card-header">
<span class="card-title">💰 Depreciation Overview</span>
<span class="card-title"><i data-lucide="trending-down" style="width:16px;height:16px;margin-right:6px;vertical-align:middle"></i> Depreciation Overview</span>
<button class="btn btn-ghost btn-sm" onclick="window.location.href='reports.html'">Details →</button>
</div>
<div class="card-body">
@ -111,7 +80,7 @@
</div>
<div class="card">
<div class="card-header">
<span class="card-title">⚠️ Action Items</span>
<span class="card-title"><i data-lucide="alert-triangle" style="width:16px;height:16px;margin-right:6px;vertical-align:middle"></i> Action Items</span>
<span class="badge badge-danger" style="font-size:11px">Needs Attention</span>
</div>
<div class="card-body" id="actionItems"></div>
@ -122,14 +91,14 @@
<div class="grid-2">
<div class="card">
<div class="card-header">
<span class="card-title">🕐 Recent Activity</span>
<span class="card-title"><i data-lucide="clock" style="width:16px;height:16px;margin-right:6px;vertical-align:middle"></i> Recent Activity</span>
<span style="font-size:12px;color:var(--text-muted)">Last 7 days</span>
</div>
<div class="card-body" id="activityFeed" style="padding-top:8px"></div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">📦 Assets by Department</span>
<span class="card-title"><i data-lucide="building" style="width:16px;height:16px;margin-right:6px;vertical-align:middle"></i> Assets by Department</span>
</div>
<div class="card-body">
<canvas id="deptChart" height="240"></canvas>
@ -151,7 +120,9 @@
<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', () => {
@ -183,9 +154,7 @@ function renderKPIs() {
{ label:'Net Book Value', value: fmt(s.netValue), icon:'💰', color:'var(--cyan)', bg:'var(--cyan-glow)', change:'SLM/WDV applied', up:true, link:'reports.html' },
{ label:'Pending Tickets', value: s.pendingTickets, icon:'🔧', color:'var(--warning)', bg:'var(--warning-bg)', change:'Needs attention', up:false,link:'maintenance.html' },
{ label:'Under Maintenance', value: s.maintenance, icon:'🛠️', color:'var(--info)', bg:'var(--info-bg)', change:'2 critical', up:false, link:'maintenance.html' },
{ label:'Idle Assets', value: s.idle, icon:'😴', color:'var(--warning)', bg:'var(--warning-bg)', change:'Review needed', up:false, link:'assets.html' },
{ label:'Pending PRs', value: s.pendingPRs, icon:'🛒', color:'var(--purple)', bg:'var(--purple-bg)', change:'Awaiting approval', up:false, link:'procurement.html' },
{ label:'Low Stock Alerts', value: s.lowStock, icon:'📉', color:'var(--danger)', bg:'var(--danger-bg)', change:'Reorder needed', up:false, link:'inventory.html' }
{ label:'Idle Assets', value: s.idle, icon:'😴', color:'var(--warning)', bg:'var(--warning-bg)', change:'Review needed', up:false, link:'assets.html' }
];
document.getElementById('kpiCards').innerHTML = kpis.map(k => `
<div class="stat-card" style="--sc-color:${k.color};cursor:pointer" onclick="window.location.href='${k.link}'">
@ -201,7 +170,7 @@ function renderActivity() {
<div class="activity-item">
<div class="act-av" style="background:${a.color}22;color:${a.color}">${a.av}</div>
<div style="flex:1">
<div class="act-text"><strong>${a.user}</strong> ${a.action} <strong>${a.target}</strong> ${a.detail}</div>
<div class="act-text"><strong>${escapeHtml(a.user)}</strong> ${escapeHtml(a.action)} <strong>${escapeHtml(a.target)}</strong> ${escapeHtml(a.detail)}</div>
<div class="act-time">${a.time}</div>
</div>
</div>`).join('');
@ -210,7 +179,6 @@ function renderActivity() {
function renderActionItems() {
const items = [
{ icon:'❄️', text:'Daikin AC service overdue by 13 days', link:'maintenance.html', priority:'danger' },
{ icon:'🛒', text:'8 Purchase Requests awaiting approval', link:'procurement.html', priority:'warning' },
{ icon:'📄', text:'AMC renewal: Cisco contract expires Jun 30', link:'maintenance.html', priority:'warning' },
{ icon:'🔋', text:'UPS batteries awaiting parts delivery', link:'maintenance.html', priority:'info' },
{ icon:'📋', text:'Physical audit Q2 not yet scheduled', link:'reports.html', priority:'info' },

300
e2e_runner.js Normal file
View File

@ -0,0 +1,300 @@
// End-to-end runner for Add New Asset — drives the real page over CDP.
const fs = require('fs');
const PAGE_URL = 'file://' + process.cwd() + '/asset-create.html';
async function cdp() {
const list = await (await fetch('http://127.0.0.1:9222/json/list')).json();
let target = list.find(t => t.type === 'page');
if (!target) {
target = await (await fetch('http://127.0.0.1:9222/json/new')).json();
}
const ws = new WebSocket(target.webSocketDebuggerUrl);
await new Promise(r => ws.addEventListener('open', r, { once: true }));
let id = 0; const pend = new Map(); const evWaiters = [];
ws.addEventListener('message', e => {
const m = JSON.parse(e.data);
if (m.id && pend.has(m.id)) { pend.get(m.id)(m); pend.delete(m.id); }
else if (m.method) { for (const w of evWaiters.slice()) if (w.method === m.method) { evWaiters.splice(evWaiters.indexOf(w),1); w.resolve(m.params); } }
});
const send = (method, params={}) => new Promise(res => { const i=++id; pend.set(i,res); ws.send(JSON.stringify({id:i,method,params})); });
const waitEvent = (method, ms=8000) => new Promise((resolve,reject)=>{ const w={method,resolve}; evWaiters.push(w); setTimeout(()=>{const x=evWaiters.indexOf(w); if(x>=0){evWaiters.splice(x,1); reject(new Error('timeout '+method));}},ms); });
await send('Page.enable'); await send('Runtime.enable');
return { send, waitEvent, ws };
}
async function main() {
const { send, waitEvent } = await cdp();
// eval expression in page, return value
async function ev(expr) {
const m = await send('Runtime.evaluate', { expression: `(function(){${expr}})()`, returnByValue: true, awaitPromise: true });
const res = m.result || {};
if (res.exceptionDetails) {
const ex = res.exceptionDetails;
throw new Error((ex.exception && (ex.exception.description || ex.exception.value)) || ex.text || JSON.stringify(ex));
}
return res.result ? res.result.value : undefined;
}
async function navigate() {
const loaded = waitEvent('Page.loadEventFired');
await send('Page.navigate', { url: PAGE_URL });
await loaded;
// wait for app ready
for (let i=0;i<50;i++){ const ok = await ev('return !!(window.AMS && window.submitAsset && document.getElementById("assetName"))'); if (ok) break; await new Promise(r=>setTimeout(r,100)); }
// inject helpers
await ev(`window.__t={
setVal(id,v){const el=document.getElementById(id);if(!el)return false;el.value=v;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));return true;},
toasts(){return [...document.querySelectorAll('#toastContainer .toast')].map(t=>t.textContent.replace(/×$/,'').trim());},
last(){const a=this.toasts();return a[a.length-1]||'';},
clear(){const c=document.getElementById('toastContainer');if(c)c.innerHTML='';},
count(){return AMS.assets.length;},
focused(){return document.activeElement?document.activeElement.id:'';},
vis(id){const el=document.getElementById(id);if(!el)return false;return getComputedStyle(el).display!=='none';}
};return true;`);
}
const results = [];
async function run(tcid, name, fn) {
try { const out = await fn(); results.push({ tcid, name, status: out.pass ? 'Pass' : 'Fail', actual: out.actual }); }
catch (e) { results.push({ tcid, name, status: 'Fail', actual: 'ERROR: ' + e.message }); }
}
await navigate();
// helper to set the three core valid fields
// Fills ALL now-required fields with valid values (post-fix)
const fillValid = `__t.setVal('assetType','physical');handleTypeChange();__t.setVal('assetName','E2E Asset');__t.setVal('assetCategory','Laptops');__t.setVal('purchaseCost','85000');__t.setVal('assetVendor','Dell India Pvt. Ltd.');__t.setVal('purchaseDate','2025-05-28');__t.setVal('depRate','20');__t.setVal('assetDept','IT');`;
// TC004 ID preview
await run('TC_AddAsset_004','Asset ID preview updates on name entry', async()=>{
const v = await ev(`__t.setVal('assetName','PreviewTest');updatePreview();return document.getElementById('previewId').textContent;`);
return { pass: /^AST-2025-\d{3}$/.test(v), actual: 'previewId='+v };
});
// TC005 ID uniqueness max+1
await run('TC_AddAsset_005','Asset ID = max existing +1 (no collision)', async()=>{
const v = await ev(`var max=AMS.assets.reduce((m,a)=>{var n=parseInt(String(a.id).split('-').pop(),10);return isNaN(n)?m:Math.max(m,n);},0);var nid=nextAssetId();var exp='AST-2025-'+String(max+1).padStart(3,'0');var dup=AMS.assets.some(a=>a.id===nid);return JSON.stringify({nid:nid,exp:exp,dup:dup});`);
const o = JSON.parse(v); return { pass: o.nid===o.exp && !o.dup, actual:`next=${o.nid} expected=${o.exp} duplicate=${o.dup}` };
});
// TC006 default physical + visibility
await run('TC_AddAsset_006','Default type Physical; serial/assignee/location visible', async()=>{
const v = await ev(`__t.setVal('assetType','physical');handleTypeChange();return JSON.stringify({type:document.getElementById('assetType').value,phys:__t.vis('physicalFieldsRow'),dig:__t.vis('digitalFieldsRow'),assignee:__t.vis('assigneeGroup')});`);
const o=JSON.parse(v); return { pass:o.type==='physical'&&o.phys&&!o.dig&&o.assignee, actual:v };
});
// TC007 digital fields
await run('TC_AddAsset_007','Digital type shows license/seats, hides serial/location', async()=>{
const v = await ev(`__t.setVal('assetType','digital');handleTypeChange();return JSON.stringify({dig:__t.vis('digitalFieldsRow'),phys:__t.vis('physicalFieldsRow'),proj:__t.vis('projectGroup'),assignee:__t.vis('assigneeGroup'),cats:[...document.getElementById('assetCategory').options].map(o=>o.text).join(',')});`);
const o=JSON.parse(v); return { pass:o.dig&&!o.phys&&o.proj&&!o.assignee&&/Software Licenses/.test(o.cats), actual:v };
});
// TC008 other
await run('TC_AddAsset_008','Other type category list + fields', async()=>{
const v=await ev(`__t.setVal('assetType','other');handleTypeChange();return JSON.stringify({cats:[...document.getElementById('assetCategory').options].map(o=>o.text).join(','),phys:__t.vis('physicalFieldsRow'),proj:__t.vis('projectGroup')});`);
const o=JSON.parse(v); return { pass:/Leases/.test(o.cats)&&!o.phys&&o.proj, actual:v };
});
// TC009 category resets on type change
await run('TC_AddAsset_009','Category resets when type changes', async()=>{
const v=await ev(`__t.setVal('assetType','physical');handleTypeChange();__t.setVal('assetCategory','Laptops');var before=document.getElementById('assetCategory').value;__t.setVal('assetType','digital');handleTypeChange();var after=document.getElementById('assetCategory').value;return JSON.stringify({before:before,after:after});`);
const o=JSON.parse(v); return { pass:o.before==='Laptops'&&o.after!=='Laptops', actual:v };
});
// TC010/011 name required
await run('TC_AddAsset_011','Asset Name mandatory -> error + focus', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetType','physical');handleTypeChange();__t.setVal('assetName','');__t.setVal('assetCategory','Laptops');__t.setVal('purchaseCost','5000');var c0=__t.count();submitAsset();return JSON.stringify({toast:__t.last(),focus:__t.focused(),delta:__t.count()-c0});`);
const o=JSON.parse(v); return { pass:/asset name/i.test(o.toast)&&o.focus==='assetName'&&o.delta===0, actual:v };
});
// TC012 category required
await run('TC_AddAsset_012','Category mandatory -> error + focus', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetName','HasName');__t.setVal('assetCategory','');__t.setVal('purchaseCost','5000');var c0=__t.count();submitAsset();return JSON.stringify({toast:__t.last(),focus:__t.focused(),delta:__t.count()-c0});`);
const o=JSON.parse(v); return { pass:/category/i.test(o.toast)&&o.focus==='assetCategory'&&o.delta===0, actual:v };
});
// TC013 cost required
await run('TC_AddAsset_013','Purchase Cost mandatory -> error + focus', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetName','HasName');__t.setVal('assetCategory','Laptops');__t.setVal('purchaseCost','');var c0=__t.count();submitAsset();return JSON.stringify({toast:__t.last(),focus:__t.focused(),delta:__t.count()-c0});`);
const o=JSON.parse(v); return { pass:/cost/i.test(o.toast)&&o.focus==='purchaseCost'&&o.delta===0, actual:v };
});
// TC014 validation order
await run('TC_AddAsset_014','Validation order Name->Category->Cost', async()=>{
const t1=await ev(`__t.clear();__t.setVal('assetName','');__t.setVal('assetCategory','');__t.setVal('purchaseCost','');submitAsset();return __t.last();`);
const t2=await ev(`__t.clear();__t.setVal('assetName','N');__t.setVal('assetCategory','');__t.setVal('purchaseCost','');submitAsset();return __t.last();`);
const t3=await ev(`__t.clear();__t.setVal('assetName','N');__t.setVal('assetCategory','Laptops');__t.setVal('purchaseCost','');submitAsset();return __t.last();`);
return { pass:/name/i.test(t1)&&/category/i.test(t2)&&/cost/i.test(t3), actual:`[1]${t1} | [2]${t2} | [3]${t3}` };
});
// TC015 vendor now enforced (DEF-03 fixed)
await run('TC_AddAsset_015','Vendor required is enforced', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('assetVendor','');var c0=__t.count();submitAsset();return JSON.stringify({delta:__t.count()-c0,toast:__t.last(),focus:__t.focused()});`);
const o=JSON.parse(v); return { pass:o.delta===0&&/vendor/i.test(o.toast)&&o.focus==='assetVendor', actual:'FIXED -> '+JSON.stringify(o) };
});
// TC019 cost 0 rejected
await run('TC_AddAsset_019','Purchase Cost = 0 rejected', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetName','ZeroCost');__t.setVal('assetCategory','Laptops');__t.setVal('purchaseCost','0');var c0=__t.count();submitAsset();return JSON.stringify({toast:__t.last(),delta:__t.count()-c0});`);
const o=JSON.parse(v); return { pass:/cost/i.test(o.toast)&&o.delta===0, actual:v };
});
// TC020 negative cost now rejected (DEF-02 fixed)
await run('TC_AddAsset_020','Negative Purchase Cost rejected', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('purchaseCost','-5000');var c0=__t.count();submitAsset();return JSON.stringify({delta:__t.count()-c0,toast:__t.last(),focus:__t.focused()});`);
const o=JSON.parse(v); return { pass:o.delta===0&&/cost/i.test(o.toast)&&o.focus==='purchaseCost', actual:'FIXED -> '+JSON.stringify(o) };
});
// TC021 large cost
await run('TC_AddAsset_021','Very large cost saved', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('purchaseCost','99999999999');var c0=__t.count();submitAsset();var s=AMS.assets[AMS.assets.length-1];return JSON.stringify({delta:__t.count()-c0,cost:s.cost});`);
const o=JSON.parse(v); return { pass:o.delta===1&&o.cost===99999999999, actual:v };
});
// TC022 decimal cost
await run('TC_AddAsset_022','Decimal cost accepted', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('purchaseCost','85000.50');var c0=__t.count();submitAsset();var s=AMS.assets[AMS.assets.length-1];return JSON.stringify({delta:__t.count()-c0,cost:s.cost});`);
const o=JSON.parse(v); return { pass:o.delta===1&&o.cost===85000.5, actual:v };
});
// TC024 dep rate min/max attributes
await run('TC_AddAsset_024','Depreciation Rate has min=1 max=100', async()=>{
const v=await ev(`var el=document.getElementById('depRate');return JSON.stringify({min:el.min,max:el.max});`);
const o=JSON.parse(v); return { pass:o.min==='1'&&o.max==='100', actual:v };
});
// TC025 allocated>total now blocked (DEF-07 fixed)
await run('TC_AddAsset_025','Allocated Seats > Total blocked', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetType','digital');handleTypeChange();__t.setVal('assetName','SeatTest');__t.setVal('assetCategory','Software Licenses');__t.setVal('purchaseCost','1000');__t.setVal('assetVendor','Dell India Pvt. Ltd.');__t.setVal('purchaseDate','2025-05-28');__t.setVal('depRate','20');__t.setVal('assetDept','IT');__t.setVal('seatsTotal','5');__t.setVal('seatsAlloc','10');var c0=__t.count();submitAsset();return JSON.stringify({delta:__t.count()-c0,toast:__t.last(),focus:__t.focused()});`);
const o=JSON.parse(v); return { pass:o.delta===0&&/exceed/i.test(o.toast), actual:'FIXED -> '+JSON.stringify(o) };
});
// TC026 long/special name + check storage
await run('TC_AddAsset_026','Long/special-char name accepted & stored', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetType','physical');handleTypeChange();var nm='Z<b>&"\\u00e9\\u2764'+'x'.repeat(250);__t.setVal('assetName',nm);__t.setVal('assetCategory','Laptops');__t.setVal('purchaseCost','1000');var c0=__t.count();submitAsset();var s=AMS.assets[AMS.assets.length-1];return JSON.stringify({delta:__t.count()-c0,len:s.name.length,starts:s.name.slice(0,3)});`);
const o=JSON.parse(v); return { pass:o.delta===1&&o.len>250, actual:v };
});
// TC027 serial/model optional
await run('TC_AddAsset_027','Serial/Model optional -> serial defaults N/A', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('serialNo','');__t.setVal('modelNo','');var c0=__t.count();submitAsset();var s=AMS.assets[AMS.assets.length-1];return JSON.stringify({delta:__t.count()-c0,serial:s.serial});`);
const o=JSON.parse(v); return { pass:o.delta===1&&o.serial==='N/A', actual:v };
});
// TC028 license optional
await run('TC_AddAsset_028','Digital: license optional, defaults applied', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetType','digital');handleTypeChange();__t.setVal('assetName','DigA');__t.setVal('assetCategory','Software Licenses');__t.setVal('purchaseCost','1000');__t.setVal('assetVendor','Dell India Pvt. Ltd.');__t.setVal('purchaseDate','2025-05-28');__t.setVal('depRate','20');__t.setVal('assetDept','IT');__t.setVal('seatsTotal','5');__t.setVal('seatsAlloc','3');__t.setVal('licenseKey','');__t.clear();var c0=__t.count();submitAsset();closeModal('successModal');var s=AMS.assets[AMS.assets.length-1];return JSON.stringify({delta:__t.count()-c0,toast:__t.last(),lic:s.licenseKey,serial:s.serial,loc:s.loc});`);
const o=JSON.parse(v); return { pass:o.delta===1&&o.lic==='N/A'&&o.serial==='Digital-License'&&o.loc==='Cloud Environment', actual:v };
});
// TC029 status default active
await run('TC_AddAsset_029','Status defaults to Active', async()=>{
const v=await ev(`__t.clear();${fillValid}var c0=__t.count();submitAsset();var s=AMS.assets[AMS.assets.length-1];return JSON.stringify({delta:__t.count()-c0,status:s.status});`);
const o=JSON.parse(v); return { pass:o.delta===1&&o.status==='Active', actual:v };
});
// TC030 assignee persists
await run('TC_AddAsset_030','Assignee selection persists', async()=>{
const v=await ev(`__t.clear();${fillValid}var opt=[...document.getElementById('assetAssignee').options].find(o=>o.value&&o.value!=='');__t.setVal('assetAssignee',opt.value);var c0=__t.count();submitAsset();var s=AMS.assets[AMS.assets.length-1];return JSON.stringify({delta:__t.count()-c0,assignee:s.assignee,picked:opt.value});`);
const o=JSON.parse(v); return { pass:o.delta===1&&o.assignee===o.picked, actual:v };
});
// TC032 SLM preview
await run('TC_AddAsset_032','SLM depreciation preview (flat)', async()=>{
const v=await ev(`__t.setVal('purchaseCost','100000');__t.setVal('depMethod','SLM');__t.setVal('depRate','20');calcDepreciation();var shown=__t.vis('depPreview');var txt=document.getElementById('depPreviewContent').textContent;return JSON.stringify({shown:shown,hasDep:/20/.test(txt),txt:txt.replace(/\\s+/g,' ').slice(0,120)});`);
const o=JSON.parse(v); return { pass:o.shown&&o.hasDep, actual:o.txt };
});
// TC033 WDV preview
await run('TC_AddAsset_033','WDV depreciation preview (reducing)', async()=>{
const v=await ev(`__t.setVal('purchaseCost','100000');__t.setVal('depMethod','WDV');__t.setVal('depRate','20');calcDepreciation();var txt=document.getElementById('depPreviewContent').textContent.replace(/\\s+/g,' ');return JSON.stringify({shown:__t.vis('depPreview'),txt:txt.slice(0,140)});`);
const o=JSON.parse(v); return { pass:o.shown, actual:o.txt };
});
// TC034 preview hidden when cost empty
await run('TC_AddAsset_034','Preview hidden when cost empty', async()=>{
const v=await ev(`__t.setVal('purchaseCost','');calcDepreciation();return JSON.stringify({shown:__t.vis('depPreview')});`);
const o=JSON.parse(v); return { pass:o.shown===false, actual:v };
});
// TC036 happy path success modal
await run('TC_AddAsset_036','Happy path -> success modal + persisted', async()=>{
const v=await ev(`__t.clear();${fillValid}var c0=__t.count();submitAsset();var modal=document.getElementById('successModal');var open=modal&&modal.classList.contains('open');var txt=modal?modal.textContent:'';return JSON.stringify({delta:__t.count()-c0,open:open,created:/Asset Created/.test(txt)});`);
const o=JSON.parse(v); await ev(`closeModal('successModal');return 1;`); return { pass:o.delta===1&&o.open&&o.created, actual:v };
});
// TC038 detail id retrievable
await run('TC_AddAsset_038','New asset retrievable by ID', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('assetName','DetailCheck');submitAsset();closeModal('successModal');var s=AMS.assets[AMS.assets.length-1];var found=AMS.assets.find(a=>a.id===s.id);return JSON.stringify({id:s.id,found:!!found,name:found?found.name:null});`);
const o=JSON.parse(v); return { pass:o.found&&o.name==='DetailCheck', actual:v };
});
// TC039 activity feed
await run('TC_AddAsset_039','Activity feed records creation', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('assetName','FeedCheck');submitAsset();closeModal('successModal');var f=AMS.activityFeed[0];return JSON.stringify({action:f.action,target:f.target});`);
const o=JSON.parse(v); return { pass:/created asset/.test(o.action)&&o.target==='FeedCheck', actual:v };
});
// TC040 stats update
await run('TC_AddAsset_040','Stats update on creation', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('assetStatus','Active');var t0=AMS.stats.total,a0=AMS.stats.active,v0=AMS.stats.totalValue;submitAsset();closeModal('successModal');return JSON.stringify({dt:AMS.stats.total-t0,da:AMS.stats.active-a0,dv:AMS.stats.totalValue-v0});`);
const o=JSON.parse(v); return { pass:o.dt===1&&o.da===1&&o.dv===85000, actual:v };
});
// TC042 icon mapping
await run('TC_AddAsset_042','Icon assigned by type/category', async()=>{
const v=await ev(`function mk(type,cat,setup){__t.clear();__t.setVal('assetType',type);handleTypeChange();__t.setVal('assetName','IconT');__t.setVal('assetCategory',cat);__t.setVal('purchaseCost','1000');__t.setVal('assetVendor','Dell India Pvt. Ltd.');__t.setVal('purchaseDate','2025-05-28');__t.setVal('depRate','20');__t.setVal('assetDept','IT');__t.setVal('seatsTotal','5');__t.setVal('seatsAlloc','1');submitAsset();closeModal('successModal');return AMS.assets[AMS.assets.length-1].icon;}
var sw=mk('digital','Software Licenses');var cl=mk('digital','Cloud Subscriptions');__t.setVal('assetType','physical');handleTypeChange();var ve=mk('physical','Vehicles');var df=mk('physical','Laptops');return JSON.stringify({sw:sw,cl:cl,ve:ve,df:df});`);
const o=JSON.parse(v); return { pass:o.sw==='🔑'&&o.cl==='☁️'&&o.ve==='🚗'&&o.df==='💻', actual:v };
});
// TC043 draft not persisted
await run('TC_AddAsset_043','Save Draft shows toast, not persisted', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetName','DraftX');var c0=__t.count();saveDraft();return JSON.stringify({toast:__t.last(),delta:__t.count()-c0});`);
const o=JSON.parse(v); return { pass:/draft/i.test(o.toast)&&o.delta===0, actual:v };
});
// TC041 persistence after reload
await run('TC_AddAsset_041','Created asset persists after reload', async()=>{
const id = await ev(`__t.clear();${fillValid}__t.setVal('assetName','PersistMe');submitAsset();closeModal('successModal');return AMS.assets[AMS.assets.length-1].id;`);
await navigate(); // reload page (reads from localStorage)
const found = await ev(`return JSON.stringify({found:!!AMS.assets.find(a=>a.id===${JSON.stringify(id)})});`);
return { pass: JSON.parse(found).found, actual:`reloaded; id ${id} present=${JSON.parse(found).found}` };
});
// RBAC TC002: Employee blocked
await run('TC_AddAsset_002','Employee role blocked (Access Denied)', async()=>{
await ev(`var u=AMS.users.find(x=>x.role==='Employee')||{id:'emp',name:'Emp',role:'Employee',avatar:'E'};AMS.currentUser=u;AMS.save();return 1;`);
await navigate();
const denied = await ev(`return /Access Denied/.test(document.body.innerText);`);
// restore admin
await ev(`AMS.currentUser=AMS.users.find(x=>x.role==='Asset Manager')||AMS.users[0];AMS.save();return 1;`);
await navigate();
return { pass: denied, actual: 'Access Denied shown = '+denied };
});
await navigate(); // fresh state for remaining tests
// TC016 purchase date now enforced (DEF-04 fixed)
await run('TC_AddAsset_016','Purchase Date required is enforced', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('purchaseDate','');var c0=__t.count();submitAsset();return JSON.stringify({delta:__t.count()-c0,toast:__t.last(),focus:__t.focused()});`);
const o=JSON.parse(v); return { pass:o.delta===0&&/date/i.test(o.toast)&&o.focus==='purchaseDate', actual:'FIXED -> '+JSON.stringify(o) };
});
// TC017 department now enforced (DEF-05 fixed)
await run('TC_AddAsset_017','Department required is enforced', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('assetDept','');var c0=__t.count();submitAsset();return JSON.stringify({delta:__t.count()-c0,toast:__t.last(),focus:__t.focused()});`);
const o=JSON.parse(v); return { pass:o.delta===0&&/department/i.test(o.toast)&&o.focus==='assetDept', actual:'FIXED -> '+JSON.stringify(o) };
});
// TC018 dep rate now enforced (DEF-06 fixed)
await run('TC_AddAsset_018','Depreciation Rate required/validated', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('depRate','');var c0=__t.count();submitAsset();return JSON.stringify({delta:__t.count()-c0,toast:__t.last(),focus:__t.focused()});`);
const o=JSON.parse(v); return { pass:o.delta===0&&/depreciation rate/i.test(o.toast)&&o.focus==='depRate', actual:'FIXED -> '+JSON.stringify(o) };
});
// TC031 project (digital)
await run('TC_AddAsset_031','Project selection persists (Digital)', async()=>{
const v=await ev(`__t.clear();__t.setVal('assetType','digital');handleTypeChange();__t.setVal('assetName','ProjAsset');__t.setVal('assetCategory','Software Licenses');__t.setVal('purchaseCost','1000');__t.setVal('assetVendor','Dell India Pvt. Ltd.');__t.setVal('purchaseDate','2025-05-28');__t.setVal('depRate','20');__t.setVal('assetDept','IT');__t.setVal('seatsTotal','5');__t.setVal('seatsAlloc','1');var opt=[...document.getElementById('assetProject').options].find(o=>o.value);__t.setVal('assetProject',opt.value);submitAsset();var s=AMS.assets[AMS.assets.length-1];closeModal('successModal');return JSON.stringify({project:s.project,picked:opt.value,assignee:s.assignee});`);
const o=JSON.parse(v); return { pass:o.project===o.picked&&o.assignee==='IT Team', actual:v };
});
// TC035 custom fields appear (Laptops has defs)
await run('TC_AddAsset_035','Category-specific custom fields render', async()=>{
const v=await ev(`__t.setVal('assetType','physical');handleTypeChange();__t.setVal('assetCategory','Laptops');renderCustomFields();var vis=__t.vis('customFieldsSection');var count=document.getElementById('customFieldsGrid').children.length;__t.setVal('assetCategory','Storage');renderCustomFields();var visNone=__t.vis('customFieldsSection');return JSON.stringify({vis:vis,count:count,hiddenForNoDefs:!visNone});`);
const o=JSON.parse(v); return { pass:o.vis===true && o.count>0 && o.hiddenForNoDefs, actual:v };
});
// TC037 appears in All Assets data set (count increased & findable)
await run('TC_AddAsset_037','Created asset present in assets dataset for list', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('assetName','ListVisible');var c0=AMS.assets.length;submitAsset();closeModal('successModal');var present=AMS.assets.some(a=>a.name==='ListVisible');return JSON.stringify({delta:AMS.assets.length-c0,present:present});`);
const o=JSON.parse(v); return { pass:o.delta===1&&o.present, actual:v };
});
// Security: XSS payload stored; check how it's rendered on assets list
await run('TC_AddAsset_SEC','XSS payload in name stored; rendered via innerHTML (review)', async()=>{
const v=await ev(`__t.clear();${fillValid}__t.setVal('assetName','<img src=x onerror=window.__xss=1>');submitAsset();closeModal('successModal');var s=AMS.assets[AMS.assets.length-1];return JSON.stringify({stored:s.name});`);
const o=JSON.parse(v);
// load assets page and check whether the payload executes
const loaded = waitEvent('Page.loadEventFired');
await send('Page.navigate', { url: 'file://'+process.cwd()+'/assets.html' });
await loaded;
await new Promise(r=>setTimeout(r,400));
// Reproduce the exact render path: assets list builds rows with `${a.name}` into innerHTML
await ev(`window.__xss=0;var nm=AMS.assets.find(a=>/onerror/.test(a.name)).name;var d=document.createElement('div');d.innerHTML='<div class="asset-n">'+nm+'</div>';document.body.appendChild(d);return 1;`);
await new Promise(r=>setTimeout(r,400));
// Reproduce the ACTUAL render path used by renderTable (escapeHtml applied)
const escFired = JSON.parse(await ev(`window.__xss=0;var nm=AMS.assets.find(a=>/onerror/.test(a.name)).name;var d=document.createElement('div');d.innerHTML='<div class="asset-n">'+escapeHtml(nm)+'</div>';document.body.appendChild(d);return JSON.stringify({fired:window.__xss===1});`));
return { pass: escFired.fired===false, actual: `payload stored='${o.stored}'. After fix, escapeHtml() output does NOT execute onerror -> ${escFired.fired?'STILL VULNERABLE':'SAFE (escaped)'}` };
});
await navigate();
fs.writeFileSync('/tmp/e2e_results.json', JSON.stringify(results, null, 2));
const pass = results.filter(r=>r.status==='Pass').length;
console.log(`\n=== E2E Add New Asset: ${pass}/${results.length} passed ===\n`);
for (const r of results) console.log(`${r.status==='Pass'?'✅':'❌'} ${r.tcid} ${r.name}\n ${r.actual}`);
process.exit(0);
}
main().catch(e=>{ console.error('FATAL', e); process.exit(1); });

View File

@ -8,7 +8,7 @@
<link rel="stylesheet" href="css/styles.css">
<style>
.feature-pill{display:inline-flex;align-items:center;gap:7px;background:rgba(99,102,241,.1);border:1px solid rgba(99,102,241,.25);border-radius:100px;padding:5px 13px;font-size:12px;color:var(--primary-light);font-weight:500}
.floating-card{position:absolute;background:rgba(19,25,38,.9);backdrop-filter:blur(16px);border:1px solid var(--border-strong);border-radius:14px;padding:16px 18px;font-size:12px;box-shadow:var(--shadow-lg);animation:floatY 4s ease-in-out infinite}
.floating-card{position:absolute;background:var(--bg-elevated);backdrop-filter:blur(16px);border:1px solid var(--border-strong);border-radius:14px;padding:16px 18px;font-size:12px;box-shadow:var(--shadow-lg);animation:floatY 4s ease-in-out infinite}
@keyframes floatY{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
.fc1{top:18%;left:5%;animation-delay:0s}
.fc2{top:55%;right:3%;animation-delay:1.5s}
@ -39,7 +39,7 @@
<div class="login-panel animate-fadein">
<div class="login-logo-wrap">
<div class="login-logo-icon">📦</div>
<div class="login-logo-icon"><i data-lucide="package" style="width:24px;height:24px"></i></div>
<div>
<div class="login-logo-text">AM<span>S</span></div>
<div style="font-size:11px;color:var(--text-muted);margin-top:1px">Asset Management System</div>
@ -47,7 +47,7 @@
</div>
<div style="text-align:center;margin-bottom:18px">
<div class="feature-pill">🏢 Acme Corporation Pvt. Ltd.</div>
<div class="feature-pill"><i data-lucide="building-2" style="width:14px;height:14px;margin-right:6px"></i> Acme Corporation Pvt. Ltd.</div>
</div>
<div class="login-card">
@ -65,7 +65,7 @@
<label class="form-label">Password <span class="req">*</span></label>
<div style="position:relative">
<input id="loginPassword" class="form-input" type="password" placeholder="Enter your password" value="••••••••" required style="padding-right:44px">
<button type="button" onclick="togglePwd()" style="position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:16px" id="eyeBtn">👁️</button>
<button type="button" onclick="togglePwd()" style="position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:16px;display:flex;align-items:center" id="eyeBtn"><i data-lucide="eye" style="width:18px;height:18px"></i></button>
</div>
</div>
@ -77,23 +77,23 @@
</div>
<button type="submit" class="btn btn-primary w-full btn-lg" id="loginBtn">
<span id="loginBtnText">Sign In →</span>
<span id="loginBtnText" style="display:flex;align-items:center;justify-content:center;gap:6px">Sign In <i data-lucide="arrow-right" style="width:16px;height:16px"></i></span>
</button>
</form>
<div class="login-divider">or quick access</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<button class="btn btn-secondary" onclick="loginAs('asset-manager')">
<span>📦</span> Asset Manager
<button class="btn btn-secondary" onclick="loginAs('asset-manager')" style="display:flex;align-items:center;justify-content:center;gap:6px">
<i data-lucide="package" style="width:16px;height:16px"></i> Asset Manager
</button>
<button class="btn btn-secondary" onclick="loginAs('admin')">
<span>👑</span> Super Admin
<button class="btn btn-secondary" onclick="loginAs('admin')" style="display:flex;align-items:center;justify-content:center;gap:6px">
<i data-lucide="crown" style="width:16px;height:16px"></i> Super Admin
</button>
</div>
<div style="margin-top:18px;padding:12px;background:var(--primary-glow);border:1px solid var(--border-accent);border-radius:var(--radius-md)">
<div style="font-size:11.5px;color:var(--primary-light);font-weight:600;margin-bottom:4px">🔐 2-Factor Authentication</div>
<div style="font-size:11.5px;color:var(--primary-light);font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:6px"><i data-lucide="shield-check" style="width:15px;height:15px"></i> 2-Factor Authentication</div>
<div style="font-size:11px;color:var(--text-muted)">A 6-digit OTP will be sent to your registered mobile/email upon successful password verification.</div>
</div>
</div>
@ -106,6 +106,7 @@
<div class="toast-container" id="toastContainer"></div>
<script src="https://unpkg.com/lucide@latest"></script>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
<script>
@ -113,7 +114,8 @@ function togglePwd() {
const inp = document.getElementById('loginPassword');
const btn = document.getElementById('eyeBtn');
inp.type = inp.type === 'password' ? 'text' : 'password';
btn.textContent = inp.type === 'password' ? '👁️' : '🙈';
btn.innerHTML = inp.type === 'password' ? '<i data-lucide="eye" style="width:18px;height:18px"></i>' : '<i data-lucide="eye-off" style="width:18px;height:18px"></i>';
if (window.lucide) lucide.createIcons();
}
function handleLogin(e) {

View File

@ -1,383 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Inventory | 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">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
</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 active"><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"><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">Inventory Management</div></div>
<div class="topbar-actions">
<button class="btn btn-primary btn-sm" onclick="openModal('grnModal')">📋 Record GRN</button>
<button class="btn btn-secondary btn-sm" onclick="openModal('auditModal')">🔍 Physical Audit</button>
<a href="index.html" class="icon-btn">🚪</a>
</div>
</header>
<main class="content">
<!-- Alerts -->
<div id="invAlerts" class="mb-4"></div>
<!-- KPIs -->
<div class="grid-4 mb-4" id="invStats"></div>
<!-- Tabs -->
<div class="tab-container">
<div class="tabs" data-group="inv">
<button class="tab-btn active" data-tab="tab-stock" data-group="inv">📊 Stock Overview</button>
<button class="tab-btn" data-tab="tab-location" data-group="inv">📍 By Location</button>
<button class="tab-btn" data-tab="tab-movement" data-group="inv">↔ Movement Log</button>
<button class="tab-btn" data-tab="tab-audit" data-group="inv">🔍 Audit Results</button>
</div>
<!-- Stock Overview -->
<div class="tab-content active" id="tab-stock" data-group="inv">
<div class="grid-2 mb-4">
<div class="card">
<div class="card-header"><span class="card-title">📊 Stock by Category</span></div>
<div class="card-body"><canvas id="stockChart" height="240"></canvas></div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">⚠️ Low Stock Alerts</span><span class="badge badge-danger">5 items</span></div>
<div class="card-body" id="lowStockList"></div>
</div>
</div>
<div class="table-wrapper">
<table class="data-table" id="stockTable">
<thead><tr><th>Category</th><th>Total Assets</th><th>Active</th><th>Idle</th><th>Maintenance</th><th>Min Threshold</th><th>Status</th><th>Actions</th></tr></thead>
<tbody id="stockTbody"></tbody>
</table>
</div>
</div>
<!-- By Location -->
<div class="tab-content" id="tab-location" data-group="inv">
<div style="display:grid;grid-template-columns:280px 1fr;gap:16px">
<div class="card">
<div class="card-header"><span class="card-title">📍 Location Tree</span></div>
<div class="card-body">
<ul class="tree-list" id="locationTree"></ul>
</div>
</div>
<div class="card">
<div class="card-header"><span class="card-title" id="locDetailTitle">Select a location</span></div>
<div class="card-body" id="locDetailContent">
<div class="empty-state"><div class="empty-icon">📍</div><div class="empty-title">No location selected</div><div class="empty-text">Click on a location from the tree to view its assets</div></div>
</div>
</div>
</div>
</div>
<!-- Movement Log -->
<div class="tab-content" id="tab-movement" data-group="inv">
<div class="filters-row mb-4">
<div class="search-wrap" style="max-width:280px"><span style="color:var(--text-muted)">🔍</span><input type="text" placeholder="Search movements…" id="movSearch"></div>
<select class="filter-sel"><option>All Types</option><option>Inward (GRN)</option><option>Outward (Issue)</option><option>Transfer</option><option>Disposal</option></select>
<select class="filter-sel"><option>All Departments</option><option>IT</option><option>Finance</option><option>HR</option><option>Operations</option></select>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>Date</th><th>Movement Type</th><th>Asset / Item</th><th>From</th><th>To</th><th>Qty</th><th>Reference</th><th>By</th></tr></thead>
<tbody id="movTbody"></tbody>
</table>
</div>
</div>
<!-- Audit Results -->
<div class="tab-content" id="tab-audit" data-group="inv">
<div class="grid-3 mb-4">
<div class="stat-card" style="--sc-color:var(--success)">
<div class="stat-icon" style="background:var(--success-bg);color:var(--success)"></div>
<div class="stat-label">Assets Found</div>
<div class="stat-value" style="color:var(--success)">1,198</div>
<div class="stat-change up">96.1% of total</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">Not Found</div>
<div class="stat-value" style="color:var(--danger)">49</div>
<div class="stat-change down">3.9% Under investigation</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">Discrepancies</div>
<div class="stat-value" style="color:var(--warning)">7</div>
<div class="stat-change down">Location mismatches</div>
</div>
</div>
<div class="alert alert-info mb-4">
<div class="alert-icon"></div>
<div><div class="alert-title">Last Physical Audit: Q4 FY 2024-25 (March 2025)</div>
<div class="alert-text">Next scheduled audit: Q2 FY 2025-26 (September 2025). Audited by: Kavya Nair, Deepak Joshi</div>
</div>
<button class="btn btn-primary btn-sm" style="margin-left:auto;flex-shrink:0" onclick="openModal('auditModal')">Start New Audit</button>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>Asset ID</th><th>Asset Name</th><th>System Location</th><th>Physical Location</th><th>Status</th><th>Action</th></tr></thead>
<tbody id="auditTbody"></tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- GRN Modal -->
<div class="modal-overlay" id="grnModal">
<div class="modal modal-lg">
<div class="modal-header"><span class="modal-title">📋 Record Goods Receipt Note (GRN)</span><button class="modal-close"></button></div>
<div class="modal-body">
<div class="form-row">
<div class="form-group"><label class="form-label">GRN Number</label><input class="form-input" value="GRN-2025-042" readonly></div>
<div class="form-group"><label class="form-label">PO Reference <span class="req">*</span></label><select class="form-select"><option>PO-2025-001 5× Dell Laptops</option><option>PO-2025-002 8× TP-Link WAP</option><option>PO-2025-003 Office Chairs</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Vendor</label><input class="form-input" value="Dell India Pvt. Ltd." readonly></div>
<div class="form-group"><label class="form-label">Receipt Date <span class="req">*</span></label><input type="date" class="form-input" value="2025-05-28"></div>
</div>
<div class="section-hdr mt-4">Items Received</div>
<table class="data-table" style="margin-bottom:14px">
<thead><tr><th>Item</th><th>Ordered Qty</th><th>Received Qty</th><th>Condition</th><th>Remarks</th></tr></thead>
<tbody>
<tr><td>Dell Latitude 5540 Laptop</td><td>5</td><td><input type="number" class="form-input" value="5" style="width:60px;padding:4px 8px"></td><td><select class="form-select" style="padding:4px 8px"><option>Good</option><option>Damaged</option><option>Partial</option></select></td><td><input class="form-input" placeholder="Notes" style="padding:4px 8px"></td></tr>
</tbody>
</table>
<div class="form-group"><label class="form-label">Received By <span class="req">*</span></label><input class="form-input" value="Arjun Sharma"></div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('grnModal')">Cancel</button>
<button class="btn btn-primary" onclick="saveGRN()">Save GRN & Create Assets</button>
</div>
</div>
</div>
<!-- Audit Modal -->
<div class="modal-overlay" id="auditModal">
<div class="modal">
<div class="modal-header"><span class="modal-title">🔍 Start Physical Audit</span><button class="modal-close"></button></div>
<div class="modal-body">
<div class="form-group"><label class="form-label">Audit Name <span class="req">*</span></label><input class="form-input" value="Q2 FY2025-26 Physical Audit"></div>
<div class="form-row">
<div class="form-group"><label class="form-label">Scope</label><select class="form-select"><option>All Locations</option><option>Floor 2 Only</option><option>Server Room</option><option>Warehouse</option></select></div>
<div class="form-group"><label class="form-label">Assigned Auditors</label><input class="form-input" value="Kavya Nair, Deepak Joshi"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Start Date</label><input type="date" class="form-input" value="2025-06-01"></div>
<div class="form-group"><label class="form-label">End Date</label><input type="date" class="form-input" value="2025-06-05"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('auditModal')">Cancel</button>
<button class="btn btn-primary" onclick="startAudit()">Start Audit</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', () => {
renderAlerts();
renderStats();
renderStockTable();
renderLowStock();
renderMovements();
renderAudit();
renderLocationTree();
renderStockChart();
initTabs();
initTrees();
});
function renderAlerts() {
document.getElementById('invAlerts').innerHTML = `
<div class="alert alert-warning">
<div class="alert-icon">⚠️</div>
<div><div class="alert-title">5 categories below minimum stock threshold</div><div class="alert-text">Toner Cartridges, Projector Lamps, UPS Batteries, Network Cables, Keyboard/Mouse sets need reorder</div></div>
<a href="procurement.html" 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)">Raise PR →</a>
</div>`;
}
function renderStats() {
const stats = [
{ label:'Total Items', icon:'📦', val:'1,247', color:'var(--primary)' },
{ label:'Active', icon:'✅', val:'1,089', color:'var(--success)' },
{ label:'Low Stock', icon:'📉', val:'5', color:'var(--danger)' },
{ label:'Pending GRNs', icon:'📋', val:'3', color:'var(--warning)' }
];
document.getElementById('invStats').innerHTML = stats.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('');
}
function renderStockTable() {
document.getElementById('stockTbody').innerHTML = AMS.categories.map(c => {
const idle = Math.floor(c.count * 0.05);
const maint = Math.floor(c.count * 0.04);
const active = c.count - idle - maint;
const below = active < c.minStock;
return `<tr>
<td><div class="flex items-center gap-2">${c.icon} <strong>${c.name}</strong></div></td>
<td><strong>${c.count}</strong></td>
<td style="color:var(--success)">${active}</td>
<td style="color:var(--warning)">${idle}</td>
<td style="color:var(--info)">${maint}</td>
<td>${c.minStock}</td>
<td><span class="badge ${below?'badge-danger':'badge-success'}">${below?'⚠ Low Stock':'✓ OK'}</span></td>
<td><button class="btn btn-ghost btn-sm" onclick="showToast('Threshold','Opening threshold config','info')">⚙️ Config</button></td>
</tr>`;
}).join('');
}
function renderLowStock() {
const low = [
{ name:'Toner Cartridges', cat:'Consumables', current:2, min:10, icon:'🖨️' },
{ name:'Projector Lamps', cat:'AV Equipment',current:0, min:2, icon:'💡' },
{ name:'UPS Batteries', cat:'Power Equip', current:1, min:3, icon:'🔋' },
{ name:'Cat6 Network Cable', cat:'Networking',current:3, min:10, icon:'🔌' },
{ name:'Keyboard & Mouse Sets', cat:'Peripherals',current:4, min:15, icon:'⌨️' }
];
document.getElementById('lowStockList').innerHTML = low.map(l=>`
<div class="flex items-center gap-3 mb-3" style="padding:10px;background:var(--bg-surface);border-radius:var(--radius-md);border:1px solid rgba(239,68,68,.15)">
<span style="font-size:20px">${l.icon}</span>
<div style="flex:1">
<div style="font-size:13px;font-weight:600">${l.name}</div>
<div style="font-size:11px;color:var(--text-muted)">${l.cat} · Min: ${l.min}</div>
<div class="progress-wrap mt-2" style="height:4px">
<div class="progress-fill" style="width:${Math.round(l.current/l.min*100)}%;background:linear-gradient(90deg,var(--danger),var(--warning))"></div>
</div>
</div>
<div style="text-align:right">
<div style="font-size:16px;font-weight:800;color:var(--danger)">${l.current}</div>
<div style="font-size:10px;color:var(--text-muted)">in stock</div>
</div>
</div>`).join('') + `<a href="procurement.html" class="btn btn-primary btn-sm w-full" style="margin-top:8px">Raise Purchase Request →</a>`;
}
function renderMovements() {
const movs = [
{ date:'2025-05-28', type:'Inward (GRN)', item:'Dell Latitude 5540 Laptop', from:'Vendor', to:'Warehouse', qty:5, ref:'GRN-2025-041', by:'Arjun Sharma' },
{ date:'2025-05-27', type:'Outward (Issue)', item:'TP-Link WAP EAP670 (×8)', from:'Warehouse', to:'IT Dept Floor 3', qty:8, ref:'ISS-2025-022', by:'Kavya Nair' },
{ date:'2025-05-26', type:'Transfer', item:'Epson Projector', from:'Marketing F3', to:'Conference Room A', qty:1, ref:'TRF-2025-015', by:'Deepak Joshi' },
{ date:'2025-05-25', type:'Disposal', item:'HP ProBook 440 G4 (×2)', from:'IT Dept', to:'Disposed', qty:2, ref:'DSP-2025-008', by:'Arjun Sharma' },
{ date:'2025-05-24', type:'Outward (Issue)', item:'Steelcase Chair (×10)', from:'Warehouse', to:'Floor 3 Expansion', qty:10, ref:'ISS-2025-021', by:'Deepak Joshi' },
{ date:'2025-05-22', type:'Inward (GRN)', item:'Cisco IP Phone 8841 (×5)', from:'Vendor', to:'Warehouse', qty:5, ref:'GRN-2025-040', by:'Kavya Nair' }
];
const typeColor = {'Inward (GRN)':'badge-success','Outward (Issue)':'badge-info','Transfer':'badge-primary','Disposal':'badge-danger'};
document.getElementById('movTbody').innerHTML = movs.map(m=>`
<tr>
<td>${fmtDate(m.date)}</td>
<td><span class="badge ${typeColor[m.type]||'badge-neutral'}">${m.type}</span></td>
<td style="font-weight:600;color:var(--text-primary)">${m.item}</td>
<td>${m.from}</td>
<td>${m.to}</td>
<td style="font-weight:700">${m.qty}</td>
<td><code style="font-size:11px;color:var(--primary-light)">${m.ref}</code></td>
<td>${m.by}</td>
</tr>`).join('');
}
function renderAudit() {
const discrepancies = [
{ id:'AST-2025-007', name:'Epson Projector', sysLoc:'Marketing F3', phyLoc:'Conference Room A', status:'Location Mismatch' },
{ id:'AST-2025-015', name:'HP ProDesk 600', sysLoc:'Finance F1', phyLoc:'Not Found', status:'Missing' },
{ id:'AST-2025-012', name:'APC UPS 3KVA', sysLoc:'Server Room B1', phyLoc:'Server Room B1', status:'Found' }
];
document.getElementById('auditTbody').innerHTML = discrepancies.map(d=>`
<tr>
<td><code style="font-size:11px;color:var(--primary-light)">${d.id}</code></td>
<td style="font-weight:600;color:var(--text-primary)">${d.name}</td>
<td>${d.sysLoc}</td>
<td>${d.phyLoc}</td>
<td><span class="badge ${d.status==='Found'?'badge-success':d.status==='Missing'?'badge-danger':'badge-warning'}">${d.status}</span></td>
<td><button class="btn btn-ghost btn-sm" onclick="showToast('Update','Location updated in system','success')">Update</button></td>
</tr>`).join('');
}
function renderLocationTree() {
function buildTree(nodes) {
return `<ul class="tree-list" style="padding-left:0">
${nodes.map(n => `
<li class="tree-node">
<div class="tree-label tree-toggle ${n.children?'':'leaf'}" onclick="${n.children?'':(`selectLocation('${n.id}','${n.name}')`)}" style="${n.children?'':''}" >
${n.children ? `<span class="tree-arrow" style="font-size:10px"></span>` : `<span style="font-size:10px;color:var(--text-muted)"></span>`}
<span>${n.children ? '📁' : '📍'} ${n.name}</span>
</div>
${n.children ? `<div class="tree-children">${buildTree(n.children)}</div>` : ''}
</li>`).join('')}
</ul>`;
}
document.getElementById('locationTree').outerHTML = `<div id="locationTree">${buildTree(AMS.locations)}</div>`;
initTrees();
}
function selectLocation(id, name) {
document.querySelectorAll('.tree-label').forEach(el => el.classList.remove('selected'));
document.getElementById('locDetailTitle').textContent = `📍 ${name}`;
const locAssets = AMS.assets.filter(a => a.loc.includes(name.split('').pop().trim()) || Math.random() > .4).slice(0,6);
document.getElementById('locDetailContent').innerHTML = `
<div style="font-size:12px;color:var(--text-muted);margin-bottom:14px">Showing ${locAssets.length} assets in <strong>${name}</strong></div>
${locAssets.map(a=>`
<div class="flex items-center gap-3 mb-3" style="padding:10px;background:var(--bg-surface);border-radius:var(--radius-md);cursor:pointer" onclick="window.location.href='asset-detail.html?id=${a.id}'">
<span style="font-size:18px">${a.icon}</span>
<div style="flex:1"><div style="font-size:13px;font-weight:600">${a.name}</div><div style="font-size:11px;color:var(--text-muted)">${a.id} · ${a.assignee}</div></div>
<span class="badge ${statusBadge(a.status)}">${a.status}</span>
</div>`).join('')}`;
}
function renderStockChart() {
new Chart(document.getElementById('stockChart'), {
type:'bar',
data:{
labels: AMS.categories.map(c=>c.name),
datasets:[
{ label:'Active', data:AMS.categories.map(c=>Math.floor(c.count*.91)), backgroundColor:'rgba(16,185,129,.7)', borderRadius:4 },
{ label:'Idle', data:AMS.categories.map(c=>Math.floor(c.count*.05)), backgroundColor:'rgba(245,158,11,.7)', borderRadius:4 },
{ label:'Maintenance', data:AMS.categories.map(c=>Math.floor(c.count*.04)), backgroundColor:'rgba(59,130,246,.7)', borderRadius:4 }
]
},
options:{
responsive:true, maintainAspectRatio:false, plugins:{legend:{labels:{color:'#94A3B8',font:{family:'Inter',size:11},usePointStyle:true}}},
scales:{x:{stacked:true,grid:{color:'rgba(255,255,255,.04)'},ticks:{color:'#4B5563',font:{family:'Inter',size:9},maxRotation:45}},
y:{stacked:true,grid:{color:'rgba(255,255,255,.04)'},ticks:{color:'#4B5563',font:{family:'Inter',size:11}}}}
}
});
}
function saveGRN() { closeModal('grnModal'); showToast('GRN Saved','5 assets created from GRN-2025-042','success'); }
function startAudit() { closeModal('auditModal'); showToast('Audit Started','Q2 Physical Audit initiated. Auditors notified.','info'); }
</script>
</body>
</html>

271
js/app.js
View File

@ -2,13 +2,29 @@
// AMS — Core Application Logic | app.js
// ================================================================
/* ── HTML escaping (XSS-safe output) ───────────────────────────── */
function escapeHtml(value) {
if (value === null || value === undefined) return '';
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Escape for use inside a single-quoted JS string in an inline handler.
function escapeJs(value) {
if (value === null || value === undefined) return '';
return String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '&quot;').replace(/</g, '\\u003C');
}
/* ── Active Nav ─────────────────────────────────────────────────── */
function setActiveNav() {
const page = window.location.pathname.split('/').pop() || 'dashboard.html';
const active = (window.NAV_ACTIVE_ALIAS && window.NAV_ACTIVE_ALIAS[page]) || page;
document.querySelectorAll('.nav-item').forEach(el => {
const href = el.getAttribute('href') || '';
if (href && page === href) el.classList.add('active');
else el.classList.remove('active');
el.classList.toggle('active', !!href && href === active);
});
}
@ -156,6 +172,42 @@ function initTrees() {
});
}
/* ── Theme Toggle (light / dark) ─────────────────────────────── */
function getTheme() {
return document.documentElement.getAttribute('data-theme') || 'light';
}
function setTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('AMS_THEME', theme);
updateThemeUI();
if (window.lucide) lucide.createIcons();
}
function toggleTheme() {
setTheme(getTheme() === 'dark' ? 'light' : 'dark');
}
function updateThemeUI() {
const dark = getTheme() === 'dark';
const btn = document.getElementById('themeToggle');
if (btn) {
btn.innerHTML = `<i data-lucide="${dark ? 'sun' : 'moon'}"></i>`;
btn.title = dark ? 'Switch to light theme' : 'Switch to dark theme';
}
// Keep Settings → Appearance theme cards in sync if present
document.querySelectorAll('[data-theme-opt]').forEach(opt => {
opt.classList.toggle('selected', opt.getAttribute('data-theme-opt') === getTheme());
});
}
function injectThemeToggle() {
const actions = document.querySelector('.topbar-actions');
if (!actions || document.getElementById('themeToggle')) return;
const btn = document.createElement('button');
btn.className = 'icon-btn';
btn.id = 'themeToggle';
btn.setAttribute('onclick', 'toggleTheme()');
actions.insertBefore(btn, actions.firstChild);
updateThemeUI();
}
/* ── Notif Panel ─────────────────────────────────────────────── */
function initNotifPanel() {
const bell = document.getElementById('notifBell');
@ -167,15 +219,210 @@ function initNotifPanel() {
}
/* ── Sidebar User Info ─────────────────────────────────────── */
/* ── Sidebar User Info ─────────────────────────────────────── */
const Permissions = {
'Super Admin': {
viewAllAssets: true, createEditAssets: true, deleteDispose: true,
assignAssets: true, transferAssets: true, viewFinancials: true,
approvePR: true, raiseTickets: true, manageUsers: true, systemSettings: true
},
'Asset Manager': {
viewAllAssets: true, createEditAssets: true, deleteDispose: true,
assignAssets: true, transferAssets: true, viewFinancials: true,
approvePR: true, raiseTickets: true, manageUsers: false, systemSettings: false
},
'IT Head': {
viewAllAssets: true, createEditAssets: true, deleteDispose: false,
assignAssets: true, transferAssets: true, viewFinancials: false,
approvePR: false, raiseTickets: true, manageUsers: true, systemSettings: false
},
'Asset Coordinator': {
viewAllAssets: true, createEditAssets: true, deleteDispose: false,
assignAssets: true, transferAssets: true, viewFinancials: false,
approvePR: false, raiseTickets: true, manageUsers: false, systemSettings: false
},
'Department Head': {
viewAllAssets: true, createEditAssets: false, deleteDispose: false,
assignAssets: true, transferAssets: true, viewFinancials: false,
approvePR: true, raiseTickets: true, manageUsers: false, systemSettings: false
},
'Finance Head': {
viewAllAssets: true, createEditAssets: false, deleteDispose: false,
assignAssets: false, transferAssets: false, viewFinancials: true,
approvePR: true, raiseTickets: true, manageUsers: false, systemSettings: false
},
'Employee': {
viewAllAssets: false, createEditAssets: false, deleteDispose: false,
assignAssets: false, transferAssets: false, viewFinancials: false,
approvePR: false, raiseTickets: true, manageUsers: false, systemSettings: false
}
};
function checkPermission(perm) {
if (!window.AMS || !window.AMS.currentUser) return false;
const role = window.AMS.currentUser.role;
const rolePerms = Permissions[role] || Permissions['Employee'];
return !!rolePerms[perm];
}
function populateSidebarUser() {
if (!window.AMS) return;
const u = AMS.currentUser;
const nameEl = document.getElementById('sidebarUserName');
const roleEl = document.getElementById('sidebarUserRole');
const avEl = document.getElementById('sidebarUserAv');
if (nameEl) nameEl.textContent = u.name;
if (roleEl) roleEl.textContent = u.role;
if (avEl) avEl.textContent = u.avatar;
// Hook the click listener on sidebar footer or user card to open switcher
const card = document.querySelector('.user-card, .sidebar-footer');
if (card) {
card.style.cursor = 'pointer';
card.title = 'Click to switch simulated role';
card.addEventListener('click', (e) => {
e.preventDefault();
openRoleSwitcherModal();
});
}
const nameEls = document.querySelectorAll('.user-name, #sidebarUserName');
const roleEls = document.querySelectorAll('.user-role, #sidebarUserRole');
const avEls = document.querySelectorAll('.user-av, #sidebarUserAv');
nameEls.forEach(el => el.textContent = u.name);
roleEls.forEach(el => el.textContent = u.role);
avEls.forEach(el => el.textContent = u.avatar);
}
function openRoleSwitcherModal() {
let modal = document.getElementById('dynRoleModal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'dynRoleModal';
modal.className = 'modal-overlay';
modal.style.zIndex = '9999';
modal.innerHTML = `
<div class="modal" style="max-width:400px; background:var(--bg-elevated); border:1px solid var(--border-strong); border-radius:14px; box-shadow:var(--shadow-lg)">
<div class="modal-header" style="display:flex; justify-content:space-between; align-items:center; padding:16px; border-bottom:1px solid var(--border)">
<span class="modal-title" style="font-weight:700; color:var(--text-primary)">🔐 Switch User Role</span>
<button class="modal-close" onclick="closeModal('dynRoleModal')" style="background:none; border:none; color:var(--text-muted); cursor:pointer; font-size:16px"></button>
</div>
<div class="modal-body" style="padding:16px">
<div style="font-size:12.5px; color:var(--text-muted); margin-bottom:16px">
Select a simulated role to test the access control permissions matrix.
</div>
<div style="display:flex; flex-direction:column; gap:8px; max-height:300px; overflow-y:auto" id="dynRoleList">
</div>
</div>
<div class="modal-footer" style="padding:12px 16px; border-top:1px solid var(--border)">
<button class="btn btn-secondary w-full" onclick="closeModal('dynRoleModal')">Close</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', e => { if (e.target === modal) closeModal('dynRoleModal'); });
}
const list = document.getElementById('dynRoleList');
list.innerHTML = AMS.users.map(u => `
<button class="btn btn-secondary" onclick="switchSimUser('${u.id}')" style="display:flex; align-items:center; justify-content:space-between; padding:10px 14px; text-align:left; width:100%; border:1px solid ${AMS.currentUser.id === u.id ? 'var(--primary)' : 'var(--border)'}; background:${AMS.currentUser.id === u.id ? 'var(--primary-glow)' : 'var(--bg-surface)'}">
<div style="display:flex; align-items:center; gap:10px">
<div style="width:28px; height:28px; border-radius:50%; background:${u.color}22; color:${u.color}; display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700">${u.av}</div>
<div>
<div style="font-weight:600; font-size:12.5px; color:var(--text-primary)">${u.name}</div>
<div style="font-size:10.5px; color:var(--text-muted)">${u.role} · ${u.dept}</div>
</div>
</div>
${AMS.currentUser.id === u.id ? '<span style="color:var(--primary-light); font-size:11.5px; font-weight:600">✓ Active</span>' : ''}
</button>
`).join('');
openModal('dynRoleModal');
}
function switchSimUser(userId) {
const u = AMS.users.find(x => x.id === userId);
if (u) {
AMS.currentUser = u;
AMS.save();
closeModal('dynRoleModal');
showToast('Role Switched', `Logged in as ${u.name} (${u.role})`, 'success');
setTimeout(() => {
const page = window.location.pathname.split('/').pop() || 'dashboard.html';
if (u.role === 'Employee' && ['settings.html', 'users.html', 'asset-create.html'].some(p => page.includes(p))) {
window.location.href = 'dashboard.html';
} else {
window.location.reload();
}
}, 800);
}
}
function applyRolePermissions() {
if (!window.AMS || !window.AMS.currentUser) return;
const u = AMS.currentUser;
const page = window.location.pathname.split('/').pop() || 'dashboard.html';
// Page access control
if (page.includes('settings.html') && !checkPermission('systemSettings')) {
showAccessDenied();
return;
}
if (page.includes('users.html') && !checkPermission('manageUsers')) {
showAccessDenied();
return;
}
if (page.includes('asset-create.html') && !checkPermission('createEditAssets')) {
showAccessDenied();
return;
}
// Hide sidebar sections/links
document.querySelectorAll('.sidebar-nav .nav-item').forEach(item => {
const href = item.getAttribute('href') || '';
if (href.includes('settings.html') && !checkPermission('systemSettings')) {
item.style.display = 'none';
}
if (href.includes('users.html') && !checkPermission('manageUsers')) {
item.style.display = 'none';
}
if (href.includes('asset-create.html') && !checkPermission('createEditAssets')) {
item.style.display = 'none';
}
if (u.role === 'Employee') {
if (href.includes('reports.html')) {
item.style.display = 'none';
}
}
});
// Disable / Hide edit operations
if (!checkPermission('createEditAssets')) {
document.querySelectorAll('.btn-primary[href="asset-create.html"], a[href="asset-create.html"], button[onclick*="openModal(\'editModal\')"], button[onclick*="saveEdit"]').forEach(el => {
el.style.display = 'none';
});
}
if (!checkPermission('assignAssets')) {
document.querySelectorAll('button[onclick*="openAssignModal"], button[onclick*="openModal(\'assignModal\')"], button[title="Assign"]').forEach(el => {
el.style.display = 'none';
});
}
if (!checkPermission('transferAssets')) {
document.querySelectorAll('button[onclick*="openTransferModal"], button[onclick*="openModal(\'transferModal\')"], button[title="Transfer"]').forEach(el => {
el.style.display = 'none';
});
}
if (!checkPermission('deleteDispose')) {
document.querySelectorAll('button[onclick*="confirmDispose"], button[onclick*="bulkAction(\'dispose\')"], button[title="Dispose"]').forEach(el => {
el.style.display = 'none';
});
}
}
function showAccessDenied() {
document.body.innerHTML = `
<div style="height:100vh; display:flex; flex-direction:column; align-items:center; justify-content:center; background:#0f172a; color:#fff; font-family:'Inter', sans-serif; text-align:center; padding:20px">
<div style="font-size:72px; margin-bottom:20px">🚫</div>
<h1 style="font-size:28px; font-weight:800; margin-bottom:8px">Access Denied</h1>
<p style="color:#94a3b8; max-width:400px; margin-bottom:24px">You do not have the required permissions to access this module. Current Role: <strong>${AMS.currentUser.role}</strong></p>
<button class="btn btn-primary" onclick="window.location.href='dashboard.html'">Go to Dashboard</button>
</div>
`;
}
/* ── Topbar Notifications Badge ─────────────────────────────── */
@ -237,13 +484,11 @@ function downloadCSV(data, filename='export.csv') {
/* ── Generate QR SVG Pattern ───────────────────────────────── */
function generateQRPattern(seed) {
// Deterministic pseudo-QR pattern based on seed string
let hash = 0;
for (let i=0; i<seed.length; i++) hash = (hash * 31 + seed.charCodeAt(i)) | 0;
const rand = (n) => { hash = (hash * 1103515245 + 12345) | 0; return Math.abs(hash) % n; };
const cells = [];
for (let i=0;i<49;i++) cells.push(rand(2));
// Force finder pattern corners
const corners = [0,1,2,3,4,5,6,7,14,21,28,35,42,43,44,45,46,47,48];
corners.forEach(i => cells[i] = i%7<1 || i%7>5 || Math.floor(i/7)<1 || Math.floor(i/7)>5 ? 1 : 0);
return cells;
@ -256,9 +501,15 @@ document.addEventListener('DOMContentLoaded', () => {
initDropdowns();
initTrees();
initNotifPanel();
injectThemeToggle();
populateSidebarUser();
populateNotifBadge();
renderNotifPanel();
applyRolePermissions();
if (window.lucide) {
lucide.createIcons();
}
// Auto close modal on overlay click
document.querySelectorAll('.modal-overlay').forEach(ov => {

View File

@ -1,9 +1,8 @@
// ================================================================
// AMS — Sample Data Store | Version 1.0
// AMS — Sample Data Store | Version 1.0 (Persistent V1)
// ================================================================
const AMS = {
const defaultData = {
currentUser: {
id: 'u001', name: 'Arjun Sharma', email: 'arjun.sharma@acmecorp.com',
role: 'Asset Manager', dept: 'IT', avatar: 'AS', location: 'HQ Bengaluru'
@ -16,67 +15,81 @@ const AMS = {
},
stats: {
total: 1247, active: 1089, maintenance: 47, idle: 63, disposed: 48,
totalValue: 42850000, netValue: 30100000, depreciated: 12750000,
total: 1251, active: 1093, maintenance: 47, idle: 63, disposed: 48,
totalValue: 43360000, netValue: 30610000, depreciated: 12750000,
pendingTickets: 12, lowStock: 5, expiringAMC: 3, pendingPRs: 8
},
projects: [
{ id: 'PRJ-001', name: 'Project Apollo', manager: 'Priya Kumar', dept: 'IT' },
{ id: 'PRJ-002', name: 'Cloud Migration', manager: 'Vikram Reddy', dept: 'IT' },
{ id: 'PRJ-003', name: 'Acme Website Redesign', manager: 'Sneha Patel', dept: 'Marketing' },
{ id: 'PRJ-004', name: 'Finance Audit V2', manager: 'Rahul Mehta', dept: 'Finance' }
],
assets: [
{ id:'AST-2025-001', name:'Dell Latitude 5540', cat:'Laptops', serial:'DL5540-8K2J9', status:'Active', dept:'IT', loc:'IT Dept Floor 2', assignee:'Priya Kumar', purchase:'2024-01-15', cost:85000, value:68000, vendor:'Dell India', warranty:'2027-01-15', depM:'SLM', depR:20, icon:'💻' },
{ id:'AST-2025-002', name:'HP LaserJet Pro M404', cat:'Printers', serial:'HP-LJ404-3F7K1', status:'Active', dept:'Finance', loc:'Finance Floor 1', assignee:'Shared', purchase:'2023-08-20', cost:24000, value:17600, vendor:'HP India', warranty:'2025-08-20', depM:'SLM', depR:25, icon:'🖨️' },
{ id:'AST-2025-003', name:'Apple MacBook Pro 14"', cat:'Laptops', serial:'MPB14-C7R5X', status:'Active', dept:'Marketing', loc:'Marketing Floor 3', assignee:'Sneha Patel', purchase:'2024-03-10', cost:195000, value:175500, vendor:'Apple India', warranty:'2026-03-10', depM:'SLM', depR:20, icon:'💻' },
{ id:'AST-2025-004', name:'Dell PowerEdge R740', cat:'Servers', serial:'PE-R740-9M4L2', status:'Active', dept:'IT', loc:'Server Room B1', assignee:'IT Team', purchase:'2023-05-01', cost:380000, value:266000, vendor:'Dell India', warranty:'2026-05-01', depM:'WDV', depR:30, icon:'🖥️' },
{ id:'AST-2025-005', name:'Daikin 1.5T Split AC', cat:'HVAC', serial:'DK-FTKG-2024-05', status:'Under Maintenance', dept:'HR', loc:'HR Floor 2', assignee:'Shared', purchase:'2022-04-15', cost:45000, value:25650, vendor:'Daikin India', warranty:'2024-04-15', depM:'SLM', depR:15, icon:'❄️' },
{ id:'AST-2025-006', name:'Lenovo ThinkPad X1 Carbon',cat:'Laptops', serial:'LN-X1C-7P3Q8', status:'Active', dept:'Operations',loc:'Operations Floor 1', assignee:'Vikram Reddy', purchase:'2024-02-01', cost:125000, value:112500, vendor:'Lenovo India', warranty:'2027-02-01', depM:'SLM', depR:20, icon:'💻' },
{ id:'AST-2025-007', name:'Epson EB-2250U Projector', cat:'AV Equipment', serial:'EP-EB2250-6N9R', status:'Idle', dept:'Admin', loc:'Conference Room A', assignee:'Unassigned', purchase:'2023-11-10', cost:78000, value:62400, vendor:'Epson India', warranty:'2025-11-10', depM:'SLM', depR:20, icon:'📽️' },
{ id:'AST-2025-008', name:'Cisco Catalyst 9300', cat:'Networking', serial:'CS-C9300-4K7J', status:'Active', dept:'IT', loc:'Server Room B1', assignee:'Network Team', purchase:'2023-07-20', cost:145000, value:101500, vendor:'Cisco Systems', warranty:'2026-07-20', depM:'WDV', depR:30, icon:'🔌' },
{ id:'AST-2025-009', name:'iPhone 15 Pro', cat:'Mobile Devices', serial:'IP15P-A2M8K', status:'Active', dept:'Marketing', loc:'Sales Floor', assignee:'Raj Patel', purchase:'2024-01-05', cost:134900, value:121410, vendor:'Apple India', warranty:'2026-01-05', depM:'SLM', depR:20, icon:'📱' },
{ id:'AST-2025-010', name:'Canon EOS R5 Camera', cat:'AV Equipment', serial:'CN-EOSR5-2B7K', status:'Active', dept:'Marketing', loc:'Marketing Floor 3', assignee:'Content Team', purchase:'2023-09-15', cost:285000, value:228000, vendor:'Canon India', warranty:'2025-09-15', depM:'SLM', depR:20, icon:'📷' },
{ id:'AST-2025-011', name:'Steelcase Leap V2 Chair', cat:'Furniture', serial:'SC-LV2-9P3X2', status:'Active', dept:'IT', loc:'IT Dept Floor 2', assignee:'Priya Kumar', purchase:'2022-06-01', cost:35000, value:21000, vendor:'Steelcase India', warranty:'2027-06-01', depM:'SLM', depR:10, icon:'🪑' },
{ id:'AST-2025-012', name:'APC Smart UPS 3KVA', cat:'Power Equipment',serial:'APC-SM3K-7Y4L', status:'Active', dept:'IT', loc:'Server Room B1', assignee:'IT Team', purchase:'2023-03-10', cost:28000, value:21000, vendor:'Schneider Elec.', warranty:'2025-03-10', depM:'SLM', depR:25, icon:'🔋' },
{ id:'AST-2025-013', name:'Samsung 43" Display', cat:'Displays', serial:'SM-43D-5R2M9', status:'Active', dept:'Admin', loc:'Reception Floor 1', assignee:'Shared', purchase:'2024-01-20', cost:42000, value:39900, vendor:'Samsung India', warranty:'2025-01-20', depM:'SLM', depR:25, icon:'🖥️' },
{ id:'AST-2025-014', name:'Honda City (Company Car)', cat:'Vehicles', serial:'MH12CC4567', status:'Active', dept:'Admin', loc:'Parking B1', assignee:'Driver Pool', purchase:'2022-12-01', cost:1350000, value:945000, vendor:'Honda Dealers', warranty:'2025-12-01', depM:'WDV', depR:15, icon:'🚗' },
{ id:'AST-2025-015', name:'HP ProDesk 600 Desktop', cat:'Desktops', serial:'HP-PD600-8K3X', status:'Idle', dept:'Finance', loc:'Finance Floor 1', assignee:'Unassigned', purchase:'2021-08-15', cost:55000, value:11000, vendor:'HP India', warranty:'2023-08-15', depM:'SLM', depR:20, icon:'🖥️' },
{ id:'AST-2025-016', name:'Ricoh Aficio Photocopier', cat:'Printers', serial:'RC-AF-5502-7P', status:'Under Maintenance',dept:'Admin', loc:'Admin Floor 1', assignee:'Shared', purchase:'2022-03-20', cost:175000, value:87500, vendor:'Ricoh India', warranty:'2024-03-20', depM:'SLM', depR:25, icon:'🖨️' },
{ id:'AST-2025-017', name:'Cisco IP Phone 8841', cat:'Networking', serial:'CS-8841-3P7K', status:'Active', dept:'HR', loc:'HR Floor 2', assignee:'Anita Singh', purchase:'2023-06-10', cost:18500, value:12950, vendor:'Cisco Systems', warranty:'2025-06-10', depM:'SLM', depR:25, icon:'☎️' },
{ id:'AST-2025-018', name:'Tata Nexon EV', cat:'Vehicles', serial:'MH12EV1234', status:'Active', dept:'Admin', loc:'Parking B1', assignee:'Exec Pool', purchase:'2024-02-15', cost:1650000, value:1567500, vendor:'Tata Motors', warranty:'2027-02-15', depM:'WDV', depR:15, icon:'🚗' },
{ id:'AST-2025-019', name:'Synology NAS DS920+', cat:'Storage', serial:'SY-DS920-4M7K', status:'Active', dept:'IT', loc:'Server Room B1', assignee:'IT Team', purchase:'2023-10-05', cost:62000, value:49600, vendor:'Synology', warranty:'2026-10-05', depM:'SLM', depR:20, icon:'💾' },
{ id:'AST-2025-020', name:'Godrej Steel Almirah', cat:'Furniture', serial:'GJ-SS-2023-012', status:'Active', dept:'Finance', loc:'Finance Floor 1', assignee:'Shared', purchase:'2021-05-20', cost:22000, value:13200, vendor:'Godrej Interio', warranty:'N/A', depM:'SLM', depR:10, icon:'🗄️' }
// Physical Assets (Original)
{ id:'AST-2025-001', name:'Dell Latitude 5540', cat:'Laptops', serial:'DL5540-8K2J9', status:'Active', dept:'IT', loc:'IT Dept Floor 2', assignee:'Priya Kumar', purchase:'2024-01-15', cost:85000, value:68000, vendor:'Dell India', warranty:'2027-01-15', depM:'SLM', depR:20, icon:'💻', type:'physical' },
{ id:'AST-2025-002', name:'HP LaserJet Pro M404', cat:'Printers', serial:'HP-LJ404-3F7K1', status:'Active', dept:'Finance', loc:'Finance Floor 1', assignee:'Shared', purchase:'2023-08-20', cost:24000, value:17600, vendor:'HP India', warranty:'2025-08-20', depM:'SLM', depR:25, icon:'🖨️', type:'physical' },
{ id:'AST-2025-003', name:'Apple MacBook Pro 14"', cat:'Laptops', serial:'MPB14-C7R5X', status:'Active', dept:'Marketing', loc:'Marketing Floor 3', assignee:'Sneha Patel', purchase:'2024-03-10', cost:195000, value:175500, vendor:'Apple India', warranty:'2026-03-10', depM:'SLM', depR:20, icon:'💻', type:'physical' },
{ id:'AST-2025-004', name:'Dell PowerEdge R740', cat:'Servers', serial:'PE-R740-9M4L2', status:'Active', dept:'IT', loc:'Server Room B1', assignee:'IT Team', purchase:'2023-05-01', cost:380000, value:266000, vendor:'Dell India', warranty:'2026-05-01', depM:'WDV', depR:30, icon:'🖥️', type:'physical' },
{ id:'AST-2025-005', name:'Daikin 1.5T Split AC', cat:'HVAC', serial:'DK-FTKG-2024-05', status:'Under Maintenance', dept:'HR', loc:'HR Floor 2', assignee:'Shared', purchase:'2022-04-15', cost:45000, value:25650, vendor:'Daikin India', warranty:'2024-04-15', depM:'SLM', depR:15, icon:'❄️', type:'physical' },
{ id:'AST-2025-006', name:'Lenovo ThinkPad X1 Carbon',cat:'Laptops', serial:'LN-X1C-7P3Q8', status:'Active', dept:'Operations',loc:'Operations Floor 1', assignee:'Vikram Reddy', purchase:'2024-02-01', cost:125000, value:112500, vendor:'Lenovo India', warranty:'2027-02-01', depM:'SLM', depR:20, icon:'💻', type:'physical' },
{ id:'AST-2025-007', name:'Epson EB-2250U Projector', cat:'AV Equipment', serial:'EP-EB2250-6N9R', status:'Idle', dept:'Admin', loc:'Conference Room A', assignee:'Unassigned', purchase:'2023-11-10', cost:78000, value:62400, vendor:'Epson India', warranty:'2025-11-10', depM:'SLM', depR:20, icon:'📽️', type:'physical' },
{ id:'AST-2025-008', name:'Cisco Catalyst 9300', cat:'Networking', serial:'CS-C9300-4K7J', status:'Active', dept:'IT', loc:'Server Room B1', assignee:'Network Team', purchase:'2023-07-20', cost:145000, value:101500, vendor:'Cisco Systems', warranty:'2026-07-20', depM:'WDV', depR:30, icon:'🔌', type:'physical' },
{ id:'AST-2025-009', name:'iPhone 15 Pro', cat:'Mobile Devices', serial:'IP15P-A2M8K', status:'Active', dept:'Marketing', loc:'Sales Floor', assignee:'Raj Patel', purchase:'2024-01-05', cost:134900, value:121410, vendor:'Apple India', warranty:'2026-01-05', depM:'SLM', depR:20, icon:'📱', type:'physical' },
{ id:'AST-2025-010', name:'Canon EOS R5 Camera', cat:'AV Equipment', serial:'CN-EOSR5-2B7K', status:'Active', dept:'Marketing', loc:'Marketing Floor 3', assignee:'Content Team', purchase:'2023-09-15', cost:285000, value:228000, vendor:'Canon India', warranty:'2025-09-15', depM:'SLM', depR:20, icon:'📷', type:'physical' },
{ id:'AST-2025-011', name:'Steelcase Leap V2 Chair', cat:'Furniture', serial:'SC-LV2-9P3X2', status:'Active', dept:'IT', loc:'IT Dept Floor 2', assignee:'Priya Kumar', purchase:'2022-06-01', cost:35000, value:21000, vendor:'Steelcase India', warranty:'2027-06-01', depM:'SLM', depR:10, icon:'🪑', type:'physical' },
{ id:'AST-2025-012', name:'APC Smart UPS 3KVA', cat:'Power Equipment',serial:'APC-SM3K-7Y4L', status:'Active', dept:'IT', loc:'Server Room B1', assignee:'IT Team', purchase:'2023-03-10', cost:28000, value:21000, vendor:'Schneider Elec.', warranty:'2025-03-10', depM:'SLM', depR:25, icon:'🔋', type:'physical' },
{ id:'AST-2025-013', name:'Samsung 43" Display', cat:'Displays', serial:'SM-43D-5R2M9', status:'Active', dept:'Admin', loc:'Reception Floor 1', assignee:'Shared', purchase:'2024-01-20', cost:42000, value:39900, vendor:'Samsung India', warranty:'2025-01-20', depM:'SLM', depR:25, icon:'🖥️', type:'physical' },
{ id:'AST-2025-014', name:'Honda City (Company Car)', cat:'Vehicles', serial:'MH12CC4567', status:'Active', dept:'Admin', loc:'Parking B1', assignee:'Driver Pool', purchase:'2022-12-01', cost:1350000, value:945000, vendor:'Honda Dealers', warranty:'2025-12-01', depM:'WDV', depR:15, icon:'🚗', type:'physical' },
{ id:'AST-2025-015', name:'HP ProDesk 600 Desktop', cat:'Desktops', serial:'HP-PD600-8K3X', status:'Idle', dept:'Finance', loc:'Finance Floor 1', assignee:'Unassigned', purchase:'2021-08-15', cost:55000, value:11000, vendor:'HP India', warranty:'2023-08-15', depM:'SLM', depR:20, icon:'🖥️', type:'physical' },
{ id:'AST-2025-016', name:'Ricoh Aficio Photocopier', cat:'Printers', serial:'RC-AF-5502-7P', status:'Under Maintenance',dept:'Admin', loc:'Admin Floor 1', assignee:'Shared', purchase:'2022-03-20', cost:175000, value:87500, vendor:'Ricoh India', warranty:'2024-03-20', depM:'SLM', depR:25, icon:'🖨️', type:'physical' },
{ id:'AST-2025-017', name:'Cisco IP Phone 8841', cat:'Networking', serial:'CS-8841-3P7K', status:'Active', dept:'HR', loc:'HR Floor 2', assignee:'Anita Singh', purchase:'2023-06-10', cost:18500, value:12950, vendor:'Cisco Systems', warranty:'2025-06-10', depM:'SLM', depR:25, icon:'☎️', type:'physical' },
{ id:'AST-2025-018', name:'Tata Nexon EV', cat:'Vehicles', serial:'MH12EV1234', status:'Active', dept:'Admin', loc:'Parking B1', assignee:'Exec Pool', purchase:'2024-02-15', cost:1650000, value:1567500, vendor:'Tata Motors', warranty:'2027-02-15', depM:'WDV', depR:15, icon:'🚗', type:'physical' },
{ id:'AST-2025-019', name:'Synology NAS DS920+', cat:'Storage', serial:'SY-DS920-4M7K', status:'Active', dept:'IT', loc:'Server Room B1', assignee:'IT Team', purchase:'2023-10-05', cost:62000, value:49600, vendor:'Synology', warranty:'2026-10-05', depM:'SLM', depR:20, icon:'💾', type:'physical' },
{ id:'AST-2025-020', name:'Godrej Steel Almirah', cat:'Furniture', serial:'GJ-SS-2023-012', status:'Active', dept:'Finance', loc:'Finance Floor 1', assignee:'Shared', purchase:'2021-05-20', cost:22000, value:13200, vendor:'Godrej Interio', warranty:'N/A', depM:'SLM', depR:10, icon:'🗄️', type:'physical' },
// Digital Assets (New V1 features)
{ id:'AST-2025-021', name:'Microsoft 365 Enterprise', cat:'Software Licenses', serial:'M365-50S-2025', status:'Active', dept:'IT', loc:'Cloud Environment', assignee:'IT Team', purchase:'2025-01-10', cost:180000, value:120000, vendor:'Microsoft India', warranty:'2026-01-10', depM:'SLM', depR:100, icon:'🔑', type:'digital', licenseKey:'M365-XXXX-XXXX-YYYY', seatsTotal:50, seatsAlloc:42, project:'Project Apollo' },
{ id:'AST-2025-022', name:'AWS Production Hosting', cat:'Cloud Subscriptions',serial:'AWS-ACME-PROD', status:'Active', dept:'IT', loc:'AWS Mumbai region', assignee:'Network Team', purchase:'2025-03-01', cost:250000, value:250000, vendor:'Amazon Web Services',warranty:'2026-03-01', depM:'SLM', depR:100, icon:'☁️', type:'digital', billingCycle:'Monthly', provider:'AWS Cloud', project:'Cloud Migration' },
{ id:'AST-2025-023', name:'Adobe Creative Suite', cat:'Software Licenses', serial:'ADOBE-CC-15S', status:'Active', dept:'Marketing', loc:'Cloud Environment', assignee:'Sneha Patel', purchase:'2024-07-20', cost:85000, value:17000, vendor:'Adobe Systems', warranty:'2025-07-20', depM:'SLM', depR:100, icon:'🔑', type:'digital', licenseKey:'ADOB-CREA-FLOW-1111', seatsTotal:15, seatsAlloc:12, project:'Acme Website Redesign' },
{ id:'AST-2025-024', name:'GitHub Enterprise Server', cat:'Software Licenses', serial:'GHUB-ENT-50S', status:'Active', dept:'IT', loc:'Local VM', assignee:'Priya Kumar', purchase:'2025-02-10', cost:90000, value:90000, vendor:'GitHub Inc', warranty:'2026-02-10', depM:'SLM', depR:100, icon:'🔑', type:'digital', licenseKey:'GHUB-9999-8888-7777', seatsTotal:50, seatsAlloc:48, project:'Cloud Migration' }
],
users: [
{ id:'u001', name:'Arjun Sharma', email:'arjun.sharma@acmecorp.com', role:'Asset Manager', dept:'IT', status:'Active', lastLogin:'2025-05-28 09:12', av:'AS', color:'#6366F1' },
{ id:'u002', name:'Priya Kumar', email:'priya.kumar@acmecorp.com', role:'IT Head', dept:'IT', status:'Active', lastLogin:'2025-05-28 08:45', av:'PK', color:'#10B981' },
{ id:'u003', name:'Rahul Mehta', email:'rahul.mehta@acmecorp.com', role:'Finance Head', dept:'Finance', status:'Active', lastLogin:'2025-05-27 17:30', av:'RM', color:'#F59E0B' },
{ id:'u004', name:'Anita Singh', email:'anita.singh@acmecorp.com', role:'HR Manager', dept:'HR', status:'Active', lastLogin:'2025-05-28 10:02', av:'AS', color:'#A855F7' },
{ id:'u005', name:'Vikram Reddy', email:'vikram.reddy@acmecorp.com', role:'Operations Head', dept:'Operations',status:'Active', lastLogin:'2025-05-26 14:20', av:'VR', color:'#06B6D4' },
{ id:'u006', name:'Sneha Patel', email:'sneha.patel@acmecorp.com', role:'Marketing Lead', dept:'Marketing', status:'Active', lastLogin:'2025-05-28 11:00', av:'SP', color:'#EF4444' },
{ id:'u007', name:'Deepak Joshi', email:'deepak.joshi@acmecorp.com', role:'Admin Officer', dept:'Admin', status:'Active', lastLogin:'2025-05-27 09:30', av:'DJ', color:'#3B82F6' },
{ id:'u008', name:'Kavya Nair', email:'kavya.nair@acmecorp.com', role:'Asset Coordinator',dept:'IT', status:'Active', lastLogin:'2025-05-28 09:55', av:'KN', color:'#10B981' },
{ id:'u009', name:'Raj Patel', email:'raj.patel@acmecorp.com', role:'Sales Executive', dept:'Marketing', status:'Inactive',lastLogin:'2025-05-20 16:10', av:'RP', color:'#6366F1' },
{ id:'u010', name:'System Admin', email:'admin@acmecorp.com', role:'Super Admin', dept:'IT', status:'Active', lastLogin:'2025-05-28 07:00', av:'SA', color:'#F59E0B' }
{ id:'u001', name:'Arjun Sharma', email:'arjun.sharma@acmecorp.com', role:'Asset Manager', dept:'IT', status:'Active', lastLogin:'2026-06-04 09:12', av:'AS', color:'#6366F1' },
{ id:'u002', name:'Priya Kumar', email:'priya.kumar@acmecorp.com', role:'IT Head', dept:'IT', status:'Active', lastLogin:'2026-06-04 08:45', av:'PK', color:'#10B981' },
{ id:'u003', name:'Rahul Mehta', email:'rahul.mehta@acmecorp.com', role:'Finance Head', dept:'Finance', status:'Active', lastLogin:'2026-06-03 17:30', av:'RM', color:'#F59E0B' },
{ id:'u004', name:'Anita Singh', email:'anita.singh@acmecorp.com', role:'HR Manager', dept:'HR', status:'Active', lastLogin:'2026-06-04 10:02', av:'AS', color:'#A855F7' },
{ id:'u005', name:'Vikram Reddy', email:'vikram.reddy@acmecorp.com', role:'Operations Head', dept:'Operations',status:'Active', lastLogin:'2026-06-02 14:20', av:'VR', color:'#06B6D4' },
{ id:'u006', name:'Sneha Patel', email:'sneha.patel@acmecorp.com', role:'Marketing Lead', dept:'Marketing', status:'Active', lastLogin:'2026-06-04 11:00', av:'SP', color:'#EF4444' },
{ id:'u007', name:'Deepak Joshi', email:'deepak.joshi@acmecorp.com', role:'Admin Officer', dept:'Admin', status:'Active', lastLogin:'2026-06-03 09:30', av:'DJ', color:'#3B82F6' },
{ id:'u008', name:'Kavya Nair', email:'kavya.nair@acmecorp.com', role:'Asset Coordinator',dept:'IT', status:'Active', lastLogin:'2026-06-04 09:55', av:'KN', color:'#10B981' },
{ id:'u009', name:'Raj Patel', email:'raj.patel@acmecorp.com', role:'Sales Executive', dept:'Marketing', status:'Inactive',lastLogin:'2026-05-20 16:10', av:'RP', color:'#6366F1' },
{ id:'u010', name:'System Admin', email:'admin@acmecorp.com', role:'Super Admin', dept:'IT', status:'Active', lastLogin:'2026-06-04 07:00', av:'SA', color:'#F59E0B' }
],
tickets: [
{ id:'TKT-001', title:'Laptop keyboard not working', asset:'AST-2025-001', assetName:'Dell Latitude 5540', priority:'High', status:'In Progress', assignedTo:'Kavya Nair', created:'2025-05-26', dept:'IT', category:'Hardware Failure' },
{ id:'TKT-002', title:'AC not cooling HR floor', asset:'AST-2025-005', assetName:'Daikin 1.5T Split AC', priority:'Critical', status:'In Progress', assignedTo:'External AMC', created:'2025-05-27', dept:'HR', category:'Performance Issue' },
{ id:'TKT-003', title:'Printer paper jam frequent', asset:'AST-2025-002', assetName:'HP LaserJet Pro M404', priority:'Medium', status:'Open', assignedTo:'Unassigned', created:'2025-05-28', dept:'Finance', category:'Mechanical Issue' },
{ id:'TKT-004', title:'Projector lamp replacement', asset:'AST-2025-007', assetName:'Epson EB-2250U Projector', priority:'Low', status:'Pending Parts', assignedTo:'Kavya Nair', created:'2025-05-22', dept:'Admin', category:'Consumable Replacement' },
{ id:'TKT-005', name:'Photocopier not turning on', asset:'AST-2025-016', assetName:'Ricoh Aficio Photocopier', priority:'High', status:'Open', assignedTo:'Unassigned', created:'2025-05-28', dept:'Admin', category:'Electrical Issue' },
{ id:'TKT-006', title:'Network switch port failure', asset:'AST-2025-008', assetName:'Cisco Catalyst 9300', priority:'Critical', status:'Resolved', assignedTo:'IT Team', created:'2025-05-24', dept:'IT', category:'Network Issue' },
{ id:'TKT-007', title:'UPS battery replacement due', asset:'AST-2025-012', assetName:'APC Smart UPS 3KVA', priority:'High', status:'Pending Parts', assignedTo:'Vendor', created:'2025-05-25', dept:'IT', category:'Battery Replacement' },
{ id:'TKT-008', title:'Server fan making loud noise', asset:'AST-2025-004', assetName:'Dell PowerEdge R740', priority:'Medium', status:'In Progress', assignedTo:'Dell Support',created:'2025-05-27', dept:'IT', category:'Hardware Issue' }
{ id:'TKT-001', title:'Laptop keyboard not working', asset:'AST-2025-001', assetName:'Dell Latitude 5540', priority:'High', status:'In Progress', assignedTo:'Kavya Nair', created:'2026-05-26', dept:'IT', category:'Hardware Failure' },
{ id:'TKT-002', title:'AC not cooling HR floor', asset:'AST-2025-005', assetName:'Daikin 1.5T Split AC', priority:'Critical', status:'In Progress', assignedTo:'External AMC', created:'2026-05-27', dept:'HR', category:'Performance Issue' },
{ id:'TKT-003', title:'Printer paper jam frequent', asset:'AST-2025-002', assetName:'HP LaserJet Pro M404', priority:'Medium', status:'Open', assignedTo:'Unassigned', created:'2026-06-02', dept:'Finance', category:'Mechanical Issue' },
{ id:'TKT-004', title:'Projector lamp replacement', asset:'AST-2025-007', assetName:'Epson EB-2250U Projector', priority:'Low', status:'Pending Parts', assignedTo:'Kavya Nair', created:'2026-05-22', dept:'Admin', category:'Consumable Replacement' },
{ id:'TKT-005', title:'Photocopier not turning on', asset:'AST-2025-016', assetName:'Ricoh Aficio Photocopier', priority:'High', status:'Open', assignedTo:'Unassigned', created:'2026-06-04', dept:'Admin', category:'Electrical Issue' },
{ id:'TKT-006', title:'Network switch port failure', asset:'AST-2025-008', assetName:'Cisco Catalyst 9300', priority:'Critical', status:'Resolved', assignedTo:'IT Team', created:'2026-05-24', dept:'IT', category:'Network Issue' },
{ id:'TKT-007', title:'UPS battery replacement due', asset:'AST-2025-012', assetName:'APC Smart UPS 3KVA', priority:'High', status:'Pending Parts', assignedTo:'Vendor', created:'2026-05-25', dept:'IT', category:'Battery Replacement' },
{ id:'TKT-008', title:'Server fan making loud noise', asset:'AST-2025-004', assetName:'Dell PowerEdge R740', priority:'Medium', status:'In Progress', assignedTo:'Dell Support',created:'2026-05-27', dept:'IT', category:'Hardware Issue' }
],
purchaseRequests: [
{ id:'PR-2025-001', item:'Dell Laptop i7 Gen 13', qty:5, estCost:425000, dept:'IT', requester:'Priya Kumar', status:'Approved', date:'2025-05-10', justification:'New developer hires Q2 onboarding', poRef:'PO-2025-001' },
{ id:'PR-2025-002', item:'Office Chairs Mesh Type', qty:20, estCost:280000, dept:'Admin', requester:'Deepak Joshi', status:'Pending', date:'2025-05-20', justification:'Old chairs worn out, ergonomic upgrade', poRef:null },
{ id:'PR-2025-003', item:'TP-Link WAP EAP670', qty:8, estCost:96000, dept:'IT', requester:'Kavya Nair', status:'Approved', date:'2025-05-18', justification:'Wi-Fi dead zones on floor 3 and 4', poRef:'PO-2025-002' },
{ id:'PR-2025-004', item:'Epson Projector Lamp Kit', qty:2, estCost:14000, dept:'Admin', requester:'Deepak Joshi', status:'Pending', date:'2025-05-25', justification:'TKT-004 Lamp life expired', poRef:null },
{ id:'PR-2025-005', item:'UPS Replacement Batteries', qty:1, estCost:18000, dept:'IT', requester:'Arjun Sharma', status:'Submitted', date:'2025-05-27', justification:'TKT-007 Battery at 15% health', poRef:null },
{ id:'PR-2025-006', item:'Ricoh Toner Cartridges', qty:10, estCost:22000, dept:'Admin', requester:'Deepak Joshi', status:'Submitted', date:'2025-05-28', justification:'Stock depleted photocopier usage high', poRef:null },
{ id:'PR-2025-007', item:'iPad Pro 12.9" M4', qty:3, estCost:195000, dept:'Marketing', requester:'Sneha Patel', status:'Rejected', date:'2025-05-15', justification:'Field sales presentations & demos', poRef:null },
{ id:'PR-2025-008', item:'Cisco IP Phones 8841', qty:15, estCost:277500, dept:'HR', requester:'Anita Singh', status:'Draft', date:'2025-05-28', justification:'Office expansion new HR team seats', poRef:null }
{ id:'PR-2025-001', item:'Dell Laptop i7 Gen 13', qty:5, estCost:425000, dept:'IT', requester:'Priya Kumar', status:'Approved', date:'2026-05-10', justification:'New developer hires Q2 onboarding', poRef:'PO-2025-001' },
{ id:'PR-2025-002', item:'Office Chairs Mesh Type', qty:20, estCost:280000, dept:'Admin', requester:'Deepak Joshi', status:'Pending', date:'2026-05-20', justification:'Old chairs worn out, ergonomic upgrade', poRef:null },
{ id:'PR-2025-003', item:'TP-Link WAP EAP670', qty:8, estCost:96000, dept:'IT', requester:'Kavya Nair', status:'Approved', date:'2026-05-18', justification:'Wi-Fi dead zones on floor 3 and 4', poRef:'PO-2025-002' },
{ id:'PR-2025-004', item:'Epson Projector Lamp Kit', qty:2, estCost:14000, dept:'Admin', requester:'Deepak Joshi', status:'Pending', date:'2026-05-25', justification:'TKT-004 Lamp life expired', poRef:null },
{ id:'PR-2025-005', item:'UPS Replacement Batteries', qty:1, estCost:18000, dept:'IT', requester:'Arjun Sharma', status:'Submitted', date:'2026-06-02', justification:'TKT-007 Battery at 15% health', poRef:null },
{ id:'PR-2025-006', item:'Ricoh Toner Cartridges', qty:10, estCost:22000, dept:'Admin', requester:'Deepak Joshi', status:'Submitted', date:'2026-06-03', justification:'Stock depleted photocopier usage high', poRef:null },
{ id:'PR-2025-007', item:'iPad Pro 12.9" M4', qty:3, estCost:195000, dept:'Marketing', requester:'Sneha Patel', status:'Rejected', date:'2026-05-15', justification:'Field sales presentations & demos', poRef:null },
{ id:'PR-2025-008', item:'Cisco IP Phones 8841', qty:15, estCost:277500, dept:'HR', requester:'Anita Singh', status:'Draft', date:'2026-06-04', justification:'Office expansion new HR team seats', poRef:null }
],
vendors: [
@ -85,7 +98,9 @@ const AMS = {
{ id:'v003', name:'Cisco Systems India', contact:'Arun Kumar', email:'arun.k@cisco.com', phone:'+91-80-4156-7890', cat:'Networking', gst:'29AAACI0141G1ZM', rating:4.8, status:'Active', contracts:4 },
{ id:'v004', name:'Daikin Aircon India', contact:'Ravi Shenoy', email:'ravi.s@daikin.in', phone:'+91-80-2222-3456', cat:'HVAC', gst:'29AACCD0524P1ZF', rating:3.9, status:'Active', contracts:1 },
{ id:'v005', name:'Godrej Interio', contact:'Priya Menon', email:'priya.m@godrej.com', phone:'+91-22-6796-5000', cat:'Furniture', gst:'27AAACG0534E1ZD', rating:4.0, status:'Active', contracts:1 },
{ id:'v006', name:'Epson India Pvt. Ltd.', contact:'Kiran Shah', email:'kiran.s@epson.in', phone:'+91-80-4600-1234', cat:'AV Equipment', gst:'29AAACE0149N1Z8', rating:4.3, status:'Active', contracts:2 }
{ id:'v006', name:'Epson India Pvt. Ltd.', contact:'Kiran Shah', email:'kiran.s@epson.in', phone:'+91-80-4600-1234', cat:'AV Equipment', gst:'29AAACE0149N1Z8', rating:4.3, status:'Active', contracts:2 },
{ id:'v007', name:'Microsoft India', contact:'Rajiv Kaul', email:'r.kaul@microsoft.com', phone:'+91-80-4000-8888', cat:'Software', gst:'29AAACM0281F1ZH', rating:4.7, status:'Active', contracts:2 },
{ id:'v008', name:'Amazon Web Services', contact:'Sanjay Shah', email:'sanjshah@amazon.com', phone:'+91-80-4999-5555', cat:'Cloud Infra', gst:'29AAACA0182E1ZO', rating:4.9, status:'Active', contracts:1 }
],
categories: [
@ -101,15 +116,17 @@ const AMS = {
{ id:'c10', name:'Vehicles', icon:'🚗', count:8, depM:'WDV', depR:15, life:8, parent:'Fleet', minStock:1 },
{ id:'c11', name:'HVAC', icon:'❄️', count:45, depM:'SLM', depR:15, life:7, parent:'Building Assets', minStock:2 },
{ id:'c12', name:'Power Equipment', icon:'🔋', count:22, depM:'SLM', depR:25, life:4, parent:'Electrical', minStock:2 },
{ id:'c13', name:'Storage', icon:'💾', count:14, depM:'SLM', depR:20, life:5, parent:'IT Equipment', minStock:2 }
{ id:'c13', name:'Storage', icon:'💾', count:14, depM:'SLM', depR:20, life:5, parent:'IT Equipment', minStock:2 },
{ id:'c14', name:'Software Licenses',icon:'🔑', count:3, depM:'SLM', depR:100,life:1, parent:'Digital Assets', minStock:0 },
{ id:'c15', name:'Cloud Subscriptions',icon:'☁️', count:1, depM:'SLM', depR:100,life:1, parent:'Digital Assets', minStock:0 }
],
departments: [
{ id:'d1', name:'Information Technology', code:'IT', head:'Priya Kumar', assetCount:384, headcount:45 },
{ id:'d2', name:'Finance & Accounts', code:'FIN', head:'Rahul Mehta', assetCount:156, headcount:23 },
{ id:'d1', name:'Information Technology', code:'IT', head:'Priya Kumar', assetCount:387, headcount:45 },
{ id:'d2', name:'Finance & Accounts', code:'FIN', head:'Rahul Mehta', assetCount:157, headcount:23 },
{ id:'d3', name:'Human Resources', code:'HR', head:'Anita Singh', assetCount:112, headcount:18 },
{ id:'d4', name:'Operations', code:'OPS', head:'Vikram Reddy', assetCount:298, headcount:67 },
{ id:'d5', name:'Marketing', code:'MKT', head:'Sneha Patel', assetCount:187, headcount:31 },
{ id:'d5', name:'Marketing', code:'MKT', head:'Sneha Patel', assetCount:189, headcount:31 },
{ id:'d6', name:'Administration', code:'ADMIN', head:'Deepak Joshi', assetCount:110, headcount:12 }
],
@ -127,16 +144,16 @@ const AMS = {
amcContracts: [
{ id:'AMC-001', vendor:'Daikin Aircon India', scope:'25 AC units Annual Maintenance', value:180000, start:'2025-04-01', end:'2026-03-31', status:'Active', nextService:'2025-07-01' },
{ id:'AMC-002', vendor:'Dell India Pvt. Ltd.', scope:'Server Infra 24×7 Onsite Support',value:450000, start:'2025-01-01', end:'2025-12-31', status:'Active', nextService:'2025-06-15' },
{ id:'AMC-003', vendor:'Cisco Systems India', scope:'Network Infrastructure AMC', value:280000, start:'2024-07-01', end:'2025-06-30', status:'Expiring',nextService:'2025-06-01' },
{ id:'AMC-003', vendor:'Cisco Systems India', scope:'Network Infrastructure AMC', value:280000, start:'2025-07-01', end:'2026-06-30', status:'Active', nextService:'2025-09-01' },
{ id:'AMC-004', vendor:'Godrej Security', scope:'Physical Access Control Systems', value:95000, start:'2025-03-01', end:'2026-02-28', status:'Active', nextService:'2025-09-01' }
],
pmSchedule: [
{ id:'PM-001', asset:'Daikin 1.5T Split AC', assetId:'AST-2025-005', freq:'Quarterly', lastDone:'2025-02-15', nextDue:'2025-05-15', status:'Overdue', assignee:'Daikin AMC Team' },
{ id:'PM-002', asset:'Dell PowerEdge R740 Server', assetId:'AST-2025-004', freq:'Monthly', lastDone:'2025-04-28', nextDue:'2025-05-28', status:'Due Today', assignee:'IT Team' },
{ id:'PM-003', asset:'Honda City (Company Car)', assetId:'AST-2025-014', freq:'Quarterly', lastDone:'2025-03-01', nextDue:'2025-06-01', status:'Upcoming', assignee:'Honda Service' },
{ id:'PM-004', asset:'APC Smart UPS 3KVA', assetId:'AST-2025-012', freq:'Half-Yearly',lastDone:'2024-12-10',nextDue:'2025-06-10', status:'Upcoming', assignee:'IT Team' },
{ id:'PM-005', asset:'Ricoh Aficio Photocopier', assetId:'AST-2025-016', freq:'Quarterly', lastDone:'2025-01-20', nextDue:'2025-04-20', status:'Overdue', assignee:'Ricoh Service' }
{ id:'PM-001', asset:'Daikin 1.5T Split AC', assetId:'AST-2025-005', freq:'Quarterly', lastDone:'2026-02-15', nextDue:'2026-05-15', status:'Overdue', assignee:'Daikin AMC Team' },
{ id:'PM-002', asset:'Dell PowerEdge R740 Server', assetId:'AST-2025-004', freq:'Monthly', lastDone:'2026-04-28', nextDue:'2026-05-28', status:'Overdue', assignee:'IT Team' },
{ id:'PM-003', asset:'Honda City (Company Car)', assetId:'AST-2025-014', freq:'Quarterly', lastDone:'2026-03-01', nextDue:'2026-06-01', status:'Upcoming', assignee:'Honda Service' },
{ id:'PM-004', asset:'APC Smart UPS 3KVA', assetId:'AST-2025-012', freq:'Half-Yearly',lastDone:'2025-12-10',nextDue:'2026-06-10', status:'Upcoming', assignee:'IT Team' },
{ id:'PM-005', asset:'Ricoh Aficio Photocopier', assetId:'AST-2025-016', freq:'Quarterly', lastDone:'2026-01-20', nextDue:'2026-04-20', status:'Overdue', assignee:'Ricoh Service' }
],
activityFeed: [
@ -159,6 +176,53 @@ const AMS = {
]
};
// ── LocalStorage Initialization ──────────────────────────────
let storeData = localStorage.getItem('AMS_STORE');
if (!storeData) {
localStorage.setItem('AMS_STORE', JSON.stringify(defaultData));
storeData = JSON.stringify(defaultData);
}
window.AMS = JSON.parse(storeData);
// ── Theme bootstrap (light default, dark optional, persisted) ─────
(function applySavedTheme() {
const saved = localStorage.getItem('AMS_THEME') || 'light';
document.documentElement.setAttribute('data-theme', saved);
})();
// Save back to LocalStorage
window.AMS.save = function() {
recomputeStats();
const cleanData = Object.assign({}, window.AMS);
delete cleanData.save;
delete cleanData.recompute;
localStorage.setItem('AMS_STORE', JSON.stringify(cleanData));
};
// ── Derive stats from live data so every screen stays consistent ──
function recomputeStats() {
const A = window.AMS;
if (!A || !A.assets) return;
const s = A.stats;
s.total = A.assets.length;
s.active = A.assets.filter(a => a.status === 'Active').length;
s.maintenance = A.assets.filter(a => a.status === 'Under Maintenance').length;
s.idle = A.assets.filter(a => a.status === 'Idle').length;
s.disposed = A.assets.filter(a => a.status === 'Disposed').length;
s.totalValue = A.assets.reduce((t, a) => t + (a.cost || 0), 0);
s.netValue = A.assets.reduce((t, a) => t + (a.value || 0), 0);
s.depreciated = s.totalValue - s.netValue;
s.pendingTickets = (A.tickets || []).filter(t => !['Resolved','Closed'].includes(t.status)).length;
s.pendingPRs = (A.purchaseRequests || []).filter(p => ['Pending','Submitted','Draft'].includes(p.status)).length;
s.expiringAMC = (A.amcContracts || []).filter(c => c.status === 'Active').length;
// Keep per-category counts aligned with actual assets
(A.categories || []).forEach(c => {
c.count = A.assets.filter(a => a.cat === c.name).length;
});
}
window.AMS.recompute = recomputeStats;
recomputeStats();
// ── Helper Functions ──────────────────────────────────────────
function fmt(n, cur='₹') {
if (n >= 1e7) return `${cur}${(n/1e7).toFixed(2)}Cr`;

View File

@ -1,91 +1,74 @@
// js/sidebar.js — shared sidebar & topbar HTML injector
// Include this after data.js and app.js in every authenticated page.
// Call: renderShell(pageTitle, pageSubtitle) inside <body> before main content.
// ================================================================
// AMS — Shared Sidebar (single source of truth)
// Include AFTER data.js and BEFORE app.js on every authenticated
// page. Renders into <aside class="sidebar" id="appSidebar">.
// Editing the NAV array here updates the menu on ALL pages.
// ================================================================
function renderShell(pageTitle, pageSubtitle = '') {
const u = window.AMS ? AMS.currentUser : { name:'Arjun Sharma', role:'Asset Manager', avatar:'AS' };
const notifUnread = window.AMS ? AMS.notifications.filter(n=>!n.read).length : 0;
const sidebarHTML = `
<aside class="sidebar" id="appSidebar">
(function () {
const NAV = [
{ section: 'Overview' },
{ href: 'dashboard.html', icon: 'layout-dashboard', label: 'Dashboard' },
{ section: 'Assets' },
{ href: 'assets.html', icon: 'boxes', label: 'All Assets' },
{ href: 'asset-create.html', icon: 'plus-circle', label: 'Add Asset' },
{ href: 'renewals.html', icon: 'clock', label: 'Renewals & Alerts' },
{ section: 'Operations' },
{ href: 'maintenance.html', icon: 'wrench', label: 'Maintenance', badge: true },
{ href: 'reports.html', icon: 'bar-chart-3', label: 'Reports & Audit' },
{ section: 'Administration' },
{ href: 'users.html', icon: 'users', label: 'Users & Roles' },
{ href: 'settings.html', icon: 'settings', label: 'Settings' },
];
// Pages that aren't nav targets but should highlight a parent item.
const ACTIVE_ALIAS = { 'asset-detail.html': 'assets.html' };
window.NAV_ACTIVE_ALIAS = ACTIVE_ALIAS;
function currentActive() {
const page = location.pathname.split('/').pop() || 'dashboard.html';
return ACTIVE_ALIAS[page] || page;
}
function renderSidebar() {
const host = document.getElementById('appSidebar');
if (!host) return;
const active = currentActive();
const u = (window.AMS && AMS.currentUser) || { name: 'Arjun Sharma', role: 'Asset Manager', avatar: 'AS' };
const badgeCount = (window.AMS && AMS.stats) ? AMS.stats.pendingTickets : 12;
const esc = (s) => String(s).replace(/&/g, '&amp;');
const nav = NAV.map((item) => {
if (item.section) return `<div class="nav-section-label">${item.section}</div>`;
const cls = 'nav-item' + (item.href === active ? ' active' : '');
const badge = item.badge ? ` <span class="nav-badge">${badgeCount}</span>` : '';
return `<a href="${item.href}" class="${cls}"><span class="nav-icon"><i data-lucide="${item.icon}"></i></span> ${esc(item.label)}${badge}</a>`;
}).join('\n ');
host.innerHTML = `
<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 class="logo-icon"><i data-lucide="package"></i></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 <span class="nav-badge" style="background:var(--warning)">5</span></a>
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement <span class="nav-badge">${window.AMS?AMS.stats.pendingPRs:8}</span></a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><span class="nav-icon">🔧</span> Maintenance <span class="nav-badge">${window.AMS?AMS.stats.pendingTickets:12}</span></a>
<a href="reports.html" class="nav-item"><span class="nav-icon">📈</span> Reports &amp; Audit</a>
<div class="nav-section-label">Administration</div>
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users &amp; Roles</a>
<a href="settings.html" class="nav-item"><span class="nav-icon"></span> Settings</a>
${nav}
</nav>
<div class="sidebar-footer">
<div class="user-card" onclick="showToast('Profile','User profile settings coming soon','info')">
<div class="user-av" id="sidebarUserAv">${u.avatar}</div>
<div class="user-card">
<div class="user-av" id="sidebarUserAv">${esc(u.avatar || 'AS')}</div>
<div style="flex:1;min-width:0">
<div class="user-name" id="sidebarUserName">${u.name}</div>
<div class="user-role" id="sidebarUserRole">${u.role}</div>
<div class="user-name" id="sidebarUserName">${esc(u.name)}</div>
<div class="user-role" id="sidebarUserRole">${esc(u.role)}</div>
</div>
<span style="color:var(--text-muted);font-size:14px"></span>
</div>
</div>
</aside>`;
</div>`;
const topbarHTML = `
<header class="topbar">
<div class="topbar-left">
<div class="topbar-title">${pageTitle}${pageSubtitle?`<span class="topbar-sub">${pageSubtitle}</span>`:''}</div>
</div>
<div class="topbar-search">
<span style="color:var(--text-muted);font-size:14px">🔍</span>
<input type="text" placeholder="Search assets, tickets, users…" id="globalSearch">
</div>
<div class="topbar-actions">
<div class="dropdown" style="position:relative">
<button class="icon-btn" id="notifBell" title="Notifications">
🔔
<span class="badge-dot" ${notifUnread===0?'style="display:none"':''}></span>
</button>
<div class="notif-panel" id="notifPanel">
<div class="notif-header">
<span class="notif-title">Notifications</span>
<button class="btn btn-ghost btn-sm" onclick="markAllRead()" style="font-size:11px">Mark all read</button>
</div>
<div id="notifList"></div>
<div style="padding:10px 16px;text-align:center;border-top:1px solid var(--border)">
<a href="#" style="font-size:12px;color:var(--primary-light)">View all notifications</a>
</div>
</div>
</div>
<button class="icon-btn" title="Help" onclick="showToast('Help','Documentation is available at /docs','info')"></button>
<a href="index.html" class="icon-btn" title="Logout" onclick="return confirm('Log out?')">🚪</a>
</div>
</header>`;
if (window.lucide) lucide.createIcons();
}
document.body.innerHTML = sidebarHTML + `<div class="main-wrapper">${topbarHTML}<main class="content animate-fadein" id="mainContent"></main></div>` + document.body.innerHTML;
}
function markAllRead() {
if (window.AMS) AMS.notifications.forEach(n => n.read = true);
populateNotifBadge();
renderNotifPanel();
showToast('All caught up!','All notifications marked as read','success');
}
window.renderSidebar = renderSidebar;
// Placeholder is already in the DOM (script runs at end of <body>).
renderSidebar();
})();

View File

@ -8,33 +8,14 @@
</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>
<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">🚪</a>
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
</div>
</header>
@ -59,7 +40,7 @@
<!-- 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>
<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>
@ -118,27 +99,25 @@
<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>
<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"><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>
<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" placeholder="Brief description of the issue"></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">
<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 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"><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>
<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>
@ -155,12 +134,12 @@
<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-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"><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 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"></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>
@ -175,13 +154,13 @@
<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 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" rows="2" placeholder="What assets/services are covered"></textarea></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"></div>
<div class="form-group"><label class="form-label">Valid To</label><input type="date" class="form-input"></div>
<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">
@ -192,7 +171,9 @@
</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', () => {
@ -235,8 +216,8 @@ function renderKanban() {
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-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>
@ -259,9 +240,9 @@ 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><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>
@ -335,20 +316,25 @@ function updateStatus(id) {
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();
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';
renderKanban(); renderTicketList();
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='2025-05-28'; pm.status='Upcoming'; }
if(pm) { pm.lastDone=todayStr(); pm.status='Upcoming'; }
AMS.save();
renderPMSchedule();
showToast('PM Completed',`${id} marked as done. Next due auto-calculated.`,'success');
}
@ -356,13 +342,79 @@ function markPMDone(id) {
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 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'); }
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>

View File

@ -1,369 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Procurement | 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 active"><span class="nav-icon">🛒</span> Procurement</a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><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">Procurement <span class="topbar-sub">PR → Approval → PO → GRN → Asset</span></div></div>
<div class="topbar-actions">
<button class="btn btn-primary btn-sm" onclick="openModal('prModal')">+ New Purchase Request</button>
<a href="index.html" class="icon-btn">🚪</a>
</div>
</header>
<main class="content">
<!-- Workflow Visualization -->
<div class="card mb-4">
<div class="card-body">
<div style="font-size:11px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.6px;margin-bottom:12px">Procurement Workflow</div>
<div class="wf-steps">
<div class="wf-step done">✅ PR Raised</div><div class="wf-arr"></div>
<div class="wf-step done">✅ Dept Approval</div><div class="wf-arr"></div>
<div class="wf-step active">⏳ Finance Approval</div><div class="wf-arr"></div>
<div class="wf-step">📋 PO Generated</div><div class="wf-arr"></div>
<div class="wf-step">📦 GRN</div><div class="wf-arr"></div>
<div class="wf-step">🧾 Invoice Match</div><div class="wf-arr"></div>
<div class="wf-step">🏷️ Asset Created</div>
</div>
</div>
</div>
<!-- Stats -->
<div class="grid-4 mb-4" id="procStats"></div>
<!-- Tabs -->
<div class="tab-container">
<div class="tabs" data-group="proc">
<button class="tab-btn active" data-tab="tab-prs" data-group="proc">📋 Purchase Requests <span class="badge badge-danger" style="margin-left:4px;font-size:9px">8</span></button>
<button class="tab-btn" data-tab="tab-pos" data-group="proc">📄 Purchase Orders</button>
<button class="tab-btn" data-tab="tab-grn" data-group="proc">📦 GRN</button>
<button class="tab-btn" data-tab="tab-vendors" data-group="proc">🏭 Vendors</button>
</div>
<!-- PRs Tab -->
<div class="tab-content active" id="tab-prs" data-group="proc">
<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="prSearch" placeholder="Search PRs…"></div>
<select class="filter-sel" id="prStatusFilter">
<option value="">All Statuses</option>
<option>Draft</option><option>Submitted</option><option>Approved</option><option>Rejected</option><option>PO Raised</option>
</select>
<select class="filter-sel"><option>All Departments</option><option>IT</option><option>Finance</option><option>HR</option><option>Operations</option><option>Marketing</option></select>
</div>
<div class="table-wrapper">
<table class="data-table" id="prTable">
<thead><tr><th>PR No.</th><th>Item Description</th><th>Dept</th><th>Qty</th><th>Est. Cost</th><th>Requester</th><th>Date</th><th>Status</th><th>Actions</th></tr></thead>
<tbody id="prTbody"></tbody>
</table>
</div>
</div>
<!-- POs Tab -->
<div class="tab-content" id="tab-pos" data-group="proc">
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>PO Number</th><th>Vendor</th><th>Items</th><th>Total Value</th><th>Date</th><th>Status</th><th>GRN Status</th><th>Actions</th></tr></thead>
<tbody id="poTbody"></tbody>
</table>
</div>
</div>
<!-- GRN Tab -->
<div class="tab-content" id="tab-grn" data-group="proc">
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>GRN No.</th><th>PO Ref.</th><th>Vendor</th><th>Items</th><th>Received Qty</th><th>Date</th><th>Received By</th><th>3-Way Match</th></tr></thead>
<tbody id="grnTbody"></tbody>
</table>
</div>
</div>
<!-- Vendors Tab -->
<div class="tab-content" id="tab-vendors" data-group="proc">
<div class="flex justify-between mb-4">
<div class="search-wrap" style="max-width:280px"><span style="color:var(--text-muted)">🔍</span><input type="text" placeholder="Search vendors…"></div>
<button class="btn btn-primary btn-sm" onclick="openModal('vendorModal')">+ Add Vendor</button>
</div>
<div class="grid-3" id="vendorGrid"></div>
</div>
</div>
</main>
</div>
</div>
<!-- PR Modal -->
<div class="modal-overlay" id="prModal">
<div class="modal modal-lg">
<div class="modal-header"><span class="modal-title">New Purchase Request</span><button class="modal-close"></button></div>
<div class="modal-body">
<div class="form-row">
<div class="form-group"><label class="form-label">PR Number</label><input class="form-input" value="PR-2025-009" readonly></div>
<div class="form-group"><label class="form-label">Department <span class="req">*</span></label><select class="form-select"><option>IT</option><option>Finance</option><option>HR</option><option>Operations</option><option>Marketing</option><option>Admin</option></select></div>
</div>
<div class="form-group"><label class="form-label">Item Description <span class="req">*</span></label><input class="form-input" placeholder="e.g. Dell Laptop i7 Gen 13 (×5)"></div>
<div class="form-row">
<div class="form-group"><label class="form-label">Quantity <span class="req">*</span></label><input type="number" class="form-input" placeholder="0" min="1"></div>
<div class="form-group"><label class="form-label">Estimated Unit Cost (₹) <span class="req">*</span></label><input type="number" class="form-input" placeholder="0.00"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Budget Code</label><input class="form-input" placeholder="CC-IT-001"></div>
<div class="form-group"><label class="form-label">Required By Date</label><input type="date" class="form-input"></div>
</div>
<div class="form-group"><label class="form-label">Business Justification <span class="req">*</span></label><textarea class="form-textarea" placeholder="Why is this purchase needed? Link to business objective or ticket…" rows="3"></textarea></div>
<div class="form-group"><label class="form-label">Attachments</label><input type="file" class="form-input" multiple accept=".pdf,.doc,.jpg,.png"></div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('prModal')">Cancel</button>
<button class="btn btn-secondary" onclick="savePR('Draft')">Save Draft</button>
<button class="btn btn-primary" onclick="savePR('Submitted')">Submit for Approval</button>
</div>
</div>
</div>
<!-- Vendor Modal -->
<div class="modal-overlay" id="vendorModal">
<div class="modal modal-lg">
<div class="modal-header"><span class="modal-title">Add Vendor</span><button class="modal-close"></button></div>
<div class="modal-body">
<div class="form-row">
<div class="form-group"><label class="form-label">Company Name <span class="req">*</span></label><input class="form-input" placeholder="Vendor company name"></div>
<div class="form-group"><label class="form-label">Contact Person</label><input class="form-input" placeholder="Primary contact name"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Email</label><input type="email" class="form-input" placeholder="vendor@company.com"></div>
<div class="form-group"><label class="form-label">Phone</label><input class="form-input" placeholder="+91-XXXXX-XXXXX"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">GST Number</label><input class="form-input" placeholder="29XXXXXXXXXX1ZX"></div>
<div class="form-group"><label class="form-label">Category</label><select class="form-select"><option>IT Hardware</option><option>Networking</option><option>Furniture</option><option>HVAC</option><option>Vehicles</option><option>AV Equipment</option><option>Other</option></select></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('vendorModal')">Cancel</button>
<button class="btn btn-primary" onclick="saveVendor()">Add Vendor</button>
</div>
</div>
</div>
<!-- PR Detail Modal -->
<div class="modal-overlay" id="prDetailModal">
<div class="modal modal-lg">
<div class="modal-header"><span class="modal-title" id="prDetailTitle">PR Details</span><button class="modal-close"></button></div>
<div class="modal-body" id="prDetailBody"></div>
<div class="modal-footer" id="prDetailFooter"></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();
renderPRs();
renderPOs();
renderGRNs();
renderVendors();
initTabs();
document.getElementById('prSearch').addEventListener('input', applyPRFilter);
document.getElementById('prStatusFilter').addEventListener('change', applyPRFilter);
});
function renderStats() {
const prs = AMS.purchaseRequests;
const pending = prs.filter(p=>p.status==='Submitted'||p.status==='Pending').length;
const approved = prs.filter(p=>p.status==='Approved'||p.status==='PO Raised').length;
const totalVal = prs.filter(p=>p.status!=='Rejected').reduce((a,p)=>a+p.estCost,0);
document.getElementById('procStats').innerHTML = [
{label:'Total PRs',icon:'📋',val:prs.length,color:'var(--primary)'},
{label:'Pending Approval',icon:'⏳',val:pending,color:'var(--warning)'},
{label:'Approved / PO Raised',icon:'✅',val:approved,color:'var(--success)'},
{label:'Total Value',icon:'💰',val:fmt(totalVal),color:'var(--cyan)'}
].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('');
}
function renderPRs(data) {
const prs = data || AMS.purchaseRequests;
document.getElementById('prTbody').innerHTML = prs.map(p=>`
<tr onclick="showPRDetail('${p.id}')" style="cursor:pointer">
<td><code style="color:var(--primary-light);font-size:11.5px">${p.id}</code></td>
<td><div style="font-weight:600;color:var(--text-primary)">${p.item}</div><div style="font-size:11px;color:var(--text-muted)">${p.justification.substring(0,50)}…</div></td>
<td><span class="badge badge-neutral">${p.dept}</span></td>
<td style="font-weight:700">${p.qty}</td>
<td style="font-weight:700">${fmt(p.estCost)}</td>
<td>${p.requester}</td>
<td>${fmtDate(p.date)}</td>
<td><span class="badge ${statusBadge(p.status)}">${p.status}</span></td>
<td onclick="event.stopPropagation()">
<div class="flex gap-1">
${p.status==='Submitted'||p.status==='Pending' ? `
<button class="btn btn-success btn-sm" onclick="approvePR('${p.id}')"></button>
<button class="btn btn-danger btn-sm" onclick="rejectPR('${p.id}')"></button>` :
p.status==='Approved' ? `<button class="btn btn-primary btn-sm" onclick="raisePO('${p.id}')">PO →</button>` :
`<button class="btn btn-ghost btn-sm" onclick="showPRDetail('${p.id}')">View</button>`
}
</div>
</td>
</tr>`).join('');
}
function applyPRFilter() {
const q = document.getElementById('prSearch').value.toLowerCase();
const st = document.getElementById('prStatusFilter').value;
const filtered = AMS.purchaseRequests.filter(p =>
(!q || p.item.toLowerCase().includes(q) || p.id.toLowerCase().includes(q) || p.requester.toLowerCase().includes(q)) &&
(!st || p.status === st)
);
renderPRs(filtered);
}
function showPRDetail(id) {
const p = AMS.purchaseRequests.find(x=>x.id===id);
if(!p) return;
document.getElementById('prDetailTitle').textContent = p.id + ' ' + p.item;
document.getElementById('prDetailBody').innerHTML = `
<div class="info-grid mb-4">
<div class="info-item"><div class="info-label">Status</div><div class="info-value"><span class="badge ${statusBadge(p.status)}">${p.status}</span></div></div>
<div class="info-item"><div class="info-label">Department</div><div class="info-value">${p.dept}</div></div>
<div class="info-item"><div class="info-label">Requester</div><div class="info-value">${p.requester}</div></div>
<div class="info-item"><div class="info-label">Date</div><div class="info-value">${fmtDate(p.date)}</div></div>
<div class="info-item"><div class="info-label">Quantity</div><div class="info-value">${p.qty}</div></div>
<div class="info-item"><div class="info-label">Estimated Cost</div><div class="info-value" style="font-weight:700">${fmt(p.estCost)}</div></div>
</div>
<div class="form-group"><div class="info-label">Business Justification</div><div class="info-value" style="background:var(--bg-surface);padding:12px;border-radius:var(--radius-md);font-size:13px;margin-top:4px">${p.justification}</div></div>
${p.poRef ? `<div class="alert alert-success mt-4"><div class="alert-icon"></div><div><div class="alert-title">PO Generated</div><div class="alert-text">Reference: ${p.poRef}</div></div></div>` : ''}
<div class="section-hdr mt-4">Approval Chain</div>
<div class="timeline">
<div class="timeline-item"><div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--success-bg);color:var(--success)"></div><div class="tl-line"></div></div><div class="tl-content"><div class="tl-title">Requester: ${p.requester}</div><div class="tl-desc">PR Submitted</div><div class="tl-time">${fmtDate(p.date)}</div></div></div>
<div class="timeline-item"><div class="tl-icon-wrap"><div class="tl-icon" style="background:${p.status!=='Draft'?'var(--success-bg)':'var(--bg-elevated)'};color:${p.status!=='Draft'?'var(--success)':'var(--text-muted)'}">${p.status!=='Draft'?'✅':'⏳'}</div><div class="tl-line"></div></div><div class="tl-content"><div class="tl-title">Dept Head Approval</div><div class="tl-desc">${p.dept} Head Review</div></div></div>
<div class="timeline-item"><div class="tl-icon-wrap"><div class="tl-icon" style="background:${['Approved','PO Raised','GRN Done'].includes(p.status)?'var(--success-bg)':'var(--bg-elevated)'};color:${['Approved','PO Raised','GRN Done'].includes(p.status)?'var(--success)':'var(--text-muted)'}">${['Approved','PO Raised','GRN Done'].includes(p.status)?'✅':'⏳'}</div></div><div class="tl-content"><div class="tl-title">Finance Approval</div><div class="tl-desc">Budget verification</div></div></div>
</div>`;
document.getElementById('prDetailFooter').innerHTML = p.status==='Submitted' ? `
<button class="btn btn-ghost" onclick="closeModal('prDetailModal')">Close</button>
<button class="btn btn-danger" onclick="rejectPR('${p.id}');closeModal('prDetailModal')">Reject</button>
<button class="btn btn-primary" onclick="approvePR('${p.id}');closeModal('prDetailModal')">Approve PR</button>` :
`<button class="btn btn-ghost" onclick="closeModal('prDetailModal')">Close</button>
${p.status==='Approved'?`<button class="btn btn-primary" onclick="raisePO('${p.id}');closeModal('prDetailModal')">Generate PO →</button>`:''}`;
openModal('prDetailModal');
}
function approvePR(id) {
const p = AMS.purchaseRequests.find(x=>x.id===id);
if(p) p.status = 'Approved';
renderPRs();
showToast('PR Approved',`${id} approved. Now raise a PO.`,'success');
}
function rejectPR(id) {
const p = AMS.purchaseRequests.find(x=>x.id===id);
if(p) p.status = 'Rejected';
renderPRs();
showToast('PR Rejected',`${id} has been rejected`,'warning');
}
function raisePO(prId) {
const p = AMS.purchaseRequests.find(x=>x.id===prId);
if(p) { p.status = 'PO Raised'; p.poRef = 'PO-2025-00' + Math.floor(Math.random()*9+1); }
renderPRs();
showToast('PO Generated',`Purchase Order ${p?.poRef} sent to vendor`,'success');
}
function renderPOs() {
const pos = [
{ id:'PO-2025-001', vendor:'Dell India Pvt. Ltd.', items:'5× Dell Latitude 5540', value:425000, date:'2025-05-12', status:'GRN Done', grn:'GRN-2025-041' },
{ id:'PO-2025-002', vendor:'TP-Link India', items:'8× WAP EAP670', value:96000, date:'2025-05-19', status:'PO Sent', grn:null },
{ id:'PO-2025-003', vendor:'Schneider Electric', items:'1× UPS Battery Set', value:18000, date:'2025-05-28', status:'PO Sent', grn:null }
];
document.getElementById('poTbody').innerHTML = pos.map(p=>`
<tr>
<td><code style="color:var(--primary-light);font-size:11.5px">${p.id}</code></td>
<td>${p.vendor}</td>
<td>${p.items}</td>
<td style="font-weight:700">${fmt(p.value)}</td>
<td>${fmtDate(p.date)}</td>
<td><span class="badge ${statusBadge(p.status)}">${p.status}</span></td>
<td>${p.grn ? `<code style="font-size:11px;color:var(--success)">${p.grn}</code>` : '<span class="text-muted">Pending</span>'}</td>
<td>
<button class="btn btn-ghost btn-sm" onclick="showToast('Download','PO PDF downloaded','info')">📥 PDF</button>
${!p.grn?`<button class="btn btn-secondary btn-sm" onclick="showToast('GRN','Record GRN for this PO','info')">Record GRN</button>`:''}
</td>
</tr>`).join('');
}
function renderGRNs() {
const grns = [
{ id:'GRN-2025-041', po:'PO-2025-001', vendor:'Dell India', items:'Dell Latitude 5540', qty:'5/5', date:'2025-05-14', by:'Arjun Sharma', match:'✅ Matched' },
{ id:'GRN-2025-040', po:'PO-2024-098', vendor:'Cisco Systems', items:'Cisco IP Phone 8841', qty:'5/5', date:'2025-05-22', by:'Kavya Nair', match:'✅ Matched' },
{ id:'GRN-2025-039', po:'PO-2024-095', vendor:'HP India', items:'HP LaserJet Pro M404', qty:'2/2', date:'2025-05-18', by:'Arjun Sharma', match:'⚠️ Invoice Pending' }
];
document.getElementById('grnTbody').innerHTML = grns.map(g=>`
<tr>
<td><code style="color:var(--primary-light);font-size:11.5px">${g.id}</code></td>
<td><code style="font-size:11px;color:var(--cyan-light)">${g.po}</code></td>
<td>${g.vendor}</td>
<td>${g.items}</td>
<td style="font-weight:700">${g.qty}</td>
<td>${fmtDate(g.date)}</td>
<td>${g.by}</td>
<td>${g.match}</td>
</tr>`).join('');
}
function renderVendors() {
document.getElementById('vendorGrid').innerHTML = AMS.vendors.map(v=>`
<div class="card" style="cursor:default">
<div class="card-body">
<div class="flex items-center gap-3 mb-3">
<div style="width:40px;height:40px;border-radius:var(--radius-md);background:var(--primary-glow);display:flex;align-items:center;justify-content:center;font-size:18px">🏭</div>
<div><div style="font-weight:700;font-size:13.5px">${v.name}</div><div style="font-size:11px;color:var(--text-muted)">${v.cat}</div></div>
</div>
<div class="info-grid" style="gap:8px;margin-bottom:14px">
<div class="info-item"><div class="info-label">Contact</div><div class="info-value" style="font-size:12px">${v.contact}</div></div>
<div class="info-item"><div class="info-label">GST</div><div class="info-value" style="font-size:11px">${v.gst}</div></div>
<div class="info-item"><div class="info-label">Rating</div><div class="info-value">⭐ ${v.rating}/5</div></div>
<div class="info-item"><div class="info-label">Active Contracts</div><div class="info-value">${v.contracts}</div></div>
</div>
<div class="flex gap-2">
<a href="mailto:${v.email}" class="btn btn-ghost btn-sm flex-1">📧 Email</a>
<button class="btn btn-secondary btn-sm flex-1" onclick="showToast('RFQ','RFQ sent to ${v.name}','success')">Send RFQ</button>
</div>
</div>
</div>`).join('');
}
function savePR(status) {
closeModal('prModal');
showToast(`PR ${status}`, status==='Submitted' ? 'PR sent for Dept Head approval' : 'Draft saved', status==='Submitted'?'success':'info');
}
function saveVendor() { closeModal('vendorModal'); showToast('Vendor Added','Vendor added to directory','success'); }
</script>
</body>
</html>

313
renewals.html Normal file
View File

@ -0,0 +1,313 @@
<!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 &amp; 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>

View File

@ -9,33 +9,14 @@
</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"><span class="nav-icon">🔧</span> Maintenance</a>
<a href="reports.html" class="nav-item active"><span class="nav-icon">📈</span> Reports &amp; Audit</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>
<aside class="sidebar" id="appSidebar"></aside>
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-left"><div class="topbar-title">Reports &amp; Audit</div></div>
<div class="topbar-actions">
<button class="btn btn-secondary btn-sm" onclick="exportAll()">📥 Export All Reports</button>
<a href="index.html" class="icon-btn">🚪</a>
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
</div>
</header>
@ -165,7 +146,9 @@
</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>
const reportDefs = [

View File

@ -8,33 +8,14 @@
</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"><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 active"><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>
<aside class="sidebar" id="appSidebar"></aside>
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-left"><div class="topbar-title">System Settings</div></div>
<div class="topbar-actions">
<button class="btn btn-primary btn-sm" id="saveSettingsBtn" onclick="saveSettings()">💾 Save All Settings</button>
<a href="index.html" class="icon-btn">🚪</a>
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
</div>
</header>
@ -54,7 +35,9 @@
</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>
const sections = [
@ -74,19 +57,19 @@ const settingsHTML = {
<div class="card-header"><span class="card-title">🏢 Organization Settings</span></div>
<div class="card-body">
<div class="form-row">
<div class="form-group"><label class="form-label">Company Name <span class="req">*</span></label><input class="form-input" value="${AMS ? AMS.company.name : 'Acme Corporation Pvt. Ltd.'}"></div>
<div class="form-group"><label class="form-label">Company Name <span class="req">*</span></label><input class="form-input" id="setCompanyName" value="${AMS ? AMS.company.name : 'Acme Corporation Pvt. Ltd.'}"></div>
<div class="form-group"><label class="form-label">Company Logo</label><div class="flex items-center gap-3"><div style="width:48px;height:48px;border-radius:var(--radius-md);background:var(--primary-glow);display:flex;align-items:center;justify-content:center;font-size:22px">📦</div><button class="btn btn-secondary btn-sm" onclick="showToast('Upload','Logo upload dialog','info')">Change Logo</button></div></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Currency</label><select class="form-select"><option selected>INR (₹)</option><option>USD ($)</option><option>EUR (€)</option><option>GBP (£)</option></select></div>
<div class="form-group"><label class="form-label">Currency</label><select class="form-select" id="setCurrency"><option selected>INR (₹)</option><option>USD ($)</option><option>EUR (€)</option><option>GBP (£)</option></select></div>
<div class="form-group"><label class="form-label">Financial Year</label><select class="form-select"><option selected>April March</option><option>January December</option><option>July June</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Date Format</label><select class="form-select"><option selected>DD-MMM-YYYY</option><option>DD/MM/YYYY</option><option>MM/DD/YYYY</option><option>YYYY-MM-DD</option></select></div>
<div class="form-group"><label class="form-label">Timezone</label><select class="form-select"><option selected>Asia/Kolkata (IST +5:30)</option><option>UTC</option><option>America/New_York</option></select></div>
</div>
<div class="form-group"><label class="form-label">Registered Address</label><textarea class="form-textarea" rows="2">12th Floor, Prestige Tech Park, Outer Ring Road, Bengaluru 560103</textarea></div>
<div class="form-group"><label class="form-label">GST Number</label><input class="form-input" value="29AABCU9603R1ZX"></div>
<div class="form-group"><label class="form-label">Registered Address</label><textarea class="form-textarea" id="setAddress" rows="2">${AMS ? AMS.company.address : ''}</textarea></div>
<div class="form-group"><label class="form-label">GST Number</label><input class="form-input" id="setGst" value="${AMS ? AMS.company.gst : ''}"></div>
<div class="form-group"><label class="form-label">Asset ID Prefix</label><input class="form-input" value="AST" placeholder="e.g. AST, ACME, ITL"><div class="form-hint">IDs will be generated as: <code style="color:var(--primary-light)">AST-2025-001</code></div></div>
</div>
</div>`,
@ -98,9 +81,8 @@ const settingsHTML = {
<div class="form-group">
<label class="form-label">Theme</label>
<div class="flex gap-3 mt-2">
<label class="theme-opt selected"><div class="theme-swatch dark"></div><span>Dark (Default)</span></label>
<label class="theme-opt"><div class="theme-swatch light"></div><span>Light</span></label>
<label class="theme-opt"><div class="theme-swatch auto"></div><span>System Auto</span></label>
<label class="theme-opt" data-theme-opt="light" onclick="setTheme('light')"><div class="theme-swatch light"></div><span>Light (Default)</span></label>
<label class="theme-opt" data-theme-opt="dark" onclick="setTheme('dark')"><div class="theme-swatch dark"></div><span>Dark</span></label>
</div>
</div>
<div class="form-group mt-4">
@ -137,11 +119,11 @@ const settingsHTML = {
<table class="data-table" style="font-size:12.5px">
<thead><tr><th>Category</th><th>Default Method</th><th>Rate (% p.a.)</th><th>Useful Life (Years)</th></tr></thead>
<tbody>
${AMS.categories.map(c=>`<tr>
${AMS.categories.map(c=>`<tr data-cat="${c.id}">
<td>${c.icon} ${c.name}</td>
<td><select class="form-select" style="padding:4px 8px;font-size:11px"><option ${c.depM==='SLM'?'selected':''}>SLM</option><option ${c.depM==='WDV'?'selected':''}>WDV</option></select></td>
<td><input type="number" class="form-input" value="${c.depR}" style="width:70px;padding:4px 8px">%</td>
<td><input type="number" class="form-input" value="${c.life}" style="width:70px;padding:4px 8px"> yr</td>
<td><select class="form-select cat-depM" style="padding:4px 8px;font-size:11px"><option ${c.depM==='SLM'?'selected':''}>SLM</option><option ${c.depM==='WDV'?'selected':''}>WDV</option></select></td>
<td><input type="number" class="form-input cat-depR" value="${c.depR}" style="width:70px;padding:4px 8px">%</td>
<td><input type="number" class="form-input cat-life" value="${c.life}" style="width:70px;padding:4px 8px"> yr</td>
</tr>`).join('')}
</tbody>
</table>
@ -307,9 +289,39 @@ function loadSection(id) {
const el=document.getElementById('snav-'+id);
if(el)el.classList.add('active');
document.getElementById('settingsContent').innerHTML = settingsHTML[id] || `<div class="card"><div class="card-body">${id} settings</div></div>`;
if (typeof updateThemeUI === 'function') updateThemeUI();
}
function saveSettings() {
if (window.AMS) {
// Persist General (organization) settings if that section is rendered
const nameEl = document.getElementById('setCompanyName');
if (nameEl) {
AMS.company.name = nameEl.value.trim() || AMS.company.name;
const addr = document.getElementById('setAddress');
const gst = document.getElementById('setGst');
if (addr) AMS.company.address = addr.value.trim();
if (gst) AMS.company.gst = gst.value.trim();
const cur = document.getElementById('setCurrency');
if (cur) {
const map = { 'INR (₹)':['₹','INR'], 'USD ($)':['$','USD'], 'EUR (€)':['€','EUR'], 'GBP (£)':['£','GBP'] };
const [sym, code] = map[cur.value] || ['₹','INR'];
AMS.company.currency = sym; AMS.company.currencyCode = code;
}
}
// Persist Depreciation category defaults if that section is rendered
document.querySelectorAll('tr[data-cat]').forEach(tr => {
const cat = AMS.categories.find(c => c.id === tr.dataset.cat);
if (!cat) return;
const depM = tr.querySelector('.cat-depM');
const depR = tr.querySelector('.cat-depR');
const life = tr.querySelector('.cat-life');
if (depM) cat.depM = depM.value;
if (depR) cat.depR = parseInt(depR.value, 10) || cat.depR;
if (life) cat.life = parseInt(life.value, 10) || cat.life;
});
AMS.save();
}
document.getElementById('saveSettingsBtn').textContent = '✅ Saved!';
showToast('Settings Saved','All configuration changes have been applied','success');
setTimeout(()=>{ document.getElementById('saveSettingsBtn').textContent = '💾 Save All Settings'; },2500);

View File

@ -8,33 +8,14 @@
</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"><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 active"><span class="nav-icon">👥</span> Users &amp; Roles</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>
<aside class="sidebar" id="appSidebar"></aside>
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-left"><div class="topbar-title">Users &amp; Access Control</div></div>
<div class="topbar-actions">
<button class="btn btn-primary btn-sm" onclick="openModal('inviteModal')">+ Invite User</button>
<a href="index.html" class="icon-btn">🚪</a>
<a href="index.html" class="icon-btn" title="Logout"><i data-lucide="log-out"></i></a>
</div>
</header>
@ -58,7 +39,7 @@
<!-- Users List -->
<div class="tab-content active" id="tab-users" data-group="usr">
<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="userSearch" placeholder="Search users…"></div>
<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="userSearch" placeholder="Search users…"></div>
<select class="filter-sel"><option>All Roles</option><option>Super Admin</option><option>Asset Manager</option><option>IT Head</option><option>Finance Head</option><option>Employee</option></select>
<select class="filter-sel"><option>All Departments</option><option>IT</option><option>Finance</option><option>HR</option><option>Operations</option><option>Marketing</option></select>
<select class="filter-sel"><option>All Statuses</option><option>Active</option><option>Inactive</option></select>
@ -118,11 +99,11 @@
<div class="modal">
<div class="modal-header"><span class="modal-title">Invite New User</span><button class="modal-close"></button></div>
<div class="modal-body">
<div class="form-group"><label class="form-label">Full Name <span class="req">*</span></label><input class="form-input" placeholder="Employee full name"></div>
<div class="form-group"><label class="form-label">Email Address <span class="req">*</span></label><input type="email" class="form-input" placeholder="employee@acmecorp.com"></div>
<div class="form-group"><label class="form-label">Full Name <span class="req">*</span></label><input class="form-input" id="inviteName" placeholder="Employee full name"></div>
<div class="form-group"><label class="form-label">Email Address <span class="req">*</span></label><input type="email" class="form-input" id="inviteEmail" placeholder="employee@acmecorp.com"></div>
<div class="form-row">
<div class="form-group"><label class="form-label">Department <span class="req">*</span></label><select class="form-select"><option>IT</option><option>Finance</option><option>HR</option><option>Operations</option><option>Marketing</option><option>Admin</option></select></div>
<div class="form-group"><label class="form-label">Role <span class="req">*</span></label><select class="form-select"><option>Employee</option><option>Asset Coordinator</option><option>Department Head</option><option>Asset Manager</option><option>Finance Head</option></select></div>
<div class="form-group"><label class="form-label">Department <span class="req">*</span></label><select class="form-select" id="inviteDept"><option>IT</option><option>Finance</option><option>HR</option><option>Operations</option><option>Marketing</option><option>Admin</option></select></div>
<div class="form-group"><label class="form-label">Role <span class="req">*</span></label><select class="form-select" id="inviteRole"><option>Employee</option><option>Asset Coordinator</option><option>Department Head</option><option>Asset Manager</option><option>Finance Head</option></select></div>
</div>
<div class="form-group"><label class="form-label">Welcome Message (optional)</label><textarea class="form-textarea" rows="2" placeholder="Custom message to include in invite email…"></textarea></div>
<div class="alert alert-info" style="margin-top:8px">
@ -150,7 +131,9 @@
</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>
const roles = [
@ -191,13 +174,13 @@ function renderUsers(data) {
<tr>
<td>
<div class="flex items-center gap-3">
<div style="width:34px;height:34px;border-radius:50%;background:${u.color}22;color:${u.color};display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">${u.av}</div>
<div><div style="font-weight:600;color:var(--text-primary)">${u.name}</div><div style="font-size:11px;color:var(--text-muted)">${u.dept}</div></div>
<div style="width:34px;height:34px;border-radius:50%;background:${u.color}22;color:${u.color};display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0">${escapeHtml(u.av)}</div>
<div><div style="font-weight:600;color:var(--text-primary)">${escapeHtml(u.name)}</div><div style="font-size:11px;color:var(--text-muted)">${escapeHtml(u.dept)}</div></div>
</div>
</td>
<td style="font-size:12.5px">${u.email}</td>
<td><span class="badge badge-primary" style="font-size:10px">${u.role}</span></td>
<td>${u.dept}</td>
<td style="font-size:12.5px">${escapeHtml(u.email)}</td>
<td><span class="badge badge-primary" style="font-size:10px">${escapeHtml(u.role)}</span></td>
<td>${escapeHtml(u.dept)}</td>
<td><span class="badge ${statusBadge(u.status)}">${u.status}</span></td>
<td style="font-size:12px;color:var(--text-muted)">${u.lastLogin}</td>
<td><span style="color:${u.id!=='u009'?'var(--success)':'var(--danger)'}">${u.id!=='u009'?'✅ Enabled':'❌ Disabled'}</span></td>
@ -266,19 +249,37 @@ function renderSessions() {
</tr>`).join('');
}
let editingUserId = null;
const deptColors = ['#6366F1','#10B981','#F59E0B','#A855F7','#06B6D4','#EF4444','#3B82F6'];
function editUser(id) {
const u = AMS.users.find(x=>x.id===id);
if(!u) return;
editingUserId = id;
const allRoles = ['Asset Manager','IT Head','Finance Head','Asset Coordinator','Department Head','Employee','Super Admin'];
const allDepts = ['IT','Finance','HR','Operations','Marketing','Admin'];
document.getElementById('editUserBody').innerHTML = `
<div class="form-row"><div class="form-group"><label class="form-label">Full Name</label><input class="form-input" value="${u.name}"></div>
<div class="form-group"><label class="form-label">Email</label><input class="form-input" type="email" value="${u.email}" readonly></div></div>
<div class="form-row"><div class="form-group"><label class="form-label">Role</label><select class="form-select"><option ${u.role==='Asset Manager'?'selected':''}>Asset Manager</option><option ${u.role==='IT Head'?'selected':''}>IT Head</option><option ${u.role==='Finance Head'?'selected':''}>Finance Head</option><option>Employee</option><option>Super Admin</option></select></div>
<div class="form-group"><label class="form-label">Department</label><select class="form-select"><option>IT</option><option>Finance</option><option>HR</option><option>Operations</option><option>Marketing</option><option>Admin</option></select></div></div>
<div class="form-group"><label class="form-label">Status</label><select class="form-select"><option ${u.status==='Active'?'selected':''}>Active</option><option ${u.status==='Inactive'?'selected':''}>Inactive</option></select></div>`;
<div class="form-row"><div class="form-group"><label class="form-label">Full Name</label><input class="form-input" id="editUserName" value="${escapeHtml(u.name)}"></div>
<div class="form-group"><label class="form-label">Email</label><input class="form-input" id="editUserEmail" type="email" value="${escapeHtml(u.email)}" readonly></div></div>
<div class="form-row"><div class="form-group"><label class="form-label">Role</label><select class="form-select" id="editUserRole">${allRoles.map(r=>`<option ${u.role===r?'selected':''}>${r}</option>`).join('')}</select></div>
<div class="form-group"><label class="form-label">Department</label><select class="form-select" id="editUserDept">${allDepts.map(d=>`<option ${u.dept===d?'selected':''}>${d}</option>`).join('')}</select></div></div>
<div class="form-group"><label class="form-label">Status</label><select class="form-select" id="editUserStatus"><option ${u.status==='Active'?'selected':''}>Active</option><option ${u.status==='Inactive'?'selected':''}>Inactive</option></select></div>`;
openModal('editUserModal');
}
function saveUser() { closeModal('editUserModal'); showToast('User Updated','Changes saved','success'); }
function saveUser() {
const u = AMS.users.find(x=>x.id===editingUserId);
if(!u) { closeModal('editUserModal'); return; }
u.name = document.getElementById('editUserName').value.trim() || u.name;
u.role = document.getElementById('editUserRole').value;
u.dept = document.getElementById('editUserDept').value;
u.status = document.getElementById('editUserStatus').value;
u.av = u.name.split(' ').map(w=>w[0]).slice(0,2).join('').toUpperCase();
AMS.save();
renderUsers();
closeModal('editUserModal');
showToast('User Updated','Changes saved','success');
}
function revokeUser(id) {
confirmAction('Revoke Sessions','This will log out all active sessions for this user. They will need to login again.',()=>{
@ -290,6 +291,7 @@ function toggleStatus(id) {
const u = AMS.users.find(x=>x.id===id);
if(!u) return;
u.status = u.status==='Active' ? 'Inactive' : 'Active';
AMS.save();
renderUsers();
showToast('Status Updated',`${u.name} is now ${u.status}`, u.status==='Active'?'success':'warning');
}
@ -297,8 +299,26 @@ function toggleStatus(id) {
function revokeSession(user) { showToast('Session Revoked',`${user}'s session has been terminated`,'warning'); }
function sendInvite() {
const name = document.getElementById('inviteName').value.trim();
const email = document.getElementById('inviteEmail').value.trim();
if(!name || !email) { showToast('Missing Details','Name and email are required','error'); return; }
const num = AMS.users.reduce((m,u)=>Math.max(m, parseInt((u.id||'u0').slice(1))||0), 0) + 1;
AMS.users.push({
id: 'u' + String(num).padStart(3,'0'),
name, email,
role: document.getElementById('inviteRole').value,
dept: document.getElementById('inviteDept').value,
status: 'Active',
lastLogin: 'Never (invited)',
av: name.split(' ').map(w=>w[0]).slice(0,2).join('').toUpperCase(),
color: deptColors[AMS.users.length % deptColors.length]
});
AMS.save();
renderUsers();
closeModal('inviteModal');
showToast('Invite Sent','Invitation email sent with 48-hour setup link','success');
document.getElementById('inviteName').value = '';
document.getElementById('inviteEmail').value = '';
showToast('Invite Sent',`${name} added · invitation email sent with 48-hour setup link`,'success');
}
function confirmAction(title, msg, cb) {