chore: update project dependencies and internal vendor libraries
This commit is contained in:
parent
c35b02391e
commit
7bab488ce8
BIN
AMS_TestCases_AddNewAsset.xlsx
Normal file
BIN
AMS_TestCases_AddNewAsset.xlsx
Normal file
Binary file not shown.
@ -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');
|
||||
|
||||
@ -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> · 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>
|
||||
|
||||
125
assets.html
125
assets.html
@ -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 & Audit</a>
|
||||
<div class="nav-section-label">Administration</div>
|
||||
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users & 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>`;
|
||||
|
||||
2516
css/styles.css
2516
css/styles.css
File diff suppressed because it is too large
Load Diff
@ -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 & Audit</a>
|
||||
<div class="nav-section-label">Administration</div>
|
||||
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users & 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
300
e2e_runner.js
Normal 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); });
|
||||
24
index.html
24
index.html
@ -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) {
|
||||
|
||||
383
inventory.html
383
inventory.html
@ -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
271
js/app.js
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
// 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, '"').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 => {
|
||||
|
||||
188
js/data.js
188
js/data.js
@ -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`;
|
||||
|
||||
141
js/sidebar.js
141
js/sidebar.js
@ -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, '&');
|
||||
|
||||
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 & Audit</a>
|
||||
|
||||
<div class="nav-section-label">Administration</div>
|
||||
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users & 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();
|
||||
})();
|
||||
|
||||
158
maintenance.html
158
maintenance.html
@ -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>
|
||||
|
||||
369
procurement.html
369
procurement.html
@ -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
313
renewals.html
Normal 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 & 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>
|
||||
25
reports.html
25
reports.html
@ -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 & 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 & 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 = [
|
||||
|
||||
@ -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);
|
||||
|
||||
96
users.html
96
users.html
@ -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 & 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 & 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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user