feat: AMS V1 — multi-page HTML prototype

- Login page with animated floating stat cards
- Executive dashboard with Chart.js KPIs and activity feed
- Full asset registry (list, search, filter, bulk actions, QR)
- Asset detail page with 6 tabs (financial, maintenance, history…)
- 3-step asset creation wizard with category-specific fields
- Inventory: stock overview, GRN, location tree, physical audit
- Procurement: PR → approval → PO → GRN → asset lifecycle
- Maintenance: Kanban board, PM schedule, AMC contracts
- Reports: depreciation schedule, utilization, compliance gauge
- User management: roles, permission matrix, session control
- Settings: 8 config sections (org, depreciation, security, integrations)
- Premium dark UI with CSS variables, Chart.js 4, toast system
This commit is contained in:
Vaibhav Surve 2026-05-28 18:09:32 +05:30
commit 2dfe150a5c
17 changed files with 4915 additions and 0 deletions

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
.Spotlight-V100
.Trashes
# Editor
.idea/
.vscode/
*.swp
*.swo
# Node (if ever added)
node_modules/
npm-debug.log*
# Build artifacts
dist/
build/
*.zip

BIN
AMS_Product_Plan_v1.0.docx Normal file

Binary file not shown.

400
asset-create.html Normal file
View File

@ -0,0 +1,400 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Add New Asset | 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">
<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>
<!-- MAIN -->
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-left">
<a href="assets.html" class="btn btn-ghost btn-sm" style="margin-right:6px">← Back to Assets</a>
<div class="topbar-title">Add New Asset</div>
</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>
</div>
</header>
<main class="content">
<div style="display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start">
<!-- Step Sidebar -->
<div class="card p-5" id="stepsSidebar">
<div style="font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.6px;margin-bottom:14px">Creation Steps</div>
<div id="stepNav"></div>
<hr class="divider">
<div style="font-size:11.5px;color:var(--text-muted)">
<div class="mb-2">💡 <strong style="color:var(--text-secondary)">Auto-generated:</strong></div>
<div style="margin-bottom:4px">• Asset ID: <code style="color:var(--primary-light)" id="previewId">AST-2025-021</code></div>
<div>• QR code on save</div>
</div>
</div>
<!-- Form Area -->
<div id="formArea">
<!-- Step 1 -->
<div class="card" id="step1" data-step="1">
<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 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 class="form-group">
<label class="form-label">Category <span class="req">*</span></label>
<select class="form-select" id="assetCategory" onchange="renderCustomFields()">
<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>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Serial Number</label>
<input type="text" class="form-input" id="serialNo" placeholder="Manufacturer serial number">
</div>
<div class="form-group">
<label class="form-label">Model / Part Number</label>
<input type="text" class="form-input" id="modelNo" placeholder="Model identifier">
</div>
</div>
<div class="form-row">
<div class="form-group">
<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>
</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">
<hr class="divider">
<div class="section-hdr">Category-specific Fields</div>
<div id="customFieldsGrid" class="form-row"></div>
</div>
</div>
</div>
<!-- Step 2 -->
<div class="card mt-4" id="step2" data-step="2">
<div class="card-header"><span class="card-title">💰 Step 2: Purchase & Financial Details</span></div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<label class="form-label">Vendor / Supplier <span class="req">*</span></label>
<select class="form-select" id="assetVendor">
<option value="">Select vendor…</option>
<option>Dell India Pvt. Ltd.</option><option>HP India Pvt. Ltd.</option>
<option>Cisco Systems India</option><option>Apple India</option>
<option>Lenovo India</option><option>Daikin India</option>
<option>Samsung India</option><option>Godrej Interio</option>
<option value="new">+ Add New Vendor</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Purchase Date <span class="req">*</span></label>
<input type="date" class="form-input" id="purchaseDate" value="2025-05-28">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Purchase Cost (₹) <span class="req">*</span></label>
<div class="input-group">
<span class="input-prefix"></span>
<input type="number" class="form-input" id="purchaseCost" placeholder="0.00" oninput="calcDepreciation()" style="border-radius:0 var(--radius-md) var(--radius-md) 0;border-left:none">
</div>
</div>
<div class="form-group">
<label class="form-label">Invoice / PO Reference</label>
<input type="text" class="form-input" id="invoiceRef" placeholder="INV-2025-XXXX or PO-XXXX">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Warranty Expiry</label>
<input type="date" class="form-input" id="warrantyDate">
</div>
<div class="form-group">
<label class="form-label">Warranty Type</label>
<select class="form-select"><option>On-site</option><option>Carry-in</option><option>Comprehensive AMC</option><option>No Warranty</option></select>
</div>
</div>
<hr class="divider">
<div class="section-hdr">Depreciation Configuration</div>
<div class="form-row-3">
<div class="form-group">
<label class="form-label">Method <span class="req">*</span></label>
<select class="form-select" id="depMethod" onchange="calcDepreciation()">
<option value="SLM">SLM Straight Line</option>
<option value="WDV">WDV Written Down Value</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Rate (% p.a.) <span class="req">*</span></label>
<input type="number" class="form-input" id="depRate" value="20" min="1" max="100" oninput="calcDepreciation()">
</div>
<div class="form-group">
<label class="form-label">Residual Value (₹)</label>
<input type="number" class="form-input" id="residualValue" placeholder="0">
</div>
</div>
<div id="depPreview" style="display:none;margin-top:8px;padding:14px;background:var(--bg-surface);border-radius:var(--radius-md);border:1px solid var(--border)">
<div style="font-size:12px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:10px">Depreciation Preview</div>
<div id="depPreviewContent"></div>
</div>
</div>
</div>
<!-- Step 3 -->
<div class="card mt-4" id="step3" data-step="3">
<div class="card-header"><span class="card-title">📍 Step 3: Location & Assignment</span></div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<label class="form-label">Department <span class="req">*</span></label>
<select class="form-select" id="assetDept">
<option value="">Select department…</option>
<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">Location <span class="req">*</span></label>
<select class="form-select" id="assetLoc">
<option>IT Dept Floor 2</option><option>Finance Floor 1</option>
<option>HR Floor 2</option><option>Server Room B1</option>
<option>Marketing Floor 3</option><option>Admin Floor 1</option>
<option>Warehouse Whitefield</option><option>Parking B1</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Assign To (Employee)</label>
<select class="form-select" id="assetAssignee">
<option value="">Unassigned (Pool Asset)</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><option>Deepak Joshi Admin</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Assignment Date</label>
<input type="date" class="form-input" value="2025-05-28">
</div>
</div>
<div class="form-group">
<label class="form-label">Cost Center</label>
<select class="form-select">
<option>CC-IT-001 (IT Infrastructure)</option><option>CC-FIN-002 (Finance Ops)</option>
<option>CC-HR-003 (Human Resources)</option><option>CC-OPS-004 (Operations)</option>
<option>CC-MKT-005 (Marketing)</option>
</select>
</div>
<hr class="divider">
<div class="section-hdr">Additional Options</div>
<div class="flex gap-4 flex-wrap">
<label class="flex items-center gap-2" style="cursor:pointer;font-size:13px">
<input type="checkbox" checked> Generate QR code on save
</label>
<label class="flex items-center gap-2" style="cursor:pointer;font-size:13px">
<input type="checkbox" checked> Send acknowledgment email to assignee
</label>
<label class="flex items-center gap-2" style="cursor:pointer;font-size:13px">
<input type="checkbox"> Schedule first PM after 3 months
</label>
</div>
</div>
</div>
<!-- Submit Row -->
<div class="card mt-4">
<div class="card-body" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px">
<div>
<div style="font-size:13px;font-weight:600">Ready to save?</div>
<div style="font-size:12px;color:var(--text-muted)">Asset will be created and QR code will be generated automatically.</div>
</div>
<div class="flex gap-3">
<button class="btn btn-secondary" onclick="saveDraft()">💾 Save as Draft</button>
<button class="btn btn-primary btn-lg" onclick="submitAsset()">✅ Create Asset</button>
</div>
</div>
</div>
</div><!-- /formArea -->
</div>
</main>
</div>
</div>
<!-- Success Modal -->
<div class="modal-overlay" id="successModal">
<div class="modal" style="max-width:420px">
<div class="modal-body" 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 id="newAssetId" style="color:var(--primary-light);font-size:14px"></code></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>
<button class="btn btn-primary" onclick="viewNew()">View Asset →</button>
</div>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
<script>
const steps = [
{ num:1, label:'Basic Info', sub:'Name, category, serial' },
{ num:2, label:'Purchase & Finance', sub:'Cost, depreciation' },
{ num:3, label:'Location & Assign', sub:'Department, assignee' }
];
const customFieldDefs = {
'Laptops': [['Processor','text','e.g. Intel Core i7'],['RAM','text','e.g. 16 GB'],['Storage','text','e.g. 512 GB SSD'],['OS','text','e.g. Windows 11 Pro'],['Screen Size','text','e.g. 14 inch']],
'Desktops': [['Processor','text','e.g. Core i5'],['RAM','text','e.g. 8 GB'],['Storage','text','e.g. 1 TB HDD'],['OS','text','e.g. Windows 11']],
'Servers': [['CPU','text','e.g. Xeon Gold'],['RAM','text','e.g. 64 GB ECC'],['Storage','text','e.g. 2× 1.8 TB SAS'],['RAID','text','e.g. RAID 10'],['OS','text','e.g. Ubuntu Server']],
'Vehicles': [['Registration No.','text','e.g. MH12AB1234'],['Fuel Type','select','Petrol|Diesel|Electric|CNG'],['Engine CC','number','1498'],['Insurance Expiry','date',''],['Seating Capacity','number','5']],
'HVAC': [['Capacity','text','e.g. 1.5 Ton'],['Type','select','Split|Window|Cassette|Ducted'],['Refrigerant','text','e.g. R-32'],['Star Rating','number','5']],
'Printers': [['Print Speed (ppm)','number','40'],['Technology','select','Laser|Inkjet|Dot Matrix'],['Paper Sizes','text','A4, A5, Letter'],['Monthly Duty Cycle','text','e.g. 80,000 pages']],
'Networking': [['Port Count','number','24'],['Speed','text','e.g. 1 Gbps'],['Managed','select','Yes|No'],['PoE','select','Yes|No']],
'Mobile Devices': [['OS','select','iOS|Android|Windows'],['Storage','text','256 GB'],['IMEI','text','']],
'Displays': [['Screen Size','text','43 inch'],['Resolution','text','4K UHD'],['Panel Type','select','IPS|VA|OLED|TN']],
'Furniture': [['Material','text','e.g. Engineered Wood'],['Color','text','Black'],['Load Capacity','text','e.g. 120 kg']]
};
document.addEventListener('DOMContentLoaded', () => {
renderStepNav();
});
function renderStepNav() {
document.getElementById('stepNav').innerHTML = steps.map(s => `
<div class="step-nav-item ${s.num===1?'active':''}" id="stepNavItem-${s.num}" onclick="scrollToStep(${s.num})">
<div class="step-num" id="stepNum-${s.num}">${s.num}</div>
<div><div class="step-nav-label">${s.label}</div><div class="step-nav-sub">${s.sub}</div></div>
</div>`).join('');
}
function scrollToStep(n) {
const el = document.getElementById(`step${n}`);
if (el) el.scrollIntoView({behavior:'smooth',block:'start'});
}
function renderCustomFields() {
const cat = document.getElementById('assetCategory').value;
const section = document.getElementById('customFieldsSection');
const grid = document.getElementById('customFieldsGrid');
const fields = customFieldDefs[cat];
if (!fields) { section.style.display='none'; return; }
section.style.display = '';
grid.innerHTML = fields.map(([label,type,ph])=>`
<div class="form-group">
<label class="form-label">${label}</label>
${type==='select'
? `<select class="form-select">${ph.split('|').map(o=>`<option>${o}</option>`).join('')}</select>`
: `<input type="${type}" class="form-input" placeholder="${ph||''}">`
}
</div>`).join('');
}
function updatePreview() {
const n = document.getElementById('assetName').value;
if(n) document.getElementById('previewId').textContent = 'AST-2025-021';
}
function calcDepreciation() {
const cost = parseFloat(document.getElementById('purchaseCost').value) || 0;
const rate = parseFloat(document.getElementById('depRate').value) || 20;
const method = document.getElementById('depMethod').value;
const prev = document.getElementById('depPreview');
const cont = document.getElementById('depPreviewContent');
if (!cost) { prev.style.display='none'; return; }
prev.style.display = '';
let rows='',val=cost;
for(let i=1;i<=4;i++){
const dep = method==='SLM' ? cost*rate/100 : val*rate/100;
const close = Math.max(0,val-dep);
rows+=`<div class="flex justify-between" style="font-size:12px;padding:5px 0;border-bottom:1px solid var(--border)">
<span style="color:var(--text-muted)">Year ${i}</span>
<span style="color:var(--danger)">-${fmt(Math.round(dep))}</span>
<span style="font-weight:600">${fmt(Math.round(close))}</span>
</div>`;
val=close;
}
cont.innerHTML = `<div class="flex justify-between mb-2" style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px"><span>Year</span><span>Depreciation</span><span>Closing Value</span></div>${rows}`;
}
function saveDraft() {
const name = document.getElementById('assetName').value || 'Unnamed Asset';
showToast('Draft Saved',`"${name}" saved as draft`,'info');
}
function submitAsset() {
const name = document.getElementById('assetName').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; }
// Simulate save
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: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>
</div>
</div>`;
openModal('successModal');
}
</script>
</body>
</html>

494
asset-detail.html Normal file
View File

@ -0,0 +1,494 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Asset Detail | 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">
<!-- 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>
<!-- MAIN -->
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-left">
<button onclick="history.back()" class="btn btn-ghost btn-sm" style="margin-right:6px">← Back</button>
<div class="topbar-title" id="assetHeaderTitle">Asset Detail</div>
</div>
<div class="topbar-actions">
<button class="btn btn-secondary btn-sm" onclick="openModal('editModal')">✏️ Edit</button>
<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>
</div>
</header>
<main class="content">
<!-- Asset Header -->
<div class="card mb-4" id="assetHeader"></div>
<!-- Tabs -->
<div class="tab-container">
<div class="tabs" data-group="detail">
<button class="tab-btn active" data-tab="tab-overview" data-group="detail">📋 Overview</button>
<button class="tab-btn" data-tab="tab-financial" data-group="detail">💰 Financial</button>
<button class="tab-btn" data-tab="tab-assignments" data-group="detail">👤 Assignments</button>
<button class="tab-btn" data-tab="tab-maintenance" data-group="detail">🔧 Maintenance</button>
<button class="tab-btn" data-tab="tab-documents" data-group="detail">📄 Documents</button>
<button class="tab-btn" data-tab="tab-history" data-group="detail">🕐 History</button>
</div>
<!-- Overview Tab -->
<div class="tab-content active" id="tab-overview" data-group="detail">
<div class="grid-2">
<div class="card">
<div class="card-header"><span class="card-title">📋 Asset Information</span></div>
<div class="card-body" id="assetInfoGrid"></div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">📍 Location & Assignment</span></div>
<div class="card-body" id="assetLocationGrid"></div>
</div>
</div>
<div class="grid-2 mt-4">
<div class="card">
<div class="card-header"><span class="card-title">📱 QR Code</span>
<button class="btn btn-secondary btn-sm" onclick="printQR()">🖨️ Print</button></div>
<div class="card-body" style="text-align:center" id="assetQR"></div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">🔧 Custom Fields</span></div>
<div class="card-body" id="assetCustomFields"></div>
</div>
</div>
</div>
<!-- Financial Tab -->
<div class="tab-content" id="tab-financial" data-group="detail">
<div class="grid-2">
<div class="card">
<div class="card-header"><span class="card-title">💰 Depreciation</span></div>
<div class="card-body">
<div id="depreciationInfo"></div>
<canvas id="depChart" height="180"></canvas>
</div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">📊 Value Summary</span></div>
<div class="card-body" id="valueSummary"></div>
</div>
</div>
</div>
<!-- Assignments Tab -->
<div class="tab-content" id="tab-assignments" data-group="detail">
<div class="card">
<div class="card-header"><span class="card-title">👤 Assignment History</span>
<button class="btn btn-primary btn-sm" onclick="openModal('assignModal')">+ New Assignment</button>
</div>
<div class="card-body" id="assignmentHistory"></div>
</div>
</div>
<!-- Maintenance Tab -->
<div class="tab-content" id="tab-maintenance" data-group="detail">
<div class="card">
<div class="card-header"><span class="card-title">🔧 Maintenance Records</span>
<a href="maintenance.html" class="btn btn-secondary btn-sm">+ Raise Ticket</a>
</div>
<div class="card-body" id="maintenanceRecords"></div>
</div>
</div>
<!-- Documents Tab -->
<div class="tab-content" id="tab-documents" data-group="detail">
<div class="card">
<div class="card-header"><span class="card-title">📄 Documents</span>
<button class="btn btn-primary btn-sm" onclick="showToast('Upload','Document upload dialog','info')">📎 Upload</button>
</div>
<div class="card-body" id="assetDocs"></div>
</div>
</div>
<!-- History Tab -->
<div class="tab-content" id="tab-history" data-group="detail">
<div class="card">
<div class="card-header"><span class="card-title">🕐 Audit Trail</span></div>
<div class="card-body" id="auditTrail"></div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Edit Modal -->
<div class="modal-overlay" id="editModal">
<div class="modal modal-lg">
<div class="modal-header"><span class="modal-title">Edit Asset</span><button class="modal-close"></button></div>
<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>
<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">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>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('editModal')">Cancel</button>
<button class="btn btn-primary" onclick="saveEdit()">Save Changes</button>
</div>
</div>
</div>
<!-- Assign Modal -->
<div class="modal-overlay" id="assignModal">
<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>
<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>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('assignModal')">Cancel</button>
<button class="btn btn-primary" onclick="doAssign()">Assign</button>
</div>
</div>
</div>
<!-- Transfer Modal -->
<div class="modal-overlay" id="transferModal">
<div class="modal">
<div class="modal-header"><span class="modal-title">Transfer Asset</span><button class="modal-close"></button></div>
<div class="modal-body">
<div class="form-row">
<div class="form-group"><label class="form-label">To 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 class="form-group"><label class="form-label">To Location</label>
<select class="form-select"><option>IT Dept Floor 2</option><option>Finance Floor 1</option><option>Server Room B1</option><option>HR Floor 2</option><option>Marketing Floor 3</option></select>
</div>
</div>
<div class="form-group"><label class="form-label">Reason</label>
<select class="form-select"><option>Department Restructuring</option><option>Employee Transfer</option><option>Better Utilization</option><option>Other</option></select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('transferModal')">Cancel</button>
<button class="btn btn-primary" onclick="doTransfer()">Transfer</button>
</div>
</div>
</div>
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width:400px">
<div class="modal-header"><span class="modal-title confirm-title">Confirm</span><button class="modal-close"></button></div>
<div class="modal-body"><p class="confirm-message" style="color:var(--text-secondary)"></p></div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('confirmModal')">Cancel</button><button class="btn btn-danger confirm-ok">Confirm</button></div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
<script>
const params = new URLSearchParams(window.location.search);
const assetId = params.get('id') || 'AST-2025-001';
let asset = AMS.assets.find(a => a.id === assetId) || AMS.assets[0];
document.addEventListener('DOMContentLoaded', () => {
document.title = `${asset.name} | AMS`;
renderHeader();
renderOverview();
renderFinancial();
renderAssignments();
renderMaintenance();
renderDocuments();
renderHistory();
initTabs();
});
function renderHeader() {
document.getElementById('assetHeaderTitle').textContent = asset.name;
const depPct = 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="flex:1;min-width:0">
<div style="font-size:20px;font-weight:800;margin-bottom:4px">${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 ${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>
</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:18px;font-weight:800">${fmt(asset.cost)}</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>
</div>
<div style="text-align:right">
<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>
<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>Purchased: ${fmtDate(asset.purchase)}</span>
</div>
</div>`;
}
function renderOverview() {
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">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">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>`;
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>
</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
const cells = generateQRPattern(asset.id);
document.getElementById('assetQR').innerHTML = `
<div class="qr-box" style="margin:0 auto">
<div style="display:grid;grid-template-columns:repeat(7,11px);grid-template-rows:repeat(7,11px);gap:2px;padding:8px;background:#fff">
${cells.map(c=>`<div style="background:${c?'#000':'#fff'};border-radius:1px"></div>`).join('')}
</div>
<div style="font-size:9px;color:#333;font-weight:700;font-family:monospace">${asset.id}</div>
<div style="font-size:8px;color:#555;max-width:100px">${asset.name.substring(0,24)}</div>
</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>`;
}
function renderFinancial() {
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 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">
<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">
<span style="font-size:11px">${100-depPct}% remaining value</span>
<span style="font-size:11px">${depPct}% depreciated</span>
</div>
</div>
<div class="info-grid mb-4">
<div class="info-item"><div class="info-label">Method</div><div class="info-value">${asset.depM} (${asset.depM==='SLM'?'Straight Line':'Written Down Value'})</div></div>
<div class="info-item"><div class="info-label">Rate</div><div class="info-value">${asset.depR}% per annum</div></div>
<div class="info-item"><div class="info-label">Annual Depreciation</div><div class="info-value" style="color:var(--danger)">${fmt(annualDep)}</div></div>
<div class="info-item"><div class="info-label">Asset Age</div><div class="info-value">${years} year${years!==1?'s':''}</div></div>
</div>`;
// Depreciation chart
const purchaseYear = new Date(asset.purchase).getFullYear();
const labels=[], values=[];
let val = asset.cost;
for(let i=0;i<=5;i++){
labels.push(purchaseYear+i);
values.push(Math.max(0, Math.round(val)));
if(asset.depM==='SLM') val -= asset.cost*asset.depR/100;
else val *= (1-asset.depR/100);
}
new Chart(document.getElementById('depChart'),{
type:'line',
data:{ labels, datasets:[{label:'Book Value',data:values,borderColor:'#6366F1',backgroundColor:'rgba(99,102,241,.1)',fill:true,tension:.3,pointRadius:4,borderWidth:2}] },
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},
scales:{x:{grid:{color:'rgba(255,255,255,.04)'},ticks:{color:'#4B5563',font:{family:'Inter',size:11}}},
y:{grid:{color:'rgba(255,255,255,.04)'},ticks:{color:'#4B5563',font:{family:'Inter',size:11},callback:v=>'₹'+(v/1000).toFixed(0)+'K'}}}}
});
document.getElementById('valueSummary').innerHTML = `
<div class="info-grid">
<div class="info-item"><div class="info-label">Purchase Cost</div><div class="info-value" style="font-size:18px;font-weight:800">${fmt(asset.cost)}</div></div>
<div class="info-item"><div class="info-label">Current Book Value</div><div class="info-value" style="font-size:18px;font-weight:800;color:var(--success)">${fmt(asset.value)}</div></div>
<div class="info-item"><div class="info-label">Total Depreciation</div><div class="info-value" style="color:var(--danger)">${fmt(asset.cost-asset.value)}</div></div>
<div class="info-item"><div class="info-label">Residual Value</div><div class="info-value">${fmt(Math.round(asset.cost*0.05))}</div></div>
</div>
<hr class="divider">
<div class="section-hdr">Year-wise Depreciation</div>
<table class="data-table" style="font-size:12px">
<thead><tr><th>Year</th><th>Opening Value</th><th>Depreciation</th><th>Closing Value</th></tr></thead>
<tbody>
${(()=>{
let v=asset.cost, rows=''; const py=new Date(asset.purchase).getFullYear();
for(let i=0;i<Math.min(5,Math.ceil(100/asset.depR));i++){
const dep = asset.depM==='SLM' ? asset.cost*asset.depR/100 : v*asset.depR/100;
const close = Math.max(0,v-dep);
rows+=`<tr><td>${py+i}</td><td>${fmt(Math.round(v))}</td><td style="color:var(--danger)">-${fmt(Math.round(dep))}</td><td>${fmt(Math.round(close))}</td></tr>`;
v=close;
}
return rows;
})()}
</tbody>
</table>`;
}
function renderAssignments() {
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>
<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-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>`;
}
function renderMaintenance() {
const rel = AMS.tickets.filter(t => t.asset === asset.id);
if (!rel.length) {
document.getElementById('maintenanceRecords').innerHTML = `<div class="empty-state"><div class="empty-icon">🔧</div><div class="empty-title">No maintenance records</div><div class="empty-text">No tickets raised for this asset</div><a href="maintenance.html" class="btn btn-primary btn-sm">Raise Ticket</a></div>`;
return;
}
document.getElementById('maintenanceRecords').innerHTML = `
<div class="timeline">
${rel.map(t=>`
<div class="timeline-item">
<div class="tl-icon-wrap"><div class="tl-icon" style="background:var(--${statusBadge(t.status).includes('danger')?'danger':'info'}-bg);color:var(--${statusBadge(t.status).includes('danger')?'danger':'info'})">🔧</div><div class="tl-line"></div></div>
<div class="tl-content">
<div class="tl-title">${t.title||t.name} <span class="badge ${statusBadge(t.status)}" style="margin-left:6px">${t.status}</span></div>
<div class="tl-desc">${t.category} · Assigned to: ${t.assignedTo}</div>
<div class="tl-time">Created: ${fmtDate(t.created)} · Priority: <span class="badge ${statusBadge(t.priority)}">${t.priority}</span></div>
</div>
</div>`).join('')}
</div>`;
}
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:'📷' }
];
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)">
<div style="font-size:22px">${d.icon}</div>
<div style="flex:1"><div style="font-weight:600;font-size:13px">${d.name}</div><div style="font-size:11px;color:var(--text-muted)">${d.type} · ${d.size} · Uploaded ${fmtDate(d.date)}</div></div>
<button class="btn btn-ghost btn-sm" onclick="showToast('Download','Downloading ${d.name}…','info')">📥</button>
<button class="btn btn-ghost btn-sm" onclick="showToast('Delete','File deleted','success')">🗑️</button>
</div>`).join('');
}
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' }
];
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>`).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 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);
});
}
function printQR() { showToast('Print','QR label sent to printer','info'); }
</script>
</body>
</html>

415
assets.html Normal file
View File

@ -0,0 +1,415 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>All Assets | 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">
<div class="sidebar-logo"><div class="logo-icon">📦</div><div><div class="logo-title">AMS</div><div class="logo-sub">Asset Management</div></div></div>
<nav class="sidebar-nav">
<div class="nav-section-label">Overview</div>
<a href="dashboard.html" class="nav-item"><span class="nav-icon">📊</span> Dashboard</a>
<div class="nav-section-label">Assets</div>
<a href="assets.html" class="nav-item active"><span class="nav-icon">📦</span> All Assets</a>
<a href="asset-create.html" class="nav-item"><span class="nav-icon"></span> Add Asset</a>
<div class="nav-section-label">Supply Chain</div>
<a href="inventory.html" class="nav-item"><span class="nav-icon">🏪</span> Inventory</a>
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement <span class="nav-badge">8</span></a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><span class="nav-icon">🔧</span> Maintenance <span class="nav-badge">12</span></a>
<a href="reports.html" class="nav-item"><span class="nav-icon">📈</span> Reports &amp; Audit</a>
<div class="nav-section-label">Administration</div>
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users &amp; Roles</a>
<a href="settings.html" class="nav-item"><span class="nav-icon">⚙️</span> Settings</a>
</nav>
<div class="sidebar-footer"><div class="user-card"><div class="user-av">AS</div><div style="flex:1;min-width:0"><div class="user-name">Arjun Sharma</div><div class="user-role">Asset Manager</div></div></div></div>
</aside>
<!-- 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-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>
</div>
</header>
<main class="content">
<!-- Stat Mini Cards -->
<div class="grid-4 mb-4" id="assetStats"></div>
<!-- Filters -->
<div class="filters-row" id="filtersRow">
<div class="search-wrap" style="max-width:300px">
<span style="color:var(--text-muted);font-size:13px">🔍</span>
<input type="text" id="tableSearch" placeholder="Filter list…">
</div>
<select class="filter-sel" id="statusFilter">
<option value="">All Statuses</option>
<option>Active</option><option>Idle</option>
<option>Under Maintenance</option><option>Disposed</option>
</select>
<select class="filter-sel" id="catFilter">
<option value="">All Categories</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>
</select>
<select class="filter-sel" id="deptFilter">
<option value="">All Departments</option>
<option>IT</option><option>Finance</option><option>HR</option>
<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>
<!-- Bulk Bar -->
<div class="bulk-bar" id="assetTableBulk">
<span class="bulk-count">0 items selected</span>
<button class="btn btn-secondary btn-sm" onclick="bulkAction('transfer')">↔ Transfer</button>
<button class="btn btn-secondary btn-sm" onclick="bulkAction('assign')">👤 Assign</button>
<button class="btn btn-secondary btn-sm" onclick="bulkAction('export')">📥 Export</button>
<button class="btn btn-danger btn-sm" onclick="bulkAction('dispose')">🗑️ Dispose</button>
</div>
<!-- Table -->
<div class="table-wrapper">
<table class="data-table" id="assetTable" data-table-checkboxes>
<thead>
<tr>
<th style="width:40px"><input type="checkbox" class="select-all"></th>
<th onclick="sortTable(1)">Asset ↕</th>
<th onclick="sortTable(2)">Category ↕</th>
<th onclick="sortTable(3)">Status ↕</th>
<th onclick="sortTable(4)">Department ↕</th>
<th onclick="sortTable(5)">Assigned To ↕</th>
<th onclick="sortTable(6)">Purchase Cost ↕</th>
<th onclick="sortTable(7)">Net Value ↕</th>
<th>Warranty</th>
<th style="width:110px">Actions</th>
</tr>
</thead>
<tbody id="assetTbody"></tbody>
</table>
<div class="pagination" id="assetPagination"></div>
</div>
</main>
</div>
</div>
<!-- ── Assign Modal ── -->
<div class="modal-overlay" id="assignModal">
<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="alert alert-info mb-4"><div class="alert-icon"></div><div><div class="alert-title" id="assignAssetName">Asset Name</div><div class="alert-text">Current assignee will receive a handover notification</div></div></div>
<div class="form-group">
<label class="form-label">Assign To (Employee) <span class="req">*</span></label>
<select class="form-select" id="assignEmployee">
<option value="">Select employee…</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><option>Deepak Joshi Admin</option>
<option>Kavya Nair IT</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Effective From</label>
<input type="date" class="form-input" id="assignFrom" value="2025-05-28">
</div>
<div class="form-group">
<label class="form-label">Expected Return (if temp)</label>
<input type="date" class="form-input" id="assignTo">
</div>
</div>
<div class="form-group">
<label class="form-label">Acknowledgment Required?</label>
<select class="form-select">
<option>Yes Send email to employee for confirmation</option>
<option>No Direct assignment</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Notes</label>
<textarea class="form-textarea" placeholder="Any notes about this assignment…" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('assignModal')">Cancel</button>
<button class="btn btn-primary" onclick="saveAssignment()">Assign Asset</button>
</div>
</div>
</div>
<!-- ── QR Modal ── -->
<div class="modal-overlay" id="qrModal">
<div class="modal" style="max-width:380px">
<div class="modal-header"><span class="modal-title">QR Code Label</span><button class="modal-close"></button></div>
<div class="modal-body" style="text-align:center">
<div id="qrDisplay" style="margin:0 auto 16px;display:inline-block"></div>
<div id="qrAssetInfo" style="font-size:12.5px;color:var(--text-muted);margin-bottom:16px"></div>
<button class="btn btn-primary" onclick="printQR()">🖨️ Print Label</button>
<button class="btn btn-secondary" onclick="downloadQR()">📥 Download PNG</button>
</div>
</div>
</div>
<!-- ── Transfer Modal ── -->
<div class="modal-overlay" id="transferModal">
<div class="modal">
<div class="modal-header"><span class="modal-title">Transfer Asset</span><button class="modal-close"></button></div>
<div class="modal-body">
<div class="form-group">
<label class="form-label">Transfer From (Current Location)</label>
<input type="text" class="form-input" id="transferFrom" disabled>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">To 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">To Location <span class="req">*</span></label>
<select class="form-select">
<option>IT Dept Floor 2</option><option>Finance Floor 1</option>
<option>HR Floor 2</option><option>Server Room B1</option>
<option>Marketing Floor 3</option><option>Conference Room A</option>
</select>
</div>
</div>
<div class="form-group">
<label class="form-label">Reason for Transfer <span class="req">*</span></label>
<select class="form-select">
<option>Department Restructuring</option><option>Employee Transfer</option>
<option>Better Utilization</option><option>Location Upgrade</option><option>Other</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Approval Required?</label>
<select class="form-select"><option>Yes Send for Department Head approval</option><option>No (Self-authorized)</option></select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('transferModal')">Cancel</button>
<button class="btn btn-primary" onclick="saveTransfer()">Initiate Transfer</button>
</div>
</div>
</div>
<!-- Confirm Modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width:400px">
<div class="modal-header"><span class="modal-title confirm-title">Confirm</span><button class="modal-close"></button></div>
<div class="modal-body"><p class="confirm-message" style="color:var(--text-secondary);font-size:13.5px"></p></div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('confirmModal')">Cancel</button><button class="btn btn-danger confirm-ok">Confirm</button></div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
<script>
let currentPage = 1;
const perPage = 10;
let sortCol = -1, sortDir = 1;
let filteredAssets = [];
document.addEventListener('DOMContentLoaded', () => {
renderStats();
renderTable();
initCheckboxes('assetTable','assetTableBulk');
document.getElementById('tableSearch').addEventListener('input', applyFilters);
document.getElementById('statusFilter').addEventListener('change', applyFilters);
document.getElementById('catFilter').addEventListener('change', applyFilters);
document.getElementById('deptFilter').addEventListener('change', applyFilters);
});
function renderStats() {
const s = AMS.stats;
document.getElementById('totalCount').textContent = `— ${s.total.toLocaleString()} total`;
document.getElementById('assetStats').innerHTML = [
{label:'Total',icon:'📦',val:s.total,color:'var(--primary)',link:'?status='},
{label:'Active',icon:'✅',val:s.active,color:'var(--success)',link:'?status=active'},
{label:'Maintenance',icon:'🔧',val:s.maintenance,color:'var(--info)',link:'?status=maintenance'},
{label:'Idle',icon:'😴',val:s.idle,color:'var(--warning)',link:'?status=idle'}
].map(k=>`
<div class="stat-card" style="--sc-color:${k.color};cursor:pointer" onclick="filterByStatus('${k.label}')">
<div class="stat-icon" style="background:${k.color}18;color:${k.color}">${k.icon}</div>
<div class="stat-label">${k.label}</div>
<div class="stat-value" style="color:${k.color}">${k.val.toLocaleString('en-IN')}</div>
</div>`).join('');
}
function filterByStatus(s) {
if(s==='Total') { document.getElementById('statusFilter').value=''; }
else if(s==='Active') document.getElementById('statusFilter').value='Active';
else if(s==='Maintenance') document.getElementById('statusFilter').value='Under Maintenance';
else if(s==='Idle') document.getElementById('statusFilter').value='Idle';
applyFilters();
}
function applyFilters() {
const q = document.getElementById('tableSearch').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)
);
currentPage = 1;
renderTable();
}
function clearFilters() {
document.getElementById('tableSearch').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('catFilter').value = '';
document.getElementById('deptFilter').value = '';
applyFilters();
}
function renderTable() {
if (!filteredAssets.length && !document.getElementById('tableSearch').value) {
filteredAssets = [...AMS.assets];
}
document.getElementById('visCount').textContent = filteredAssets.length;
const start = (currentPage-1)*perPage;
const page = filteredAssets.slice(start, start+perPage);
const tbody = document.getElementById('assetTbody');
if (!page.length) {
tbody.innerHTML = `<tr><td colspan="10"><div class="empty-state"><div class="empty-icon">📦</div><div class="empty-title">No assets found</div><div class="empty-text">Try adjusting your filters</div></div></td></tr>`;
return;
}
const warningDate = new Date(); warningDate.setDate(warningDate.getDate()+90);
tbody.innerHTML = page.map(a => {
const warrantyExpiring = a.warranty !== 'N/A' && new Date(a.warranty) < warningDate;
const depPct = Math.round((a.cost - a.value)/a.cost*100);
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></td>
<td><span class="badge badge-neutral" style="font-size:10px">${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 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 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>
</div>
</td>
</tr>`;
}).join('');
renderPagBtn();
}
function renderPagBtn() {
const total = filteredAssets.length;
const pages = Math.max(1,Math.ceil(total/perPage));
let html = `<button class="page-btn" onclick="goPage(${currentPage-1})" ${currentPage===1?'disabled':''}></button>`;
for(let i=1;i<=pages;i++) html+=`<button class="page-btn${i===currentPage?' active':''}" onclick="goPage(${i})">${i}</button>`;
html+=`<button class="page-btn" onclick="goPage(${currentPage+1})" ${currentPage===pages?'disabled':''}></button>`;
document.getElementById('assetPagination').innerHTML = html;
}
function goPage(p) { if(p<1||p>Math.ceil(filteredAssets.length/perPage))return; currentPage=p; renderTable(); window.scrollTo(0,0); }
function sortTable(col) {
if(sortCol===col) sortDir*=-1; else { sortCol=col; sortDir=1; }
const keys=['','name','cat','status','dept','assignee','cost','value','warranty'];
filteredAssets.sort((a,b)=>{
const av=a[keys[col]]||''; const bv=b[keys[col]]||'';
if(typeof av==='number') return (av-bv)*sortDir;
return av.localeCompare(bv)*sortDir;
});
renderTable();
}
function goToDetail(id) { window.location.href = `asset-detail.html?id=${encodeURIComponent(id)}`; }
function openAssignModal(id, name, dept) {
document.getElementById('assignAssetName').textContent = name;
openModal('assignModal');
}
function saveAssignment() {
const emp = document.getElementById('assignEmployee').value;
if(!emp) { showToast('Error','Please select an employee','error'); return; }
closeModal('assignModal');
showToast('Asset Assigned','Acknowledgment email sent to employee','success');
}
function openTransferModal(id, loc) {
document.getElementById('transferFrom').value = loc;
openModal('transferModal');
}
function saveTransfer() { closeModal('transferModal'); showToast('Transfer Initiated','Approval request sent to Department Head','info'); }
function showQR(id) {
const a = AMS.assets.find(x=>x.id===id);
if(!a) return;
const cells = generateQRPattern(id);
const qrHTML = `<div class="qr-box">
<div style="display:grid;grid-template-columns:repeat(7,12px);grid-template-rows:repeat(7,12px);gap:2px;background:#fff;padding:10px">
${cells.map(c=>`<div style="background:${c?'#000':'#fff'};border-radius:1px"></div>`).join('')}
</div>
<div style="font-size:9px;color:#333;font-weight:700;font-family:monospace">${a.id}</div>
<div style="font-size:8px;color:#555;max-width:120px;text-align:center">${a.name}</div>
</div>`;
document.getElementById('qrDisplay').innerHTML = qrHTML;
document.getElementById('qrAssetInfo').innerHTML = `<strong>${a.name}</strong><br>Serial: ${a.serial}<br>Category: ${a.cat}`;
openModal('qrModal');
}
function printQR() { window.print(); }
function downloadQR() { showToast('Download','QR code image downloaded','success'); }
function bulkAction(action) {
const checked = document.querySelectorAll('#assetTable .row-check:checked').length;
if(!checked) { showToast('No selection','Please select assets first','warning'); return; }
if(action==='dispose') {
confirmAction('Dispose Assets',`Mark ${checked} asset(s) as disposed? This action cannot be undone.`,()=>{
showToast('Assets Disposed',`${checked} asset(s) marked for disposal`,'success');
});
} else if(action==='transfer') {
openModal('transferModal');
} else if(action==='export') {
showToast('Export','Exporting selected assets to CSV…','info');
setTimeout(()=>{ downloadCSV(AMS.assets.slice(0,checked),'selected_assets.csv'); },600);
} else if(action==='assign') {
openModal('assignModal');
}
}
function exportAssets() {
showToast('Export','Preparing CSV export…','info');
setTimeout(()=>{ downloadCSV(AMS.assets.map(a=>({ID:a.id,Name:a.name,Category:a.cat,Status:a.status,Dept:a.dept,Assignee:a.assignee,Cost:a.cost,Value:a.value,Serial:a.serial,Vendor:a.vendor,Warranty:a.warranty})),'all_assets_export.csv'); },800);
}
</script>
</body>
</html>

475
css/styles.css Normal file
View File

@ -0,0 +1,475 @@
/* ================================================================
AMS Asset Management System | Design System v1.0
Premium Dark Theme
================================================================ */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
/* ── CSS Variables ─────────────────────────────────────────────── */
:root {
--bg-root: #07090F;
--bg-surface: #0D1117;
--bg-card: #131926;
--bg-card-hover: #18212f;
--bg-elevated: #1E2A3A;
--border: rgba(255,255,255,0.055);
--border-strong: rgba(255,255,255,0.1);
--border-accent: rgba(99,102,241,0.35);
--primary: #6366F1;
--primary-light: #818CF8;
--primary-dark: #4F46E5;
--primary-glow: rgba(99,102,241,0.12);
--primary-glow2: rgba(99,102,241,0.28);
--cyan: #06B6D4;
--cyan-light: #22D3EE;
--cyan-glow: rgba(6,182,212,0.10);
--success: #10B981;
--success-bg: rgba(16,185,129,0.10);
--warning: #F59E0B;
--warning-bg: rgba(245,158,11,0.10);
--danger: #EF4444;
--danger-bg: rgba(239,68,68,0.10);
--info: #3B82F6;
--info-bg: rgba(59,130,246,0.10);
--purple: #A855F7;
--purple-bg: rgba(168,85,247,0.10);
--text-primary: #EFF6FF;
--text-secondary:#94A3B8;
--text-muted: #4B5563;
--sidebar-w: 256px;
--topbar-h: 62px;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-xl: 20px;
--shadow-sm: 0 1px 4px rgba(0,0,0,0.4);
--shadow-md: 0 4px 20px rgba(0,0,0,0.5);
--shadow-lg: 0 8px 40px rgba(0,0,0,0.65);
--t: all 0.2s cubic-bezier(.4,0,.2,1);
--t-fast: all 0.12s ease;
--t-slow: all 0.35s cubic-bezier(.4,0,.2,1);
}
/* ── Reset ─────────────────────────────────────────────────────── */
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{font-size:14px;scroll-behavior:smooth}
body{font-family:'Inter',-apple-system,sans-serif;background:var(--bg-root);color:var(--text-primary);line-height:1.6;min-height:100vh;overflow-x:hidden;-webkit-font-smoothing:antialiased}
a{color:var(--primary-light);text-decoration:none}
a:hover{color:var(--primary)}
::-webkit-scrollbar{width:5px;height:5px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--bg-elevated);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}
/* ── App Layout ─────────────────────────────────────────────────── */
.app-layout{display:flex;min-height:100vh}
/* ── Sidebar ─────────────────────────────────────────────────────── */
.sidebar{position:fixed;left:0;top:0;bottom:0;width:var(--sidebar-w);background:var(--bg-surface);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:100;overflow-y:auto;overflow-x:hidden;transition:var(--t-slow)}
.sidebar-logo{display:flex;align-items:center;gap:12px;padding:18px 18px 16px;border-bottom:1px solid var(--border);flex-shrink:0}
.logo-icon{width:38px;height:38px;background:linear-gradient(135deg,var(--primary),var(--cyan));border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0;box-shadow:0 0 18px var(--primary-glow2)}
.logo-title{font-size:16px;font-weight:800;letter-spacing:-0.4px}
.logo-sub{font-size:9.5px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.8px;margin-top:1px}
.sidebar-nav{flex:1;padding:12px 0;overflow-y:auto}
.nav-section-label{font-size:9.5px;font-weight:700;text-transform:uppercase;letter-spacing:1px;color:var(--text-muted);padding:14px 18px 5px}
.nav-item{display:flex;align-items:center;gap:10px;padding:9px 14px;margin:1px 8px;border-radius:var(--radius-sm);color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;transition:var(--t);text-decoration:none;white-space:nowrap;overflow:hidden;position:relative}
.nav-item:hover{background:var(--bg-card);color:var(--text-primary)}
.nav-item.active{background:var(--primary-glow);color:var(--primary-light);border:1px solid var(--border-accent)}
.nav-item.active::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--primary);border-radius:0 2px 2px 0}
.nav-icon{width:18px;height:18px;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
.nav-badge{margin-left:auto;background:var(--danger);color:#fff;font-size:10px;font-weight:700;padding:1px 6px;border-radius:10px;min-width:18px;text-align:center}
.sidebar-footer{padding:14px;border-top:1px solid var(--border);flex-shrink:0}
.user-card{display:flex;align-items:center;gap:10px;padding:9px 10px;border-radius:var(--radius-md);cursor:pointer;transition:var(--t)}
.user-card:hover{background:var(--bg-card)}
.user-av{width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,var(--primary),var(--purple));display:flex;align-items:center;justify-content:center;font-weight:700;font-size:12px;flex-shrink:0}
.user-name{font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.user-role{font-size:11px;color:var(--text-muted)}
/* ── Main ─────────────────────────────────────────────────────── */
.main-wrapper{margin-left:var(--sidebar-w);display:flex;flex-direction:column;min-height:100vh;flex:1}
/* ── Topbar ─────────────────────────────────────────────────────── */
.topbar{position:sticky;top:0;z-index:50;height:var(--topbar-h);background:rgba(13,17,23,.92);backdrop-filter:blur(20px);border-bottom:1px solid var(--border);display:flex;align-items:center;gap:14px;padding:0 22px}
.topbar-left{flex:1;display:flex;align-items:center;gap:10px}
.topbar-title{font-size:17px;font-weight:700;letter-spacing:-.3px}
.topbar-sub{font-size:12px;color:var(--text-muted);font-weight:400;margin-left:6px}
.topbar-search{display:flex;align-items:center;gap:8px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-md);padding:7px 13px;width:260px;transition:var(--t)}
.topbar-search:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-glow)}
.topbar-search input{background:none;border:none;outline:none;color:var(--text-primary);font-size:13px;width:100%;font-family:inherit}
.topbar-search input::placeholder{color:var(--text-muted)}
.topbar-actions{display:flex;align-items:center;gap:8px}
.icon-btn{width:36px;height:36px;border-radius:var(--radius-md);border:1px solid var(--border);background:var(--bg-card);color:var(--text-secondary);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:var(--t);font-size:15px;position:relative}
.icon-btn:hover{background:var(--bg-elevated);color:var(--text-primary);border-color:var(--primary)}
.badge-dot{position:absolute;top:6px;right:6px;width:7px;height:7px;background:var(--danger);border-radius:50%;border:1.5px solid var(--bg-root)}
/* ── Content ─────────────────────────────────────────────────────── */
.content{flex:1;padding:22px 24px}
.page-header-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}
.page-title{font-size:21px;font-weight:800;letter-spacing:-.3px}
.page-sub{font-size:12.5px;color:var(--text-muted);margin-top:2px}
.page-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
/* ── Cards ─────────────────────────────────────────────────────── */
.card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);transition:var(--t)}
.card:hover{border-color:var(--border-strong)}
.card-header{display:flex;align-items:center;justify-content:space-between;padding:18px 20px 0}
.card-title{font-size:14px;font-weight:700;display:flex;align-items:center;gap:8px}
.card-body{padding:20px}
.card-footer{padding:12px 20px;border-top:1px solid var(--border)}
/* ── Stat Cards ─────────────────────────────────────────────────── */
.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:18px;position:relative;overflow:hidden;transition:var(--t-slow);cursor:default}
.stat-card::after{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--sc-color,var(--primary)),transparent);opacity:0;transition:var(--t)}
.stat-card:hover::after{opacity:1}
.stat-card:hover{transform:translateY(-2px);box-shadow:var(--shadow-md);border-color:var(--border-strong)}
.stat-icon{width:42px;height:42px;border-radius:var(--radius-md);display:flex;align-items:center;justify-content:center;font-size:19px;margin-bottom:14px}
.stat-label{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.6px;font-weight:700;margin-bottom:5px}
.stat-value{font-size:26px;font-weight:800;letter-spacing:-.5px;line-height:1}
.stat-change{font-size:11.5px;margin-top:6px;display:flex;align-items:center;gap:4px}
.stat-change.up{color:var(--success)}
.stat-change.down{color:var(--danger)}
/* ── Grids ─────────────────────────────────────────────────────── */
.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
.grid-3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
.grid-2{display:grid;grid-template-columns:repeat(2,1fr);gap:14px}
.grid-charts{display:grid;grid-template-columns:2fr 1fr;gap:14px}
.col-span-2{grid-column:span 2}
.col-span-3{grid-column:span 3}
/* ── Buttons ─────────────────────────────────────────────────────── */
.btn{display:inline-flex;align-items:center;gap:7px;padding:9px 18px;border-radius:var(--radius-md);border:1px solid transparent;font-size:13px;font-weight:600;cursor:pointer;transition:var(--t);font-family:inherit;white-space:nowrap;text-decoration:none;line-height:1}
.btn:active{transform:scale(.97)}
.btn-primary{background:var(--primary);color:#fff;border-color:var(--primary-dark)}
.btn-primary:hover{background:var(--primary-dark);box-shadow:0 0 22px var(--primary-glow2)}
.btn-secondary{background:var(--bg-elevated);color:var(--text-primary);border-color:var(--border)}
.btn-secondary:hover{background:var(--bg-card-hover);border-color:var(--primary);color:var(--primary-light)}
.btn-ghost{background:transparent;color:var(--text-secondary);border-color:transparent}
.btn-ghost:hover{background:var(--bg-card);color:var(--text-primary)}
.btn-danger{background:var(--danger-bg);color:var(--danger);border-color:rgba(239,68,68,.2)}
.btn-danger:hover{background:var(--danger);color:#fff}
.btn-success{background:var(--success-bg);color:var(--success);border-color:rgba(16,185,129,.2)}
.btn-success:hover{background:var(--success);color:#fff}
.btn-sm{padding:6px 12px;font-size:12px;border-radius:var(--radius-sm)}
.btn-lg{padding:12px 26px;font-size:14.5px}
.btn-icon{width:34px;height:34px;padding:0;justify-content:center;border-radius:var(--radius-sm);flex-shrink:0}
/* ── Badges ─────────────────────────────────────────────────────── */
.badge{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border-radius:100px;font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}
.badge-dot-ind{width:5px;height:5px;border-radius:50%;flex-shrink:0}
.badge-success{background:var(--success-bg);color:var(--success);border:1px solid rgba(16,185,129,.2)}
.badge-warning{background:var(--warning-bg);color:var(--warning);border:1px solid rgba(245,158,11,.2)}
.badge-danger{background:var(--danger-bg);color:var(--danger);border:1px solid rgba(239,68,68,.2)}
.badge-info{background:var(--info-bg);color:var(--info);border:1px solid rgba(59,130,246,.2)}
.badge-primary{background:var(--primary-glow);color:var(--primary-light);border:1px solid var(--border-accent)}
.badge-purple{background:var(--purple-bg);color:var(--purple);border:1px solid rgba(168,85,247,.2)}
.badge-neutral{background:var(--bg-elevated);color:var(--text-secondary);border:1px solid var(--border)}
.badge-cyan{background:var(--cyan-glow);color:var(--cyan-light);border:1px solid rgba(6,182,212,.2)}
/* ── Tables ─────────────────────────────────────────────────────── */
.table-wrapper{overflow-x:auto;border-radius:var(--radius-lg);border:1px solid var(--border);background:var(--bg-card)}
.data-table{width:100%;border-collapse:collapse;font-size:13px}
.data-table thead{background:var(--bg-surface);position:sticky;top:0;z-index:2}
.data-table th{padding:11px 14px;font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text-muted);text-align:left;border-bottom:1px solid var(--border);white-space:nowrap;cursor:pointer;user-select:none}
.data-table th:hover{color:var(--text-secondary)}
.data-table td{padding:13px 14px;border-bottom:1px solid var(--border);vertical-align:middle;color:var(--text-secondary)}
.data-table tr:last-child td{border-bottom:none}
.data-table tbody tr{transition:var(--t-fast);cursor:pointer}
.data-table tbody tr:hover{background:var(--bg-card-hover)}
.data-table tbody tr:hover td{color:var(--text-primary)}
.asset-cell{display:flex;align-items:center;gap:10px}
.asset-ic{width:30px;height:30px;border-radius:var(--radius-sm);background:var(--primary-glow);display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
.asset-n{font-weight:600;color:var(--text-primary);font-size:13px}
.asset-id{font-size:11px;color:var(--text-muted)}
/* ── Filters ─────────────────────────────────────────────────────── */
.filters-row{display:flex;align-items:center;gap:8px;margin-bottom:14px;flex-wrap:wrap}
.search-wrap{display:flex;align-items:center;gap:8px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-md);padding:8px 12px;flex:1;min-width:180px;max-width:340px;transition:var(--t)}
.search-wrap:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-glow)}
.search-wrap input{background:none;border:none;outline:none;color:var(--text-primary);font-size:13px;width:100%;font-family:inherit}
.search-wrap input::placeholder{color:var(--text-muted)}
.filter-sel{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-md);padding:8px 13px;color:var(--text-secondary);font-size:13px;font-family:inherit;outline:none;cursor:pointer;transition:var(--t)}
.filter-sel:focus{border-color:var(--primary);color:var(--text-primary)}
.filter-sel option{background:var(--bg-elevated)}
/* ── Forms ─────────────────────────────────────────────────────── */
.form-group{margin-bottom:16px}
.form-label{display:block;font-size:12px;font-weight:700;color:var(--text-secondary);margin-bottom:6px;letter-spacing:.2px}
.form-label .req{color:var(--danger);margin-left:3px}
.form-input,.form-select,.form-textarea{width:100%;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:10px 13px;color:var(--text-primary);font-size:13.5px;font-family:inherit;outline:none;transition:var(--t)}
.form-input:focus,.form-select:focus,.form-textarea:focus{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-glow);background:var(--bg-card)}
.form-input::placeholder,.form-textarea::placeholder{color:var(--text-muted)}
.form-select option{background:var(--bg-elevated)}
.form-textarea{resize:vertical;min-height:78px}
.form-hint{font-size:11px;color:var(--text-muted);margin-top:4px}
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.form-row-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px}
/* ── Tabs ─────────────────────────────────────────────────────── */
.tabs{border-bottom:1px solid var(--border);display:flex;gap:0;margin-bottom:18px;overflow-x:auto}
.tab-btn{padding:10px 18px;background:none;border:none;border-bottom:2px solid transparent;color:var(--text-muted);font-size:13px;font-weight:500;cursor:pointer;transition:var(--t);font-family:inherit;white-space:nowrap;display:flex;align-items:center;gap:7px;margin-bottom:-1px}
.tab-btn:hover{color:var(--text-primary)}
.tab-btn.active{color:var(--primary-light);border-bottom-color:var(--primary)}
.tab-content{display:none}
.tab-content.active{display:block}
/* ── Modals ─────────────────────────────────────────────────────── */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(4px);z-index:200;display:none;align-items:center;justify-content:center;padding:20px;animation:fadeIn .15s ease}
.modal-overlay.open{display:flex}
.modal{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-xl);width:100%;max-width:540px;max-height:90vh;display:flex;flex-direction:column;animation:slideUp .2s cubic-bezier(.34,1.56,.64,1);box-shadow:var(--shadow-lg),0 0 60px rgba(0,0,0,.5)}
.modal-lg{max-width:760px}
.modal-header{display:flex;align-items:center;justify-content:space-between;padding:18px 22px;border-bottom:1px solid var(--border);flex-shrink:0}
.modal-title{font-size:15px;font-weight:700}
.modal-close{width:30px;height:30px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg-surface);color:var(--text-secondary);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:15px;transition:var(--t)}
.modal-close:hover{color:var(--danger);border-color:var(--danger)}
.modal-body{padding:22px;overflow-y:auto;flex:1}
.modal-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:14px 22px;border-top:1px solid var(--border);flex-shrink:0}
/* ── Toasts ─────────────────────────────────────────────────────── */
.toast-container{position:fixed;top:18px;right:18px;z-index:9999;display:flex;flex-direction:column;gap:10px}
.toast{background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:var(--radius-md);padding:13px 16px;display:flex;align-items:center;gap:12px;font-size:13px;min-width:270px;max-width:370px;animation:slideInRight .3s cubic-bezier(.34,1.56,.64,1);box-shadow:var(--shadow-md)}
.toast.success{border-left:3px solid var(--success)}
.toast.error{border-left:3px solid var(--danger)}
.toast.warning{border-left:3px solid var(--warning)}
.toast.info{border-left:3px solid var(--info)}
.toast-icon{font-size:17px;flex-shrink:0}
.toast-msg{flex:1}
.toast-title{font-weight:600;margin-bottom:1px}
.toast-text{color:var(--text-muted);font-size:11.5px}
/* ── Kanban ─────────────────────────────────────────────────────── */
.kanban-board{display:flex;gap:14px;overflow-x:auto;padding-bottom:8px;align-items:flex-start}
.kanban-col{min-width:252px;max-width:268px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);flex-shrink:0}
.kanban-col-header{padding:13px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border)}
.kanban-col-title{font-size:12.5px;font-weight:700;display:flex;align-items:center;gap:8px}
.kanban-count{background:var(--bg-elevated);color:var(--text-muted);font-size:10px;font-weight:800;padding:2px 7px;border-radius:10px}
.kanban-cards{padding:10px;display:flex;flex-direction:column;gap:9px;min-height:120px}
.kanban-card{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-md);padding:13px;transition:var(--t);cursor:pointer}
.kanban-card:hover{border-color:var(--primary);transform:translateY(-1px);box-shadow:var(--shadow-md)}
.kanban-card-title{font-size:12.5px;font-weight:600;margin-bottom:7px;color:var(--text-primary)}
.kanban-meta{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap}
/* ── Timeline ─────────────────────────────────────────────────────── */
.timeline{display:flex;flex-direction:column}
.timeline-item{display:flex;gap:14px;padding-bottom:18px;position:relative}
.timeline-item:last-child{padding-bottom:0}
.tl-icon-wrap{display:flex;flex-direction:column;align-items:center;flex-shrink:0}
.tl-icon{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;z-index:1;flex-shrink:0}
.tl-line{width:2px;flex:1;background:var(--border);margin-top:3px}
.timeline-item:last-child .tl-line{display:none}
.tl-content{flex:1;padding-top:4px}
.tl-title{font-size:13px;font-weight:600;margin-bottom:2px}
.tl-desc{font-size:12px;color:var(--text-muted);margin-bottom:3px}
.tl-time{font-size:11px;color:var(--text-muted)}
/* ── Progress ─────────────────────────────────────────────────────── */
.progress-wrap{background:var(--bg-elevated);border-radius:100px;height:6px;overflow:hidden}
.progress-fill{height:100%;border-radius:100px;transition:width .6s ease;background:linear-gradient(90deg,var(--primary),var(--cyan))}
/* ── Alerts ─────────────────────────────────────────────────────── */
.alert{display:flex;align-items:flex-start;gap:12px;padding:13px 15px;border-radius:var(--radius-md);font-size:13px;margin-bottom:10px}
.alert-danger{background:var(--danger-bg);border:1px solid rgba(239,68,68,.2)}
.alert-warning{background:var(--warning-bg);border:1px solid rgba(245,158,11,.2)}
.alert-info{background:var(--info-bg);border:1px solid rgba(59,130,246,.2)}
.alert-success{background:var(--success-bg);border:1px solid rgba(16,185,129,.2)}
.alert-icon{font-size:17px;flex-shrink:0;margin-top:1px}
.alert-title{font-weight:600;margin-bottom:1px}
.alert-text{color:var(--text-muted);font-size:12px}
/* ── Empty State ─────────────────────────────────────────────────── */
.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:44px 24px;text-align:center}
.empty-icon{font-size:44px;margin-bottom:14px;opacity:.45}
.empty-title{font-size:15px;font-weight:700;margin-bottom:6px}
.empty-text{font-size:12.5px;color:var(--text-muted);max-width:280px;margin-bottom:18px}
/* ── Info Grid ─────────────────────────────────────────────────── */
.info-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:14px}
.info-label{font-size:10.5px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.6px;font-weight:700;margin-bottom:3px}
.info-value{font-size:13.5px;color:var(--text-primary);font-weight:500}
/* ── Depreciation Display ────────────────────────────────────────── */
.dep-bar-wrap{background:var(--bg-surface);border-radius:var(--radius-md);padding:16px;margin-bottom:14px}
.dep-nums{display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted);margin-bottom:8px}
/* ── QR Placeholder ─────────────────────────────────────────────── */
.qr-box{background:#fff;padding:14px;border-radius:var(--radius-md);display:inline-flex;flex-direction:column;align-items:center;gap:8px}
.qr-grid{width:100px;height:100px;display:grid;grid-template-columns:repeat(7,1fr);grid-template-rows:repeat(7,1fr);gap:1px}
.qr-c{background:#000;border-radius:1px}
.qr-c.w{background:#fff}
.qr-lbl{font-size:9px;color:#222;font-weight:700;font-family:monospace}
/* ── Pagination ─────────────────────────────────────────────────── */
.pagination{display:flex;align-items:center;gap:5px;justify-content:center;padding:14px;border-top:1px solid var(--border)}
.page-btn{width:30px;height:30px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg-card);color:var(--text-secondary);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:12.5px;transition:var(--t);font-family:inherit;font-weight:500}
.page-btn:hover{border-color:var(--primary);color:var(--primary-light)}
.page-btn.active{background:var(--primary);border-color:var(--primary);color:#fff}
/* ── Dropdown ─────────────────────────────────────────────────── */
.dropdown{position:relative}
.dropdown-menu{position:absolute;top:calc(100% + 8px);right:0;background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:var(--radius-md);padding:6px;min-width:175px;z-index:100;box-shadow:var(--shadow-md);display:none;animation:fadeIn .12s ease}
.dropdown-menu.open{display:block}
.dropdown-item{display:flex;align-items:center;gap:9px;padding:8px 11px;border-radius:var(--radius-sm);color:var(--text-secondary);font-size:13px;cursor:pointer;transition:var(--t-fast)}
.dropdown-item:hover{background:var(--bg-card);color:var(--text-primary)}
.dropdown-item.danger{color:var(--danger)}
.dropdown-item.danger:hover{background:var(--danger-bg)}
.dropdown-divider{height:1px;background:var(--border);margin:4px 0}
/* ── Activity ─────────────────────────────────────────────────── */
.activity-item{display:flex;align-items:flex-start;gap:11px;padding:11px 0;border-bottom:1px solid var(--border)}
.activity-item:last-child{border-bottom:none}
.act-av{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;flex-shrink:0}
.act-text{font-size:12.5px;color:var(--text-secondary);line-height:1.4}
.act-text strong{color:var(--text-primary);font-weight:600}
.act-time{font-size:11px;color:var(--text-muted);margin-top:2px}
/* ── Bulk Bar ─────────────────────────────────────────────────── */
.bulk-bar{display:none;align-items:center;gap:12px;background:var(--primary-glow);border:1px solid var(--border-accent);border-radius:var(--radius-md);padding:9px 15px;margin-bottom:10px}
.bulk-count{font-size:13px;font-weight:600;color:var(--primary-light);flex:1}
/* ── Workflow Steps ─────────────────────────────────────────────── */
.wf-steps{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.wf-step{display:flex;align-items:center;gap:7px;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-md);padding:8px 13px;font-size:12px;font-weight:600;color:var(--text-muted)}
.wf-step.active{border-color:var(--primary);color:var(--primary-light);background:var(--primary-glow)}
.wf-step.done{border-color:rgba(16,185,129,.3);color:var(--success);background:var(--success-bg)}
.wf-arr{color:var(--text-muted);font-size:12px}
/* ── Location Tree ─────────────────────────────────────────────── */
.tree-list{padding:0;list-style:none}
.tree-node{list-style:none}
.tree-label{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--radius-sm);cursor:pointer;transition:var(--t-fast);font-size:13px;color:var(--text-secondary)}
.tree-label:hover{background:var(--bg-card-hover);color:var(--text-primary)}
.tree-label.selected{background:var(--primary-glow);color:var(--primary-light)}
.tree-children{padding-left:18px;border-left:1px solid var(--border);margin-left:14px;display:none}
.tree-children.open{display:block}
.tree-arrow{transition:transform .2s;font-size:10px;color:var(--text-muted)}
.tree-arrow.open{transform:rotate(90deg)}
/* ── Perm Matrix ─────────────────────────────────────────────────── */
.perm-matrix{width:100%;border-collapse:collapse;font-size:12.5px}
.perm-matrix th,.perm-matrix td{padding:9px 13px;border:1px solid var(--border);text-align:center}
.perm-matrix th{background:var(--bg-surface);font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted);font-weight:700}
.perm-matrix td:first-child{text-align:left;font-weight:500;color:var(--text-secondary);background:var(--bg-surface)}
/* ── Report Cards ─────────────────────────────────────────────────── */
.report-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:18px;transition:var(--t);cursor:pointer}
.report-card:hover{border-color:var(--primary);transform:translateY(-2px);box-shadow:var(--shadow-md)}
.report-icon{width:44px;height:44px;border-radius:var(--radius-md);display:flex;align-items:center;justify-content:center;font-size:21px;margin-bottom:12px}
.report-title{font-size:13.5px;font-weight:700;margin-bottom:5px}
.report-desc{font-size:12px;color:var(--text-muted);line-height:1.5}
/* ── Step Wizard ─────────────────────────────────────────────────── */
.step-nav-item{display:flex;align-items:center;gap:12px;padding:11px;border-radius:var(--radius-md);cursor:pointer;transition:var(--t);margin-bottom:4px}
.step-nav-item:hover{background:var(--bg-card-hover)}
.step-nav-item.active{background:var(--primary-glow)}
.step-num{width:26px;height:26px;border-radius:50%;border:2px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--text-muted);flex-shrink:0;transition:var(--t)}
.step-nav-item.active .step-num{border-color:var(--primary);background:var(--primary);color:#fff}
.step-nav-item.done .step-num{border-color:var(--success);background:var(--success);color:#fff}
.step-nav-label{font-size:13px;font-weight:600}
.step-nav-sub{font-size:11px;color:var(--text-muted)}
/* ── Notif Panel ─────────────────────────────────────────────────── */
.notif-panel{position:absolute;top:calc(100% + 12px);right:0;width:320px;background:var(--bg-card);border:1px solid var(--border-strong);border-radius:var(--radius-lg);box-shadow:var(--shadow-lg);z-index:200;display:none;animation:slideUp .2s ease}
.notif-panel.open{display:block}
.notif-header{display:flex;align-items:center;justify-content:space-between;padding:13px 16px;border-bottom:1px solid var(--border)}
.notif-title{font-size:13.5px;font-weight:700}
.notif-item{display:flex;align-items:flex-start;gap:10px;padding:11px 16px;border-bottom:1px solid var(--border);cursor:pointer;transition:var(--t-fast)}
.notif-item:hover{background:var(--bg-card-hover)}
.notif-dot{width:7px;height:7px;border-radius:50%;background:var(--primary);flex-shrink:0;margin-top:5px}
.notif-text{font-size:12.5px;color:var(--text-secondary);line-height:1.4}
.notif-text strong{color:var(--text-primary)}
.notif-time{font-size:10.5px;color:var(--text-muted);margin-top:2px}
/* ── Login ─────────────────────────────────────────────────────── */
.login-page{min-height:100vh;display:flex;background:var(--bg-root);position:relative;overflow:hidden}
.login-bg-glow{position:absolute;inset:0;background:radial-gradient(ellipse at 20% 25%,rgba(99,102,241,.18) 0%,transparent 55%),radial-gradient(ellipse at 80% 75%,rgba(6,182,212,.12) 0%,transparent 50%),radial-gradient(ellipse at 50% 50%,rgba(168,85,247,.06) 0%,transparent 65%)}
.login-grid-bg{position:absolute;inset:0;background-image:linear-gradient(rgba(255,255,255,.015) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.015) 1px,transparent 1px);background-size:48px 48px}
.login-panel{width:100%;max-width:420px;margin:auto;z-index:1;padding:24px}
.login-logo-wrap{display:flex;align-items:center;gap:13px;margin-bottom:30px;justify-content:center}
.login-logo-icon{width:50px;height:50px;background:linear-gradient(135deg,var(--primary),var(--cyan));border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:23px;box-shadow:0 0 28px var(--primary-glow2)}
.login-logo-text{font-size:24px;font-weight:800;letter-spacing:-.4px}
.login-logo-text span{color:var(--primary-light)}
.login-card{background:rgba(19,25,38,.85);backdrop-filter:blur(22px);border:1px solid var(--border-strong);border-radius:var(--radius-xl);padding:30px;box-shadow:var(--shadow-lg)}
.login-heading{text-align:center;margin-bottom:26px}
.login-title{font-size:21px;font-weight:800;margin-bottom:5px}
.login-subtitle{font-size:12.5px;color:var(--text-muted)}
.login-divider{display:flex;align-items:center;gap:12px;margin:18px 0;color:var(--text-muted);font-size:11.5px}
.login-divider::before,.login-divider::after{content:'';flex:1;height:1px;background:var(--border)}
/* ── Section Header ─────────────────────────────────────────────── */
.section-hdr{font-size:11px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.6px;margin-bottom:10px;display:flex;align-items:center;gap:8px}
.section-hdr::after{content:'';flex:1;height:1px;background:var(--border)}
/* ── Utilities ─────────────────────────────────────────────────── */
.flex{display:flex}.flex-col{flex-direction:column}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-end{justify-content:flex-end}.flex-1{flex:1}.flex-wrap{flex-wrap:wrap}
.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.gap-5{gap:20px}
.mb-2{margin-bottom:8px}.mb-3{margin-bottom:12px}.mb-4{margin-bottom:16px}.mb-5{margin-bottom:20px}.mb-6{margin-bottom:24px}
.mt-2{margin-top:8px}.mt-3{margin-top:12px}.mt-4{margin-top:16px}
.p-4{padding:16px}.p-5{padding:20px}.p-6{padding:24px}
.text-xs{font-size:11px}.text-sm{font-size:12.5px}
.text-muted{color:var(--text-muted)}.text-secondary{color:var(--text-secondary)}.text-primary-c{color:var(--text-primary)}.text-success{color:var(--success)}.text-danger{color:var(--danger)}.text-warning{color:var(--warning)}.text-cyan{color:var(--cyan-light)}.text-accent{color:var(--primary-light)}
.text-right{text-align:right}.text-center{text-align:center}
.font-bold{font-weight:700}.font-semibold{font-weight:600}.font-medium{font-weight:500}
.w-full{width:100%}
.divider{border:none;border-top:1px solid var(--border);margin:18px 0}
.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rounded-full{border-radius:100px}
.shadow-primary{box-shadow:0 4px 20px var(--primary-glow2)}
/* ── Animations ─────────────────────────────────────────────────── */
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
@keyframes slideUp{from{opacity:0;transform:translateY(18px) scale(.96)}to{opacity:1;transform:translateY(0) scale(1)}}
@keyframes slideInRight{from{opacity:0;transform:translateX(28px)}to{opacity:1;transform:translateX(0)}}
@keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:.4}}
@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}
.animate-spin{animation:spin 1s linear infinite}
.animate-pulse{animation:pulse-dot 2s infinite}
.animate-fadein{animation:fadeIn .35s ease}
/* ── Settings Nav ────────────────────────────────────────────────── */
.settings-nav-item{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:var(--radius-md);cursor:pointer;transition:var(--t);font-size:13px;color:var(--text-secondary);margin-bottom:2px}
.settings-nav-item:hover{background:var(--bg-card-hover);color:var(--text-primary)}
.settings-nav-item.active{background:var(--primary-glow);color:var(--primary-light)}
/* ── Toggle / Switch ─────────────────────────────────────────────── */
.toggle{display:inline-flex;align-items:center;cursor:pointer;flex-shrink:0}
.toggle input{opacity:0;width:0;height:0;position:absolute}
.toggle-slider{width:40px;height:22px;background:var(--bg-elevated);border:1px solid var(--border);border-radius:100px;position:relative;transition:var(--t)}
.toggle-slider::before{content:'';position:absolute;width:16px;height:16px;background:#fff;border-radius:50%;top:2px;left:2px;transition:var(--t)}
.toggle input:checked+.toggle-slider{background:var(--primary);border-color:var(--primary)}
.toggle input:checked+.toggle-slider::before{transform:translateX(18px)}
/* ── Theme Swatches ──────────────────────────────────────────────── */
.theme-opt{display:flex;flex-direction:column;align-items:center;gap:7px;cursor:pointer;padding:10px 14px;border-radius:var(--radius-md);border:2px solid transparent;transition:var(--t);font-size:11.5px;color:var(--text-muted)}
.theme-opt:hover{background:var(--bg-card)}
.theme-opt.selected{border-color:var(--primary);background:var(--primary-glow);color:var(--primary-light)}
.theme-swatch{width:52px;height:34px;border-radius:var(--radius-sm);border:1px solid var(--border)}
.theme-swatch.dark{background:linear-gradient(135deg,#07090F 60%,#1E2A3A)}
.theme-swatch.light{background:linear-gradient(135deg,#F8FAFC 60%,#E2E8F0)}
.theme-swatch.auto{background:linear-gradient(135deg,#07090F 50%,#F8FAFC 50%)}
/* ── Input Group ─────────────────────────────────────────────────── */
.input-group{display:flex;align-items:stretch}
.input-prefix{background:var(--bg-elevated);border:1px solid var(--border);border-right:none;border-radius:var(--radius-md) 0 0 var(--radius-md);padding:10px 13px;color:var(--text-muted);font-size:13.5px;display:flex;align-items:center;flex-shrink:0}
/* ── Row Selected ────────────────────────────────────────────────── */
.row-selected{background:var(--primary-glow) !important}
.row-selected td{color:var(--text-primary) !important}
/* ── bg-elevated utility ─────────────────────────────────────────── */
.bg-elevated{background:var(--bg-elevated)}
.border{border:1px solid var(--border)}
.rounded-md{border-radius:var(--radius-md)}
/* ── Vendor / Integration Card Extras ───────────────────────────── */
.flex-shrink-0{flex-shrink:0}
/* ── Responsive ─────────────────────────────────────────────────── */
@media(max-width:1280px){.grid-4{grid-template-columns:repeat(2,1fr)}}
@media(max-width:960px){:root{--sidebar-w:0px}.sidebar{transform:translateX(-100%);width:256px}.sidebar.open{transform:translateX(0)}.main-wrapper{margin-left:0}.grid-charts{grid-template-columns:1fr}.grid-2{grid-template-columns:1fr}.form-row{grid-template-columns:1fr}}
@media(max-width:600px){.grid-4,.grid-3{grid-template-columns:1fr}.info-grid{grid-template-columns:1fr}}

323
dashboard.html Normal file
View File

@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Dashboard | AMS</title>
<meta name="description" content="Asset Management System Executive Dashboard with KPIs and analytics">
<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">
<!-- ════ SIDEBAR ════ -->
<aside class="sidebar">
<div class="sidebar-logo">
<div class="logo-icon">📦</div>
<div><div class="logo-title">AMS</div><div class="logo-sub">Asset Management</div></div>
</div>
<nav class="sidebar-nav">
<div class="nav-section-label">Overview</div>
<a href="dashboard.html" class="nav-item active"><span class="nav-icon">📊</span> Dashboard</a>
<div class="nav-section-label">Assets</div>
<a href="assets.html" class="nav-item"><span class="nav-icon">📦</span> All Assets</a>
<a href="asset-create.html" class="nav-item"><span class="nav-icon"></span> Add Asset</a>
<div class="nav-section-label">Supply Chain</div>
<a href="inventory.html" class="nav-item"><span class="nav-icon">🏪</span> Inventory <span class="nav-badge" style="background:var(--warning)">5</span></a>
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement <span class="nav-badge">8</span></a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><span class="nav-icon">🔧</span> Maintenance <span class="nav-badge">12</span></a>
<a href="reports.html" class="nav-item"><span class="nav-icon">📈</span> Reports &amp; Audit</a>
<div class="nav-section-label">Administration</div>
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users &amp; Roles</a>
<a href="settings.html" class="nav-item"><span class="nav-icon">⚙️</span> Settings</a>
</nav>
<div class="sidebar-footer">
<div class="user-card">
<div class="user-av">AS</div>
<div style="flex:1;min-width:0">
<div class="user-name">Arjun Sharma</div>
<div class="user-role">Asset Manager</div>
</div>
<span style="color:var(--text-muted)"></span>
</div>
</div>
</aside>
<!-- ════ MAIN ════ -->
<div class="main-wrapper">
<!-- Topbar -->
<header class="topbar">
<div class="topbar-left">
<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>
<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>
<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>
</div>
</header>
<main class="content">
<!-- ── Alerts ──────────────────────── -->
<div id="alertsArea" class="mb-4"></div>
<!-- ── KPI Cards ──────────────────── -->
<div class="grid-4 mb-4" id="kpiCards"></div>
<!-- ── Charts Row ─────────────────── -->
<div class="grid-charts mb-4">
<div class="card">
<div class="card-header">
<span class="card-title">📊 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">
<canvas id="statusChart" height="260"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">🍕 Assets by Category</span>
</div>
<div class="card-body">
<canvas id="categoryChart" height="260"></canvas>
</div>
</div>
</div>
<!-- ── Second Charts Row ──────────── -->
<div class="grid-2 mb-4">
<div class="card">
<div class="card-header">
<span class="card-title">💰 Depreciation Overview</span>
<button class="btn btn-ghost btn-sm" onclick="window.location.href='reports.html'">Details →</button>
</div>
<div class="card-body">
<canvas id="depreciationChart" height="210"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<span class="card-title">⚠️ Action Items</span>
<span class="badge badge-danger" style="font-size:11px">Needs Attention</span>
</div>
<div class="card-body" id="actionItems"></div>
</div>
</div>
<!-- ── Bottom Row ─────────────────── -->
<div class="grid-2">
<div class="card">
<div class="card-header">
<span class="card-title">🕐 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>
</div>
<div class="card-body">
<canvas id="deptChart" height="240"></canvas>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Confirm Modal (shared) -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width:420px">
<div class="modal-header"><span class="modal-title confirm-title">Confirm Action</span><button class="modal-close"></button></div>
<div class="modal-body"><p class="confirm-message" style="color:var(--text-secondary);font-size:13.5px"></p></div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('confirmModal')">Cancel</button><button class="btn btn-danger confirm-ok">Confirm</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();
renderKPIs();
renderActivity();
renderActionItems();
initCharts();
});
function renderAlerts() {
const alerts = [
{ type:'warning', icon:'⚠️', title:'3 AMC contracts expiring within 30 days', text:'Cisco (Jun 30), HP Warranty (Aug 20), Epson (Nov 10). Renew before expiry to avoid service gaps.' },
{ type:'danger', icon:'🔴', title:'PM overdue: 2 scheduled maintenances not completed', text:'Daikin AC (Floor 2) and Ricoh Photocopier (Admin) are past due for preventive maintenance.' }
];
document.getElementById('alertsArea').innerHTML = alerts.map(a => `
<div class="alert alert-${a.type}">
<div class="alert-icon">${a.icon}</div>
<div><div class="alert-title">${a.title}</div><div class="alert-text">${a.text}</div></div>
<button onclick="this.closest('.alert').remove()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:16px;margin-left:auto;padding:0">×</button>
</div>`).join('');
}
function renderKPIs() {
const s = AMS.stats;
const kpis = [
{ label:'Total Assets', value: s.total.toLocaleString('en-IN'), icon:'📦', color:'var(--primary)', bg:'var(--primary-glow)', change:'+3.2%', up:true, link:'assets.html' },
{ label:'Active Assets', value: s.active.toLocaleString('en-IN'),icon:'✅', color:'var(--success)', bg:'var(--success-bg)', change:'+1.4%', up:true, link:'assets.html' },
{ 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' }
];
document.getElementById('kpiCards').innerHTML = kpis.map(k => `
<div class="stat-card" style="--sc-color:${k.color};cursor:pointer" onclick="window.location.href='${k.link}'">
<div class="stat-icon" style="background:${k.bg};color:${k.color}">${k.icon}</div>
<div class="stat-label">${k.label}</div>
<div class="stat-value" style="color:${k.color}">${k.value}</div>
<div class="stat-change ${k.up?'up':'down'}">${k.up?'↑':'↓'} ${k.change}</div>
</div>`).join('');
}
function renderActivity() {
document.getElementById('activityFeed').innerHTML = AMS.activityFeed.map(a => `
<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-time">${a.time}</div>
</div>
</div>`).join('');
}
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' },
{ icon:'🖨️', text:'Ricoh photocopier out of service (TKT-005)', link:'maintenance.html', priority:'danger' }
];
document.getElementById('actionItems').innerHTML = items.map(item => `
<div class="flex items-center gap-3 mb-3" style="cursor:pointer;padding:10px;background:var(--bg-surface);border-radius:var(--radius-md);border:1px solid var(--border);transition:var(--t)" onclick="window.location.href='${item.link}'" onmouseenter="this.style.borderColor='var(--${item.priority})'" onmouseleave="this.style.borderColor='var(--border)'">
<span style="font-size:18px">${item.icon}</span>
<span style="font-size:12.5px;color:var(--text-secondary);flex:1">${item.text}</span>
<span class="badge badge-${item.priority}" style="font-size:9px">${item.priority==='danger'?'Critical':item.priority==='warning'?'High':'Info'}</span>
</div>`).join('');
}
function initCharts() {
const gridColor = 'rgba(255,255,255,0.04)';
const tickColor = '#4B5563';
// Status Chart (Bar)
new Chart(document.getElementById('statusChart'), {
type: 'bar',
data: {
labels: ['Active','Idle','Maintenance','Disposed','Lost/Stolen'],
datasets: [{
label: 'Assets',
data: [1089, 63, 47, 48, 0],
backgroundColor: ['rgba(16,185,129,.75)','rgba(245,158,11,.75)','rgba(59,130,246,.75)','rgba(75,85,99,.75)','rgba(239,68,68,.75)'],
borderColor: ['#10B981','#F59E0B','#3B82F6','#4B5563','#EF4444'],
borderWidth: 1, borderRadius: 6
}]
},
options: {
responsive:true, maintainAspectRatio:false, plugins:{ legend:{display:false} },
scales:{
x:{grid:{color:gridColor},ticks:{color:tickColor,font:{family:'Inter',size:11}}},
y:{grid:{color:gridColor},ticks:{color:tickColor,font:{family:'Inter',size:11}}}
}
}
});
// Category Doughnut
const cats = AMS.categories.slice(0,8);
new Chart(document.getElementById('categoryChart'), {
type: 'doughnut',
data: {
labels: cats.map(c=>c.name),
datasets:[{ data: cats.map(c=>c.count),
backgroundColor:['#6366F1','#06B6D4','#10B981','#F59E0B','#EF4444','#A855F7','#3B82F6','#14B8A6'],
borderColor: 'rgba(19,25,38,0)', borderWidth:2, hoverOffset:6
}]
},
options:{
responsive:true, maintainAspectRatio:false, cutout:'62%',
plugins:{ legend:{position:'right', labels:{color:'#94A3B8',font:{family:'Inter',size:11},padding:10,usePointStyle:true}} }
}
});
// Depreciation Line
const months = ['Oct','Nov','Dec','Jan','Feb','Mar','Apr','May'];
new Chart(document.getElementById('depreciationChart'), {
type: 'line',
data: {
labels: months,
datasets: [
{ label:'Gross Value', data:[45200000,45500000,45800000,46200000,46800000,47200000,47600000,48000000],
borderColor:'#6366F1', backgroundColor:'rgba(99,102,241,.08)', fill:true, tension:.4, pointRadius:3, borderWidth:2 },
{ label:'Net Book Value', data:[33200000,32800000,32300000,31900000,31400000,30900000,30500000,30100000],
borderColor:'#10B981', backgroundColor:'rgba(16,185,129,.07)', fill:true, tension:.4, pointRadius:3, borderWidth:2 }
]
},
options:{
responsive:true, maintainAspectRatio:false,
plugins:{ legend:{labels:{color:'#94A3B8',font:{family:'Inter',size:11},usePointStyle:true}} },
scales:{
x:{grid:{color:gridColor},ticks:{color:tickColor,font:{family:'Inter',size:11}}},
y:{grid:{color:gridColor},ticks:{color:tickColor,font:{family:'Inter',size:11},callback:v=>'₹'+(v/1e5).toFixed(0)+'L'}}
}
}
});
// Department Horizontal Bar
const depts = AMS.departments;
new Chart(document.getElementById('deptChart'), {
type: 'bar',
data: {
labels: depts.map(d=>d.code),
datasets:[{
label:'Assets', data: depts.map(d=>d.assetCount),
backgroundColor:'rgba(99,102,241,.6)', borderColor:'#6366F1', borderWidth:1, borderRadius:5
}]
},
options:{
indexAxis:'y', responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false}},
scales:{
x:{grid:{color:gridColor},ticks:{color:tickColor,font:{family:'Inter',size:11}}},
y:{grid:{color:gridColor},ticks:{color:tickColor,font:{family:'Inter',size:11}}}
}
}
});
}
// Notification panel
function markAllRead() {
AMS.notifications.forEach(n=>n.read=true);
renderNotifPanel();
document.querySelector('.badge-dot').style.display='none';
showToast('All caught up!','All notifications marked as read','success');
}
</script>
</body>
</html>

138
index.html Normal file
View File

@ -0,0 +1,138 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Login | AMS — Asset Management System</title>
<meta name="description" content="Secure login to Acme Corporation Asset Management System">
<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">
<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}
@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}
.fc3{bottom:20%;left:8%;animation-delay:.8s}
</style>
</head>
<body>
<div class="login-page">
<div class="login-bg-glow"></div>
<div class="login-grid-bg"></div>
<!-- Floating stat cards (decorative) -->
<div class="floating-card fc1">
<div style="color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:.6px;margin-bottom:4px">Total Assets</div>
<div style="font-size:22px;font-weight:800;color:var(--text-primary)">1,247</div>
<div style="color:var(--success);font-size:11px;margin-top:2px">↑ +3.2% this month</div>
</div>
<div class="floating-card fc2">
<div style="color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:.6px;margin-bottom:4px">Asset Value</div>
<div style="font-size:20px;font-weight:800;color:var(--text-primary)">₹4.29Cr</div>
<div style="color:var(--cyan-light);font-size:11px;margin-top:2px">Net book value</div>
</div>
<div class="floating-card fc3">
<div style="color:var(--text-muted);font-size:10px;text-transform:uppercase;letter-spacing:.6px;margin-bottom:4px">Compliance</div>
<div style="font-size:22px;font-weight:800;color:var(--success)">98.4%</div>
<div style="color:var(--text-muted);font-size:11px;margin-top:2px">Audit ready ✓</div>
</div>
<div class="login-panel animate-fadein">
<div class="login-logo-wrap">
<div class="login-logo-icon">📦</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>
</div>
</div>
<div style="text-align:center;margin-bottom:18px">
<div class="feature-pill">🏢 Acme Corporation Pvt. Ltd.</div>
</div>
<div class="login-card">
<div class="login-heading">
<div class="login-title">Welcome back 👋</div>
<div class="login-subtitle">Sign in to manage your organization's assets</div>
</div>
<form id="loginForm" onsubmit="handleLogin(event)">
<div class="form-group">
<label class="form-label">Email Address <span class="req">*</span></label>
<input id="loginEmail" class="form-input" type="email" placeholder="you@acmecorp.com" value="arjun.sharma@acmecorp.com" required>
</div>
<div class="form-group">
<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>
</div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:18px">
<label class="flex items-center gap-2" style="cursor:pointer;font-size:12.5px;color:var(--text-secondary)">
<input type="checkbox" checked> Remember me
</label>
<a href="#" style="font-size:12.5px;color:var(--primary-light)">Forgot password?</a>
</div>
<button type="submit" class="btn btn-primary w-full btn-lg" id="loginBtn">
<span id="loginBtnText">Sign In →</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>
<button class="btn btn-secondary" onclick="loginAs('admin')">
<span>👑</span> 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:11px;color:var(--text-muted)">A 6-digit OTP will be sent to your registered mobile/email upon successful password verification.</div>
</div>
</div>
<div style="text-align:center;margin-top:20px;font-size:11.5px;color:var(--text-muted)">
AMS v1.0 &nbsp;·&nbsp; <a href="#">Privacy Policy</a> &nbsp;·&nbsp; <a href="#">Support</a>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
<script>
function togglePwd() {
const inp = document.getElementById('loginPassword');
const btn = document.getElementById('eyeBtn');
inp.type = inp.type === 'password' ? 'text' : 'password';
btn.textContent = inp.type === 'password' ? '👁️' : '🙈';
}
function handleLogin(e) {
e.preventDefault();
const btn = document.getElementById('loginBtn');
const txt = document.getElementById('loginBtnText');
btn.disabled = true;
txt.innerHTML = '<span class="animate-spin" style="display:inline-block"></span> Authenticating…';
setTimeout(() => {
showToast('Login Successful', 'Welcome back, Arjun Sharma!', 'success');
setTimeout(() => { window.location.href = 'dashboard.html'; }, 800);
}, 1400);
}
function loginAs(role) {
showToast('Signing in…', `Logging in as ${role.replace('-',' ')}`, 'info');
setTimeout(() => { window.location.href = 'dashboard.html'; }, 1000);
}
</script>
</body>
</html>

383
inventory.html Normal file
View File

@ -0,0 +1,383 @@
<!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>

275
js/app.js Normal file
View File

@ -0,0 +1,275 @@
// ================================================================
// AMS — Core Application Logic | app.js
// ================================================================
/* ── Active Nav ─────────────────────────────────────────────────── */
function setActiveNav() {
const page = window.location.pathname.split('/').pop() || 'dashboard.html';
document.querySelectorAll('.nav-item').forEach(el => {
const href = el.getAttribute('href') || '';
if (href && page === href) el.classList.add('active');
else el.classList.remove('active');
});
}
/* ── Modals ─────────────────────────────────────────────────────── */
function openModal(id) {
const el = document.getElementById(id);
if (el) { el.classList.add('open'); document.body.style.overflow = 'hidden'; }
}
function closeModal(id) {
const el = document.getElementById(id);
if (el) { el.classList.remove('open'); document.body.style.overflow = ''; }
}
/* ── Tabs ─────────────────────────────────────────────────────── */
function initTabs(container) {
const root = container || document;
root.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const tabsEl = btn.closest('.tabs');
const tabGroup = tabsEl ? tabsEl.dataset.group : null;
const targetId = btn.dataset.tab;
tabsEl.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const scope = btn.closest('.tab-container') || btn.closest('.card') || btn.closest('.content') || document;
scope.querySelectorAll('.tab-content').forEach(tc => {
if (tabGroup && tc.dataset.group && tc.dataset.group !== tabGroup) return;
tc.classList.toggle('active', tc.id === targetId);
});
});
});
}
/* ── Toast ─────────────────────────────────────────────────────── */
function showToast(title, text='', type='success', dur=4200) {
const icons = { success:'✅', error:'❌', warning:'⚠️', info:'' };
const c = document.getElementById('toastContainer');
if (!c) return;
const t = document.createElement('div');
t.className = `toast ${type}`;
t.innerHTML = `<div class="toast-icon">${icons[type]}</div>
<div class="toast-msg"><div class="toast-title">${title}</div>${text?`<div class="toast-text">${text}</div>`:''}</div>
<button onclick="this.parentElement.remove()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:18px;padding:0 2px;margin-left:8px;line-height:1">×</button>`;
c.appendChild(t);
setTimeout(() => { t.style.opacity='0'; t.style.transform='translateX(30px)'; t.style.transition='all .3s ease'; setTimeout(()=>t.remove(),350); }, dur);
}
/* ── Table Search ─────────────────────────────────────────────── */
function initSearch(inputId, tableId) {
const inp = document.getElementById(inputId);
const tbl = document.getElementById(tableId);
if (!inp || !tbl) return;
inp.addEventListener('input', () => {
const q = inp.value.toLowerCase().trim();
let vis = 0;
tbl.querySelectorAll('tbody tr').forEach(r => {
const show = !q || r.textContent.toLowerCase().includes(q);
r.style.display = show ? '' : 'none';
if (show) vis++;
});
const cnt = document.getElementById(tableId + '-count');
if (cnt) cnt.textContent = vis;
});
}
/* ── Select Filter ─────────────────────────────────────────────── */
function initFilter(selId, tableId, colIdx) {
const sel = document.getElementById(selId);
const tbl = document.getElementById(tableId);
if (!sel || !tbl) return;
sel.addEventListener('change', () => {
const v = sel.value.toLowerCase();
tbl.querySelectorAll('tbody tr').forEach(r => {
if (!v) { r.style.display = ''; return; }
const cell = r.cells[colIdx];
r.style.display = (cell && cell.textContent.toLowerCase().includes(v)) ? '' : 'none';
});
});
}
/* ── Bulk Checkboxes ─────────────────────────────────────────── */
function initCheckboxes(tableId, bulkBarId) {
const tbl = document.getElementById(tableId);
const bar = document.getElementById(bulkBarId);
if (!tbl) return;
const all = tbl.querySelector('.select-all');
const rows = () => [...tbl.querySelectorAll('.row-check')];
const updateBar = () => {
if (!bar) return;
const n = rows().filter(c => c.checked).length;
bar.style.display = n > 0 ? 'flex' : 'none';
const cnt = bar.querySelector('.bulk-count');
if (cnt) cnt.textContent = `${n} item${n!==1?'s':''} selected`;
};
if (all) {
all.addEventListener('change', () => {
rows().forEach(c => { c.checked = all.checked; c.closest('tr').classList.toggle('row-selected', all.checked); });
updateBar();
});
}
rows().forEach(c => {
c.addEventListener('change', () => {
c.closest('tr').classList.toggle('row-selected', c.checked);
if (all) all.checked = rows().every(r => r.checked);
updateBar();
});
});
}
/* ── Confirm Dialog ─────────────────────────────────────────── */
function confirmAction(title, msg, cb, type='danger') {
const m = document.getElementById('confirmModal');
if (!m) { if (confirm(msg) && cb) cb(); return; }
m.querySelector('.confirm-title').textContent = title;
m.querySelector('.confirm-message').textContent = msg;
const btn = m.querySelector('.confirm-ok');
btn.onclick = () => { closeModal('confirmModal'); if (cb) cb(); };
openModal('confirmModal');
}
/* ── Dropdowns ─────────────────────────────────────────────── */
function initDropdowns() {
document.querySelectorAll('[data-dropdown]').forEach(trigger => {
trigger.addEventListener('click', e => {
e.stopPropagation();
const menuId = trigger.dataset.dropdown;
const menu = document.getElementById(menuId);
if (!menu) return;
document.querySelectorAll('.dropdown-menu.open').forEach(m => { if (m.id !== menuId) m.classList.remove('open'); });
menu.classList.toggle('open');
});
});
document.addEventListener('click', () => {
document.querySelectorAll('.dropdown-menu.open').forEach(m => m.classList.remove('open'));
});
}
/* ── Tree ─────────────────────────────────────────────────── */
function initTrees() {
document.querySelectorAll('.tree-toggle').forEach(t => {
t.addEventListener('click', () => {
const ch = t.closest('.tree-node').querySelector('.tree-children');
const ar = t.querySelector('.tree-arrow');
if (ch) { ch.classList.toggle('open'); if (ar) ar.classList.toggle('open'); }
});
});
}
/* ── Notif Panel ─────────────────────────────────────────────── */
function initNotifPanel() {
const bell = document.getElementById('notifBell');
const panel = document.getElementById('notifPanel');
if (bell && panel) {
bell.addEventListener('click', e => { e.stopPropagation(); panel.classList.toggle('open'); });
document.addEventListener('click', e => { if (!panel.contains(e.target) && e.target !== bell) panel.classList.remove('open'); });
}
}
/* ── Sidebar User Info ─────────────────────────────────────── */
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;
}
/* ── Topbar Notifications Badge ─────────────────────────────── */
function populateNotifBadge() {
if (!window.AMS) return;
const unread = AMS.notifications.filter(n => !n.read).length;
const dot = document.querySelector('#notifBell .badge-dot');
if (dot) dot.style.display = unread > 0 ? '' : 'none';
}
/* ── Render Notifications Panel ─────────────────────────────── */
function renderNotifPanel() {
if (!window.AMS) return;
const list = document.getElementById('notifList');
if (!list) return;
list.innerHTML = AMS.notifications.map(n => `
<div class="notif-item" onclick="markNotifRead('${n.id}')">
${n.read ? '' : '<div class="notif-dot"></div>'}
<div style="flex:1">
<div class="notif-text">${n.text}</div>
<div class="notif-time">${n.time}</div>
</div>
</div>`).join('');
}
function markNotifRead(id) {
if (window.AMS) {
const n = AMS.notifications.find(x => x.id === id);
if (n) n.read = true;
populateNotifBadge();
renderNotifPanel();
}
}
/* ── Pagination ─────────────────────────────────────────────── */
function renderPagination(containerId, total, perPage=10, current=1, onChange) {
const c = document.getElementById(containerId);
if (!c) return;
const pages = Math.ceil(total / perPage);
let html = `<button class="page-btn" onclick="(${onChange})(${Math.max(1,current-1)})" ${current===1?'disabled':''}></button>`;
for (let i=1;i<=pages;i++) {
html += `<button class="page-btn${i===current?' active':''}" onclick="(${onChange})(${i})">${i}</button>`;
}
html += `<button class="page-btn" onclick="(${onChange})(${Math.min(pages,current+1)})" ${current===pages?'disabled':''}></button>`;
c.innerHTML = html;
}
/* ── Download CSV ─────────────────────────────────────────── */
function downloadCSV(data, filename='export.csv') {
if (!data.length) return;
const headers = Object.keys(data[0]).join(',');
const rows = data.map(r => Object.values(r).map(v => `"${String(v).replace(/"/g,'""')}"`).join(','));
const csv = [headers, ...rows].join('\n');
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([csv],{type:'text/csv'}));
a.download = filename;
a.click();
}
/* ── 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;
}
/* ── DOMContentLoaded Init ─────────────────────────────────── */
document.addEventListener('DOMContentLoaded', () => {
setActiveNav();
initTabs();
initDropdowns();
initTrees();
initNotifPanel();
populateSidebarUser();
populateNotifBadge();
renderNotifPanel();
// Auto close modal on overlay click
document.querySelectorAll('.modal-overlay').forEach(ov => {
ov.addEventListener('click', e => { if (e.target === ov) { ov.classList.remove('open'); document.body.style.overflow = ''; } });
});
// Auto close modal on .modal-close
document.querySelectorAll('.modal-close').forEach(btn => {
btn.addEventListener('click', () => {
const ov = btn.closest('.modal-overlay');
if (ov) { ov.classList.remove('open'); document.body.style.overflow = ''; }
});
});
});

197
js/data.js Normal file
View File

@ -0,0 +1,197 @@
// ================================================================
// AMS — Sample Data Store | Version 1.0
// ================================================================
const AMS = {
currentUser: {
id: 'u001', name: 'Arjun Sharma', email: 'arjun.sharma@acmecorp.com',
role: 'Asset Manager', dept: 'IT', avatar: 'AS', location: 'HQ Bengaluru'
},
company: {
name: 'Acme Corporation Pvt. Ltd.', currency: '₹', currencyCode: 'INR',
fy: 'Apr 2025 Mar 2026', gst: '29AABCU9603R1ZX',
address: '12th Floor, Prestige Tech Park, Outer Ring Road, Bengaluru 560103'
},
stats: {
total: 1247, active: 1089, maintenance: 47, idle: 63, disposed: 48,
totalValue: 42850000, netValue: 30100000, depreciated: 12750000,
pendingTickets: 12, lowStock: 5, expiringAMC: 3, pendingPRs: 8
},
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:'🗄️' }
],
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' }
],
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' }
],
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 }
],
vendors: [
{ id:'v001', name:'Dell India Pvt. Ltd.', contact:'Suresh Iyer', email:'suresh.iyer@dell.com', phone:'+91-80-4258-9001', cat:'IT Hardware', gst:'29AAACL0532H1ZV', rating:4.5, status:'Active', contracts:3 },
{ id:'v002', name:'HP India Pvt. Ltd.', contact:'Meera Rao', email:'meera.rao@hp.com', phone:'+91-80-6688-1234', cat:'IT Hardware', gst:'07AAACH0132Q1ZB', rating:4.2, status:'Active', contracts:2 },
{ 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 }
],
categories: [
{ id:'c01', name:'Laptops', icon:'💻', count:187, depM:'SLM', depR:20, life:5, parent:'IT Equipment', minStock:10 },
{ id:'c02', name:'Desktops', icon:'🖥️', count:94, depM:'SLM', depR:20, life:5, parent:'IT Equipment', minStock:5 },
{ id:'c03', name:'Servers', icon:'🖥️', count:23, depM:'WDV', depR:30, life:4, parent:'IT Equipment', minStock:2 },
{ id:'c04', name:'Printers', icon:'🖨️', count:34, depM:'SLM', depR:25, life:4, parent:'IT Equipment', minStock:3 },
{ id:'c05', name:'Networking', icon:'🔌', count:67, depM:'WDV', depR:30, life:4, parent:'IT Equipment', minStock:5 },
{ id:'c06', name:'Mobile Devices', icon:'📱', count:145, depM:'SLM', depR:20, life:3, parent:'IT Equipment', minStock:10 },
{ id:'c07', name:'AV Equipment', icon:'📽️', count:29, depM:'SLM', depR:20, life:5, parent:'Electronics', minStock:2 },
{ id:'c08', name:'Displays', icon:'🖥️', count:78, depM:'SLM', depR:25, life:4, parent:'Electronics', minStock:5 },
{ id:'c09', name:'Furniture', icon:'🪑', count:312, depM:'SLM', depR:10, life:10, parent:'Office Furniture', minStock:20 },
{ 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 }
],
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:'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:'d6', name:'Administration', code:'ADMIN', head:'Deepak Joshi', assetCount:110, headcount:12 }
],
locations: [
{ id:'l1', name:'HQ Bengaluru', children: [
{ id:'l1a', name:'Basement', children:[{id:'l1a1',name:'Server Room B1'},{id:'l1a2',name:'Parking B1'}] },
{ id:'l1b', name:'Floor 1', children:[{id:'l1b1',name:'Reception'},{id:'l1b2',name:'Finance Floor 1'},{id:'l1b3',name:'Admin Floor 1'},{id:'l1b4',name:'Operations Floor 1'}] },
{ id:'l1c', name:'Floor 2', children:[{id:'l1c1',name:'IT Dept Floor 2'},{id:'l1c2',name:'HR Floor 2'},{id:'l1c3',name:'Conference Room A'},{id:'l1c4',name:'Conference Room B'}] },
{ id:'l1d', name:'Floor 3', children:[{id:'l1d1',name:'Marketing Floor 3'},{id:'l1d2',name:'Sales Floor'},{id:'l1d3',name:'Executive Suite'}] }
]},
{ id:'l2', name:'Warehouse Whitefield', children:[{id:'l2a',name:'Store A'},{id:'l2b',name:'Store B'}] },
{ id:'l3', name:'Branch Hyderabad', children:[{id:'l3a',name:'Office Floor 1'},{id:'l3b',name:'Office Floor 2'}] }
],
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-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' }
],
activityFeed: [
{ user:'Arjun Sharma', av:'AS', color:'#6366F1', action:'assigned', target:'Dell Latitude 5540', detail:'to Priya Kumar', time:'2 hours ago' },
{ user:'Kavya Nair', av:'KN', color:'#10B981', action:'raised ticket', target:'TKT-003', detail:'Printer paper jam Finance', time:'3 hours ago' },
{ user:'Priya Kumar', av:'PK', color:'#10B981', action:'approved PR', target:'PR-2025-001', detail:'5× Dell Laptops', time:'Yesterday' },
{ user:'System', av:'🤖', color:'#6366F1', action:'auto-generated', target:'Depreciation Report', detail:'Monthly SLM run completed', time:'Yesterday' },
{ user:'Deepak Joshi', av:'DJ', color:'#3B82F6', action:'created PR', target:'PR-2025-002', detail:'20× Office Chairs', time:'2 days ago' },
{ user:'Arjun Sharma', av:'AS', color:'#6366F1', action:'disposed', target:'HP ProBook 450 (×2)', detail:'Written off fully depreciated', time:'3 days ago' },
{ user:'Kavya Nair', av:'KN', color:'#10B981', action:'completed PM', target:'Cisco Catalyst 9300', detail:'Monthly firmware update', time:'4 days ago' },
{ user:'Anita Singh', av:'AS', color:'#A855F7', action:'transferred', target:'Cisco IP Phone 8841', detail:'From Admin to HR team', time:'5 days ago' }
],
notifications: [
{ id:'n1', type:'warning', text:'<strong>3 AMC contracts</strong> expiring within 30 days', time:'1 hour ago', read:false },
{ id:'n2', type:'danger', text:'<strong>PM overdue:</strong> Daikin AC & Ricoh Copier service pending', time:'3 hours ago', read:false },
{ id:'n3', type:'info', text:'<strong>8 Purchase Requests</strong> awaiting your approval', time:'Today 9 AM', read:false },
{ id:'n4', type:'warning', text:'<strong>Low stock alert:</strong> Toner cartridges below threshold', time:'Yesterday', read:true },
{ id:'n5', type:'success', text:'Physical audit completed for Floor 2 100% assets verified', time:'2 days ago', read:true }
]
};
// ── Helper Functions ──────────────────────────────────────────
function fmt(n, cur='₹') {
if (n >= 1e7) return `${cur}${(n/1e7).toFixed(2)}Cr`;
if (n >= 1e5) return `${cur}${(n/1e5).toFixed(2)}L`;
if (n >= 1e3) return `${cur}${(n/1e3).toFixed(1)}K`;
return `${cur}${n.toLocaleString('en-IN')}`;
}
function fmtDate(d) {
if (!d || d==='N/A') return 'N/A';
return new Date(d).toLocaleDateString('en-IN',{day:'2-digit',month:'short',year:'numeric'});
}
function statusBadge(s) {
const m = {
'Active':'badge-success','Idle':'badge-warning','Under Maintenance':'badge-info',
'Disposed':'badge-neutral','Lost':'badge-danger','Stolen':'badge-danger',
'Pending':'badge-warning','Approved':'badge-success','Rejected':'badge-danger',
'Draft':'badge-neutral','Submitted':'badge-info','PO Raised':'badge-primary',
'GRN Done':'badge-success','Open':'badge-danger','In Progress':'badge-info',
'Resolved':'badge-success','Closed':'badge-neutral','Pending Parts':'badge-purple',
'Critical':'badge-danger','High':'badge-warning','Medium':'badge-info','Low':'badge-neutral',
'Overdue':'badge-danger','Due Today':'badge-warning','Upcoming':'badge-info',
'Expiring':'badge-warning','Inactive':'badge-neutral'
};
return m[s] || 'badge-neutral';
}
function statusDot(s) {
const m = {
'Active':'var(--success)','Idle':'var(--warning)','Under Maintenance':'var(--info)',
'Disposed':'var(--text-muted)','Lost':'var(--danger)','Open':'var(--danger)',
'In Progress':'var(--info)','Resolved':'var(--success)','Closed':'var(--text-muted)'
};
return m[s] || 'var(--text-muted)';
}

91
js/sidebar.js Normal file
View File

@ -0,0 +1,91 @@
// 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.
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">
<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 <span class="nav-badge" style="background:var(--warning)">5</span></a>
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement <span class="nav-badge">${window.AMS?AMS.stats.pendingPRs:8}</span></a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><span class="nav-icon">🔧</span> Maintenance <span class="nav-badge">${window.AMS?AMS.stats.pendingTickets:12}</span></a>
<a href="reports.html" class="nav-item"><span class="nav-icon">📈</span> Reports &amp; Audit</a>
<div class="nav-section-label">Administration</div>
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users &amp; Roles</a>
<a href="settings.html" class="nav-item"><span class="nav-icon"></span> Settings</a>
</nav>
<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 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>
<span style="color:var(--text-muted);font-size:14px"></span>
</div>
</div>
</aside>`;
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>`;
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');
}

368
maintenance.html Normal file
View File

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

369
procurement.html Normal file
View File

@ -0,0 +1,369 @@
<!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>

332
reports.html Normal file
View File

@ -0,0 +1,332 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Reports & Audit | 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"><span class="nav-icon">🏪</span> Inventory</a>
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement</a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><span class="nav-icon">🔧</span> Maintenance</a>
<a href="reports.html" class="nav-item active"><span class="nav-icon">📈</span> Reports &amp; Audit</a>
<div class="nav-section-label">Administration</div>
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users</a>
<a href="settings.html" class="nav-item"><span class="nav-icon">⚙️</span> Settings</a>
</nav>
<div class="sidebar-footer"><div class="user-card"><div class="user-av">AS</div><div style="flex:1;min-width:0"><div class="user-name">Arjun Sharma</div><div class="user-role">Asset Manager</div></div></div></div>
</aside>
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-left"><div class="topbar-title">Reports &amp; Audit</div></div>
<div class="topbar-actions">
<button class="btn btn-secondary btn-sm" onclick="exportAll()">📥 Export All Reports</button>
<a href="index.html" class="icon-btn">🚪</a>
</div>
</header>
<main class="content">
<!-- Report Cards Grid -->
<div class="grid-3 mb-5" id="reportCards"></div>
<!-- Tabs -->
<div class="tab-container">
<div class="tabs" data-group="rpt">
<button class="tab-btn active" data-tab="tab-depreciation" data-group="rpt">💰 Depreciation Schedule</button>
<button class="tab-btn" data-tab="tab-utilization" data-group="rpt">📊 Utilization</button>
<button class="tab-btn" data-tab="tab-financial" data-group="rpt">🏦 Financial Summary</button>
<button class="tab-btn" data-tab="tab-compliance" data-group="rpt">🔍 Compliance</button>
</div>
<!-- Depreciation Schedule -->
<div class="tab-content active" id="tab-depreciation" data-group="rpt">
<div class="card mb-4">
<div class="card-header">
<span class="card-title">💰 Depreciation Schedule — FY 2025-26</span>
<div class="flex gap-2">
<select class="filter-sel"><option>All Methods</option><option>SLM</option><option>WDV</option></select>
<button class="btn btn-primary btn-sm" onclick="genDepReport()">Generate PDF</button>
<button class="btn btn-secondary btn-sm" onclick="exportDep()">📥 CSV</button>
</div>
</div>
<div class="card-body">
<div class="grid-4 mb-4">
<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">Gross Block</div><div class="stat-value" style="color:var(--primary)">₹4.29Cr</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">Acc. Depreciation</div><div class="stat-value" style="color:var(--danger)">₹1.28Cr</div></div>
<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">Net Block</div><div class="stat-value" style="color:var(--success)">₹3.01Cr</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">FY Dep. Charge</div><div class="stat-value" style="color:var(--warning)">₹52.4L</div></div>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>Asset ID</th><th>Asset Name</th><th>Category</th><th>Gross Cost</th><th>Method</th><th>Rate</th><th>Acc. Dep.</th><th>Net Value</th><th>Dep. (FY)</th><th>Useful Life Left</th></tr></thead>
<tbody id="depTbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Utilization -->
<div class="tab-content" id="tab-utilization" data-group="rpt">
<div class="grid-2 mb-4">
<div class="card">
<div class="card-header"><span class="card-title">📊 Asset Utilization by Department</span></div>
<div class="card-body"><canvas id="utilDeptChart" height="240"></canvas></div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">📈 Utilization Rate by Category</span></div>
<div class="card-body"><canvas id="utilCatChart" height="240"></canvas></div>
</div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">😴 Idle Assets Report — 30+ Days</span>
<button class="btn btn-secondary btn-sm" onclick="showToast('Export','Idle assets list exported','info')">📥 Export</button>
</div>
<div class="card-body">
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>Asset</th><th>Category</th><th>Dept</th><th>Last Assigned</th><th>Idle Since</th><th>Net Value</th><th>Recommendation</th></tr></thead>
<tbody id="idleTbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Financial -->
<div class="tab-content" id="tab-financial" data-group="rpt">
<div class="grid-2 mb-4">
<div class="card">
<div class="card-header"><span class="card-title">💰 Capex vs Opex (Last 12 Months)</span></div>
<div class="card-body"><canvas id="capexChart" height="240"></canvas></div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">📊 Asset Value by Department</span></div>
<div class="card-body"><canvas id="deptValueChart" height="240"></canvas></div>
</div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">💳 Budget vs Actual Spend</span></div>
<div class="card-body">
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>Department</th><th>FY Budget</th><th>Spent YTD</th><th>Balance</th><th>Utilization %</th></tr></thead>
<tbody id="budgetTbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Compliance -->
<div class="tab-content" id="tab-compliance" data-group="rpt">
<div class="grid-2 mb-4">
<div class="card">
<div class="card-header"><span class="card-title">✅ Audit Checklist — Pre-Launch</span></div>
<div class="card-body" id="auditChecklist"></div>
</div>
<div class="card">
<div class="card-header"><span class="card-title">📊 Compliance Score</span></div>
<div class="card-body" style="text-align:center">
<div style="position:relative;display:inline-block;margin:20px auto">
<canvas id="complianceGauge" width="200" height="200"></canvas>
<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-40%);text-align:center">
<div style="font-size:34px;font-weight:800;color:var(--success)">98.4%</div>
<div style="font-size:12px;color:var(--text-muted)">Compliance Score</div>
</div>
</div>
<div class="info-grid" style="margin-top:12px">
<div class="info-item"><div class="info-label">Verified Assets</div><div class="info-value" style="color:var(--success)">1,228 / 1,247</div></div>
<div class="info-item"><div class="info-label">Unverified</div><div class="info-value" style="color:var(--danger)">19</div></div>
<div class="info-item"><div class="info-label">Warranty Coverage</div><div class="info-value">84.6%</div></div>
<div class="info-item"><div class="info-label">Insured Assets</div><div class="info-value">62.3%</div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
<script>
const reportDefs = [
{ icon:'💰', title:'Depreciation Schedule', desc:'SLM/WDV depreciation for all assets. Year-wise breakdown with opening and closing values.', color:'var(--primary)', bg:'var(--primary-glow)', tab:'tab-depreciation' },
{ icon:'📊', title:'Asset Utilization Report', desc:'Assigned vs idle assets by dept. & category. Highlights under-utilized and idle assets.', color:'var(--cyan)', bg:'var(--cyan-glow)', tab:'tab-utilization' },
{ icon:'📦', title:'Physical Audit Report', desc:'Comparison of system records vs physical count. Discrepancy list with status.', color:'var(--warning)', bg:'var(--warning-bg)', tab:'tab-compliance' },
{ icon:'💳', title:'Capital Expenditure Report', desc:'Asset acquisitions by period, vendor, category. Budget vs actual spend analysis.', color:'var(--success)', bg:'var(--success-bg)', tab:'tab-financial' },
{ icon:'🗑️', title:'Disposal & Write-off Report',desc:'All disposed assets with reason, approval chain, write-off amount, and accounting entries.', color:'var(--danger)', bg:'var(--danger-bg)', tab:'tab-financial' },
{ icon:'🔧', title:'Maintenance Cost Report', desc:'Ticket-wise maintenance spend, vendor-wise costs, preventive vs reactive breakdown.', color:'var(--purple)', bg:'var(--purple-bg)', tab:'tab-utilization' },
{ icon:'📅', title:'Warranty Expiry Forecast', desc:'Assets whose warranty expires in next 30/60/90 days. Action alerts.', color:'var(--warning)', bg:'var(--warning-bg)', tab:'tab-compliance' },
{ icon:'🏦', title:'Asset Register (Trial Balance)',desc:'Complete asset register as required by Companies Act / Income Tax for audit purposes.', color:'var(--info)', bg:'var(--info-bg)', tab:'tab-depreciation' },
{ icon:'🔄', title:'Asset Movement Report', desc:'All transfers, assignments, and returns in selected period.', color:'var(--cyan)', bg:'var(--cyan-glow)', tab:'tab-utilization' }
];
document.addEventListener('DOMContentLoaded', () => {
renderReportCards();
renderDepreciation();
renderUtilization();
renderFinancial();
renderCompliance();
initCharts();
initTabs();
});
function renderReportCards() {
document.getElementById('reportCards').innerHTML = reportDefs.map(r=>`
<div class="report-card" onclick="jumpToTab('${r.tab}')">
<div class="report-icon" style="background:${r.bg};color:${r.color}">${r.icon}</div>
<div class="report-title">${r.title}</div>
<div class="report-desc">${r.desc}</div>
<div class="flex gap-2 mt-3">
<button class="btn btn-ghost btn-sm" style="font-size:11px;color:${r.color}" onclick="event.stopPropagation();genReport('${r.title}')">📄 Generate</button>
<button class="btn btn-ghost btn-sm" style="font-size:11px" onclick="event.stopPropagation();exportReport('${r.title}')">📥 Export CSV</button>
</div>
</div>`).join('');
}
function jumpToTab(tabId) {
const btn = document.querySelector(`[data-tab="${tabId}"]`);
if(btn) btn.click();
window.scrollTo(0,400);
}
function renderDepreciation() {
document.getElementById('depTbody').innerHTML = AMS.assets.slice(0,15).map(a => {
const years = Math.floor((new Date()-new Date(a.purchase))/(365.25*24*3600*1000));
const accDep = a.cost - a.value;
const fyDep = a.depM==='SLM' ? Math.round(a.cost*a.depR/100) : Math.round(a.value*a.depR/100);
const lifeLeft = Math.max(0, Math.ceil((a.value - a.cost*0.05) / Math.max(1,fyDep)));
return `<tr>
<td><code style="font-size:11px;color:var(--primary-light)">${a.id}</code></td>
<td><div style="font-weight:600;color:var(--text-primary)">${a.name}</div></td>
<td><span class="badge badge-neutral" style="font-size:9.5px">${a.cat}</span></td>
<td>${fmt(a.cost)}</td>
<td><span class="badge ${a.depM==='SLM'?'badge-info':'badge-purple'}">${a.depM}</span></td>
<td>${a.depR}%</td>
<td style="color:var(--danger)">${fmt(accDep)}</td>
<td style="font-weight:700;color:var(--success)">${fmt(a.value)}</td>
<td style="color:var(--warning)">${fmt(fyDep)}</td>
<td>${lifeLeft} yr${lifeLeft!==1?'s':''}</td>
</tr>`;
}).join('');
}
function renderUtilization() {
const idles = AMS.assets.filter(a=>a.status==='Idle');
document.getElementById('idleTbody').innerHTML = idles.map(a=>`
<tr>
<td><div style="font-weight:600;color:var(--text-primary)">${a.name}</div></td>
<td><span class="badge badge-neutral">${a.cat}</span></td>
<td>${a.dept}</td>
<td>${fmtDate(a.purchase)}</td>
<td style="color:var(--warning)">90+ days</td>
<td style="font-weight:700">${fmt(a.value)}</td>
<td><span class="badge badge-info">Reallocate</span></td>
</tr>`).join('');
}
function renderFinancial() {
const budgets = [
{ dept:'IT', budget:1500000, spent:1124000 },
{ dept:'Finance', budget:400000, spent:287000 },
{ dept:'HR', budget:350000, spent:198000 },
{ dept:'Operations',budget:800000, spent:643000 },
{ dept:'Marketing', budget:600000, spent:524000 },
{ dept:'Admin', budget:500000, spent:312000 }
];
document.getElementById('budgetTbody').innerHTML = budgets.map(b=>{
const pct = Math.round(b.spent/b.budget*100);
return `<tr>
<td style="font-weight:600;color:var(--text-primary)">${b.dept}</td>
<td>${fmt(b.budget)}</td>
<td style="color:var(--warning)">${fmt(b.spent)}</td>
<td style="color:${b.budget-b.spent>0?'var(--success)':'var(--danger)'}">${fmt(b.budget-b.spent)}</td>
<td>
<div class="flex items-center gap-2">
<div class="progress-wrap flex-1" style="height:6px"><div class="progress-fill" style="width:${pct}%;background:${pct>90?'linear-gradient(90deg,var(--warning),var(--danger))':'linear-gradient(90deg,var(--primary),var(--cyan))'}"></div></div>
<span style="font-size:12px;font-weight:700;min-width:32px">${pct}%</span>
</div>
</td>
</tr>`;
}).join('');
}
function renderCompliance() {
const checks = [
{ label:'All assets have unique Asset IDs', done:true },
{ label:'All IT assets have serial numbers', done:true },
{ label:'QR codes generated for all assets', done:true },
{ label:'Physical audit completed within last 6 months', done:false },
{ label:'Depreciation schedule up to date', done:true },
{ label:'All vehicles have valid insurance', done:false },
{ label:'Warranty tracking active for all assets', done:true },
{ label:'Disposal records maintained with approvals',done:true },
{ label:'RBAC roles assigned to all users', done:true },
{ label:'Backup of asset data taken this month', done:true }
];
document.getElementById('auditChecklist').innerHTML = checks.map(c=>`
<div class="flex items-center gap-3 mb-3">
<div style="width:22px;height:22px;border-radius:50%;background:${c.done?'var(--success-bg)':'var(--danger-bg)'};color:${c.done?'var(--success)':'var(--danger)'};display:flex;align-items:center;justify-content:center;font-size:12px;flex-shrink:0">${c.done?'✓':'✕'}</div>
<span style="font-size:13px;color:${c.done?'var(--text-secondary)':'var(--text-primary)'};">${c.label}</span>
${!c.done?`<span class="badge badge-danger" style="margin-left:auto;font-size:9px">Action Needed</span>`:''}
</div>`).join('');
}
function initCharts() {
const gridColor='rgba(255,255,255,.04)', tick={color:'#4B5563',font:{family:'Inter',size:11}};
// Dept Utilization
new Chart(document.getElementById('utilDeptChart'),{type:'bar',data:{labels:['IT','Finance','HR','Operations','Marketing','Admin'],
datasets:[{label:'Assigned',data:[340,130,98,265,172,95],backgroundColor:'rgba(99,102,241,.7)',borderRadius:4},{label:'Idle',data:[44,26,14,33,15,15],backgroundColor:'rgba(245,158,11,.5)',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:gridColor},ticks:tick},y:{stacked:true,grid:{color:gridColor},ticks:tick}}}});
// Category Utilization
new Chart(document.getElementById('utilCatChart'),{type:'bar',data:{labels:AMS.categories.slice(0,7).map(c=>c.name),
datasets:[{label:'Utilization %',data:[95,87,100,78,92,85,72],backgroundColor:AMS.categories.slice(0,7).map((_,i)=>`hsl(${230+i*15},70%,60%)`),borderRadius:5}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{grid:{color:gridColor},ticks:{...tick,maxRotation:45}},y:{grid:{color:gridColor},ticks:{...tick,callback:v=>v+'%'},max:100}}}});
// Capex Chart
const months=['Jun','Jul','Aug','Sep','Oct','Nov','Dec','Jan','Feb','Mar','Apr','May'];
new Chart(document.getElementById('capexChart'),{type:'line',data:{labels:months,datasets:[
{label:'Capex',data:[280000,150000,420000,180000,310000,95000,580000,220000,165000,340000,495000,285000],borderColor:'#6366F1',backgroundColor:'rgba(99,102,241,.1)',fill:true,tension:.4,pointRadius:3,borderWidth:2},
{label:'Opex (Maintenance)',data:[45000,62000,38000,71000,55000,88000,42000,95000,61000,73000,82000,67000],borderColor:'#10B981',backgroundColor:'rgba(16,185,129,.08)',fill:true,tension:.4,pointRadius:3,borderWidth:2}
]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:'#94A3B8',font:{family:'Inter',size:11},usePointStyle:true}}},scales:{x:{grid:{color:gridColor},ticks:tick},y:{grid:{color:gridColor},ticks:{...tick,callback:v=>'₹'+(v/1000).toFixed(0)+'K'}}}}});
// Dept Value
new Chart(document.getElementById('deptValueChart'),{type:'doughnut',data:{labels:['IT','Finance','HR','Operations','Marketing','Admin'],
datasets:[{data:[15800000,6200000,4100000,8900000,5400000,2700000],backgroundColor:['#6366F1','#06B6D4','#A855F7','#10B981','#F59E0B','#3B82F6'],borderColor:'rgba(19,25,38,0)',hoverOffset:5}]},
options:{responsive:true,maintainAspectRatio:false,cutout:'60%',plugins:{legend:{position:'right',labels:{color:'#94A3B8',font:{family:'Inter',size:11},usePointStyle:true}}}}});
// Compliance Gauge
const ctx=document.getElementById('complianceGauge').getContext('2d');
new Chart(ctx,{type:'doughnut',data:{datasets:[{data:[98.4,1.6],backgroundColor:['#10B981','rgba(239,68,68,.2)'],borderColor:'rgba(19,25,38,0)',borderWidth:0}]},
options:{responsive:false,cutout:'72%',plugins:{legend:{display:false}}}});
}
function genReport(name) { showToast('Generating PDF',`${name} PDF report being prepared…`,'info'); }
function exportReport(name) { showToast('Export',`${name} exported to CSV`,'success'); }
function genDepReport() { showToast('PDF Generated','Depreciation Schedule PDF ready for download','success'); }
function exportDep() { downloadCSV(AMS.assets.map(a=>({ID:a.id,Name:a.name,Cat:a.cat,Cost:a.cost,Method:a.depM,Rate:a.depR+'%',AccDep:a.cost-a.value,NetValue:a.value})),'depreciation_schedule.csv'); }
function exportAll() { showToast('Bulk Export','All reports being packaged as ZIP…','info'); }
</script>
</body>
</html>

324
settings.html Normal file
View File

@ -0,0 +1,324 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Settings | AMS</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="app-layout">
<aside class="sidebar">
<div class="sidebar-logo"><div class="logo-icon">📦</div><div><div class="logo-title">AMS</div><div class="logo-sub">Asset Management</div></div></div>
<nav class="sidebar-nav">
<div class="nav-section-label">Overview</div>
<a href="dashboard.html" class="nav-item"><span class="nav-icon">📊</span> Dashboard</a>
<div class="nav-section-label">Assets</div>
<a href="assets.html" class="nav-item"><span class="nav-icon">📦</span> All Assets</a>
<a href="asset-create.html" class="nav-item"><span class="nav-icon"></span> Add Asset</a>
<div class="nav-section-label">Supply Chain</div>
<a href="inventory.html" class="nav-item"><span class="nav-icon">🏪</span> Inventory</a>
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement</a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><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>
<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>
</div>
</header>
<main class="content">
<div style="display:grid;grid-template-columns:220px 1fr;gap:20px;align-items:start">
<!-- Settings Sidebar -->
<div class="card p-4">
<div style="font-size:11.5px;font-weight:700;color:var(--text-muted);text-transform:uppercase;letter-spacing:.6px;margin-bottom:12px">Settings</div>
<nav id="settingsNav"></nav>
</div>
<!-- Settings Content -->
<div id="settingsContent"></div>
</div>
</main>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
<script>
const sections = [
{ id:'general', icon:'🏢', label:'Organization' },
{ id:'appearance', icon:'🎨', label:'Appearance' },
{ id:'depreciation',icon:'💰', label:'Depreciation' },
{ id:'notifications',icon:'🔔',label:'Notifications' },
{ id:'email', icon:'📧', label:'Email / SMTP' },
{ id:'backup', icon:'💾', label:'Backup & Export' },
{ id:'security', icon:'🔐', label:'Security & 2FA' },
{ id:'integrations',icon:'🔗', label:'Integrations' }
];
const settingsHTML = {
general: `
<div class="card">
<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 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">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">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>`,
appearance: `
<div class="card">
<div class="card-header"><span class="card-title">🎨 Appearance</span></div>
<div class="card-body">
<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>
</div>
</div>
<div class="form-group mt-4">
<label class="form-label">Accent Color</label>
<div class="flex gap-3 mt-2 flex-wrap">
${['#6366F1','#06B6D4','#10B981','#F59E0B','#EF4444','#A855F7'].map((c,i)=>`
<button onclick="setAccent('${c}')" style="width:36px;height:36px;border-radius:50%;background:${c};border:${i===0?'3px solid #fff':'2px solid transparent'};cursor:pointer;transition:var(--t)" title="${c}"></button>`).join('')}
</div>
</div>
<div class="form-group mt-4">
<label class="form-label">Sidebar Style</label>
<select class="form-select"><option>Compact (Default)</option><option>Wide</option><option>Collapsed</option></select>
</div>
<div class="form-group">
<label class="form-label">Table Density</label>
<select class="form-select"><option>Comfortable (Default)</option><option>Compact</option><option>Spacious</option></select>
</div>
</div>
</div>`,
depreciation: `
<div class="card">
<div class="card-header"><span class="card-title">💰 Depreciation Defaults</span></div>
<div class="card-body">
<div class="alert alert-info mb-4">
<div class="alert-icon"></div>
<div><div class="alert-title">These are default values applied when creating new assets</div><div class="alert-text">Each asset can override these per category settings.</div></div>
</div>
<div class="form-group"><label class="form-label">Default Depreciation Method</label>
<select class="form-select"><option selected>SLM Straight Line Method (as per Companies Act)</option><option>WDV Written Down Value</option></select>
</div>
<hr class="divider">
<div class="section-hdr">Category-wise Default Rates</div>
<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>
<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>
</tr>`).join('')}
</tbody>
</table>
</div>
</div>`,
notifications: `
<div class="card">
<div class="card-header"><span class="card-title">🔔 Notification Preferences</span></div>
<div class="card-body">
<div class="section-hdr">Warranty Alerts</div>
${notifRow('Warranty expiry alert',true,'Send email 90 / 60 / 30 days before expiry')}
${notifRow('Warranty expired',true,'Immediate alert when warranty expires')}
<hr class="divider">
<div class="section-hdr">Maintenance Alerts</div>
${notifRow('PM due reminder',true,'7 days before scheduled PM')}
${notifRow('PM overdue',true,'Daily reminders for overdue PMs')}
${notifRow('Ticket assigned to me',true,'When a ticket is assigned to you')}
${notifRow('Ticket status change',false,'When status changes on your tickets')}
<hr class="divider">
<div class="section-hdr">Procurement Alerts</div>
${notifRow('PR requires my approval',true,'When a PR is sent to you for approval')}
${notifRow('PR approved / rejected',true,'When your PR is actioned')}
${notifRow('PO delivery due',false,'3 days before expected delivery date')}
<hr class="divider">
<div class="section-hdr">System Alerts</div>
${notifRow('Low stock threshold reached',true,'When any category falls below minimum')}
${notifRow('AMC expiry reminder',true,'30 days before AMC contract expires')}
${notifRow('Audit schedule reminder',false,'Quarterly audit schedule reminders')}
</div>
</div>`,
email: `
<div class="card">
<div class="card-header"><span class="card-title">📧 Email / SMTP Configuration</span></div>
<div class="card-body">
<div class="form-row">
<div class="form-group"><label class="form-label">SMTP Host</label><input class="form-input" value="smtp.acmecorp.com"></div>
<div class="form-group"><label class="form-label">SMTP Port</label><input class="form-input" value="587"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Username</label><input class="form-input" value="ams@acmecorp.com"></div>
<div class="form-group"><label class="form-label">Password</label><input type="password" class="form-input" value="•••••••••••••••"></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Sender Name</label><input class="form-input" value="AMS Acme Corporation"></div>
<div class="form-group"><label class="form-label">Encryption</label><select class="form-select"><option selected>STARTTLS</option><option>SSL/TLS</option><option>None</option></select></div>
</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Test Recipient</label><input type="email" class="form-input" placeholder="Send test email to…"></div>
<div class="form-group" style="display:flex;align-items:flex-end"><button class="btn btn-secondary w-full" onclick="testEmail()">📧 Send Test Email</button></div>
</div>
</div>
</div>`,
backup: `
<div class="card">
<div class="card-header"><span class="card-title">💾 Backup & Data Export</span></div>
<div class="card-body">
<div class="section-hdr">Automated Backup</div>
<div class="form-group"><label class="form-label">Backup Frequency</label><select class="form-select"><option selected>Daily (2:00 AM IST)</option><option>Weekly</option><option>Monthly</option><option>Disabled</option></select></div>
<div class="form-group"><label class="form-label">Backup Location</label><select class="form-select"><option selected>AWS S3 ap-south-1</option><option>Google Cloud Storage</option><option>Local Server</option><option>SFTP</option></select></div>
<div class="form-group"><label class="form-label">Retention Period</label><select class="form-select"><option>30 days</option><option selected>90 days</option><option>1 year</option></select></div>
<hr class="divider">
<div class="section-hdr">Manual Export</div>
<div class="flex gap-3 flex-wrap">
<button class="btn btn-secondary" onclick="showToast('Export','Exporting full asset database to JSON…','info')">📤 Export Database (JSON)</button>
<button class="btn btn-secondary" onclick="showToast('Export','Exporting asset register to CSV…','info')">📊 Export Asset Register (CSV)</button>
<button class="btn btn-secondary" onclick="showToast('Export','Generating audit report PDF…','info')">📄 Export Audit Report (PDF)</button>
</div>
<hr class="divider">
<div class="section-hdr">Import</div>
<div class="alert alert-warning mb-3"><div class="alert-icon">⚠️</div><div><div class="alert-title">Bulk import may overwrite existing records</div><div class="alert-text">Always take a backup before bulk importing data.</div></div></div>
<div class="flex gap-3">
<label class="btn btn-secondary" for="importFile">📥 Import Assets (CSV)</label>
<input type="file" id="importFile" accept=".csv" style="display:none" onchange="handleImport()">
<button class="btn btn-ghost btn-sm" onclick="downloadTemplate()">📋 Download Template</button>
</div>
</div>
</div>`,
security: `
<div class="card">
<div class="card-header"><span class="card-title">🔐 Security Settings</span></div>
<div class="card-body">
<div class="section-hdr">Two-Factor Authentication</div>
<div class="flex items-center justify-between mb-4 p-4 bg-elevated rounded-md border">
<div><div style="font-weight:600">Enforce 2FA for all users</div><div style="font-size:12.5px;color:var(--text-muted)">All users will be required to set up 2FA on next login</div></div>
<label class="toggle"><input type="checkbox" checked><span class="toggle-slider"></span></label>
</div>
<div class="flex items-center justify-between mb-4 p-4 bg-elevated rounded-md border">
<div><div style="font-weight:600">2FA for Admin roles only</div><div style="font-size:12.5px;color:var(--text-muted)">Only Asset Managers and Super Admins require 2FA</div></div>
<label class="toggle"><input type="checkbox"><span class="toggle-slider"></span></label>
</div>
<hr class="divider">
<div class="section-hdr">Session Policy</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Session Timeout</label><select class="form-select"><option>30 minutes</option><option selected>1 hour</option><option>4 hours</option><option>8 hours</option><option>No timeout</option></select></div>
<div class="form-group"><label class="form-label">Max Concurrent Sessions</label><select class="form-select"><option>1</option><option selected>3</option><option>5</option><option>Unlimited</option></select></div>
</div>
<div class="form-group"><label class="form-label">IP Whitelist (optional)</label><textarea class="form-textarea" rows="2" placeholder="Enter allowed IP ranges, one per line. e.g. 192.168.1.0/24"></textarea></div>
<hr class="divider">
<div class="section-hdr">Password Policy</div>
<div class="form-row">
<div class="form-group"><label class="form-label">Minimum Length</label><input type="number" class="form-input" value="10" min="8"></div>
<div class="form-group"><label class="form-label">Expire After</label><select class="form-select"><option>Never</option><option selected>90 days</option><option>180 days</option><option>1 year</option></select></div>
</div>
</div>
</div>`,
integrations: `
<div class="card">
<div class="card-header"><span class="card-title">🔗 Integrations</span></div>
<div class="card-body">
${integrationCard('Microsoft Active Directory / Azure AD','🔷','SSO via Microsoft OAuth 2.0. Sync users from AD groups.','Connected','Connected · 10 users synced')}
${integrationCard('Tally ERP / Zoho Books','📊','Auto-sync asset depreciation and financial data to accounting.','Connect','Not Connected')}
${integrationCard('JIRA / ServiceNow','🎫','Create and sync support tickets with your ITSM tool.','Connect','Not Connected')}
${integrationCard('Slack / Microsoft Teams','💬','Send alerts, notifications, and approvals to Slack/Teams channels.','Connect','Not Connected')}
${integrationCard('ERP System (SAP / Oracle)','🏭','Bi-directional sync with enterprise ERP for procurement data.','Connect','Not Connected')}
${integrationCard('Webhooks (Custom)','⚡','Send real-time events to your own systems via HTTP webhooks.','Configure','1 webhook active')}
</div>
</div>`
};
function notifRow(label, def, desc) {
return `<div class="flex items-center gap-3 mb-4" style="padding:12px;background:var(--bg-surface);border-radius:var(--radius-md);border:1px solid var(--border)">
<label class="toggle"><input type="checkbox" ${def?'checked':''}><span class="toggle-slider"></span></label>
<div style="flex:1"><div style="font-weight:600;font-size:13px">${label}</div><div style="font-size:11.5px;color:var(--text-muted)">${desc}</div></div>
</div>`;
}
function integrationCard(name, icon, desc, btnLabel, status) {
const connected = status.toLowerCase().includes('connected') || status.includes('active');
return `<div class="flex items-center gap-4 mb-4 p-4" style="background:var(--bg-surface);border-radius:var(--radius-md);border:1px solid var(--border)">
<div style="width:42px;height:42px;border-radius:10px;background:var(--bg-elevated);display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0">${icon}</div>
<div style="flex:1">
<div style="font-weight:700;font-size:13.5px">${name}</div>
<div style="font-size:12px;color:var(--text-muted);margin-bottom:4px">${desc}</div>
<div style="font-size:11px;color:${connected?'var(--success)':'var(--text-muted)'}">${status}</div>
</div>
<button class="btn ${connected?'btn-ghost':'btn-secondary'} btn-sm" onclick="showToast('Integration','${name} integration dialog opening…','info')">${btnLabel}</button>
</div>`;
}
let activeSect = 'general';
document.addEventListener('DOMContentLoaded', () => {
renderSettingsNav();
loadSection('general');
AMS.categories.forEach(c => {/* pre-load */});
});
function renderSettingsNav() {
document.getElementById('settingsNav').innerHTML = sections.map(s=>`
<div class="settings-nav-item ${s.id===activeSect?'active':''}" id="snav-${s.id}" onclick="loadSection('${s.id}')">
<span style="font-size:15px">${s.icon}</span>
<span>${s.label}</span>
</div>`).join('');
}
function loadSection(id) {
activeSect = id;
document.querySelectorAll('.settings-nav-item').forEach(el=>el.classList.remove('active'));
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>`;
}
function saveSettings() {
document.getElementById('saveSettingsBtn').textContent = '✅ Saved!';
showToast('Settings Saved','All configuration changes have been applied','success');
setTimeout(()=>{ document.getElementById('saveSettingsBtn').textContent = '💾 Save All Settings'; },2500);
}
function testEmail() { showToast('Test Email Sent','Check your inbox for the test email','success'); }
function handleImport() { showToast('Import Started','Validating CSV file…','info'); }
function downloadTemplate() { showToast('Template Downloaded','CSV import template downloaded','success'); }
function setAccent(color) { showToast('Theme Updated',`Accent color changed to ${color}`,'success'); }
</script>
</body>
</html>

309
users.html Normal file
View File

@ -0,0 +1,309 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Users & Roles | AMS</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div class="app-layout">
<aside class="sidebar">
<div class="sidebar-logo"><div class="logo-icon">📦</div><div><div class="logo-title">AMS</div><div class="logo-sub">Asset Management</div></div></div>
<nav class="sidebar-nav">
<div class="nav-section-label">Overview</div>
<a href="dashboard.html" class="nav-item"><span class="nav-icon">📊</span> Dashboard</a>
<div class="nav-section-label">Assets</div>
<a href="assets.html" class="nav-item"><span class="nav-icon">📦</span> All Assets</a>
<a href="asset-create.html" class="nav-item"><span class="nav-icon"></span> Add Asset</a>
<div class="nav-section-label">Supply Chain</div>
<a href="inventory.html" class="nav-item"><span class="nav-icon">🏪</span> Inventory</a>
<a href="procurement.html" class="nav-item"><span class="nav-icon">🛒</span> Procurement</a>
<div class="nav-section-label">Operations</div>
<a href="maintenance.html" class="nav-item"><span class="nav-icon">🔧</span> Maintenance</a>
<a href="reports.html" class="nav-item"><span class="nav-icon">📈</span> Reports</a>
<div class="nav-section-label">Administration</div>
<a href="users.html" class="nav-item active"><span class="nav-icon">👥</span> Users &amp; Roles</a>
<a href="settings.html" class="nav-item"><span class="nav-icon">⚙️</span> Settings</a>
</nav>
<div class="sidebar-footer"><div class="user-card"><div class="user-av">AS</div><div style="flex:1;min-width:0"><div class="user-name">Arjun Sharma</div><div class="user-role">Asset Manager</div></div></div></div>
</aside>
<div class="main-wrapper">
<header class="topbar">
<div class="topbar-left"><div class="topbar-title">Users &amp; Access Control</div></div>
<div class="topbar-actions">
<button class="btn btn-primary btn-sm" onclick="openModal('inviteModal')">+ Invite User</button>
<a href="index.html" class="icon-btn">🚪</a>
</div>
</header>
<main class="content">
<!-- Stats -->
<div class="grid-4 mb-4">
<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">Total Users</div><div class="stat-value" style="color:var(--primary)">10</div></div>
<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">Active</div><div class="stat-value" style="color:var(--success)">9</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">Inactive / Suspended</div><div class="stat-value" style="color:var(--warning)">1</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">Roles Defined</div><div class="stat-value" style="color:var(--info)">6</div></div>
</div>
<!-- Tabs -->
<div class="tab-container">
<div class="tabs" data-group="usr">
<button class="tab-btn active" data-tab="tab-users" data-group="usr">👥 User List</button>
<button class="tab-btn" data-tab="tab-roles" data-group="usr">🔐 Roles & Permissions</button>
<button class="tab-btn" data-tab="tab-sessions" data-group="usr">🖥️ Active Sessions</button>
</div>
<!-- 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>
<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>
</div>
<div class="table-wrapper">
<table class="data-table" id="userTable">
<thead><tr><th>User</th><th>Email</th><th>Role</th><th>Department</th><th>Status</th><th>Last Login</th><th>2FA</th><th>Actions</th></tr></thead>
<tbody id="userTbody"></tbody>
</table>
</div>
</div>
<!-- Roles & Permissions -->
<div class="tab-content" id="tab-roles" data-group="usr">
<div class="grid-2 mb-4" id="roleCards"></div>
<div class="card">
<div class="card-header"><span class="card-title">🔐 Permission Matrix</span><span style="font-size:12px;color:var(--text-muted)">✓ = Full Access · E = Edit Only · V = View Only · — = No Access</span></div>
<div class="card-body" style="overflow-x:auto">
<table class="perm-matrix">
<thead>
<tr>
<th>Module / Permission</th>
<th>Super Admin</th>
<th>Asset Manager</th>
<th>Dept Head</th>
<th>Finance Head</th>
<th>IT Head</th>
<th>Employee</th>
</tr>
</thead>
<tbody id="permMatrix"></tbody>
</table>
</div>
</div>
</div>
<!-- Active Sessions -->
<div class="tab-content" id="tab-sessions" data-group="usr">
<div class="alert alert-info mb-4">
<div class="alert-icon"></div>
<div><div class="alert-title">7 active sessions currently</div><div class="alert-text">You can revoke suspicious sessions from below. All sessions auto-expire after 7 days of inactivity.</div></div>
</div>
<div class="table-wrapper">
<table class="data-table">
<thead><tr><th>User</th><th>Device / Browser</th><th>IP Address</th><th>Location</th><th>Login Time</th><th>Last Active</th><th>Action</th></tr></thead>
<tbody id="sessionTbody"></tbody>
</table>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Invite Modal -->
<div class="modal-overlay" id="inviteModal">
<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-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>
<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">
<div class="alert-icon">📧</div>
<div><div class="alert-title">Invite process</div><div class="alert-text">An invite email with a one-time setup link (expires in 48 hours) will be sent to the user.</div></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('inviteModal')">Cancel</button>
<button class="btn btn-primary" onclick="sendInvite()">Send Invite</button>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal-overlay" id="editUserModal">
<div class="modal">
<div class="modal-header"><span class="modal-title">Edit User</span><button class="modal-close"></button></div>
<div class="modal-body" id="editUserBody"></div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('editUserModal')">Cancel</button>
<button class="btn btn-primary" onclick="saveUser()">Save Changes</button>
</div>
</div>
</div>
<div class="toast-container" id="toastContainer"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
<script>
const roles = [
{ name:'Super Admin', desc:'Full system access. Can manage all settings, users, and data.', color:'var(--danger)', perms:'All Modules', users:1 },
{ name:'Asset Manager', desc:'Manages all assets, can approve PRs, view all reports.', color:'var(--primary)', perms:'Assets, Reports, Procurement', users:2 },
{ name:'Department Head', desc:'Can view department assets, approve team PRs, raise tickets.', color:'var(--cyan)', perms:'Dept Assets, PR Approval', users:5 },
{ name:'Finance Head', desc:'Approves PRs (budget stage), views depreciation & financial reports.',color:'var(--warning)', perms:'Financial Reports, PR Approval', users:1 },
{ name:'IT Head', desc:'Manages IT assets, tickets, AMC, and user provisioning.', color:'var(--success)', perms:'IT Assets, Users (IT Dept)', users:1 },
{ name:'Employee', desc:'View own assigned assets, raise requests, acknowledge assignments.', color:'var(--text-muted)',perms:'View Own Assets, Raise Tickets', users:20 }
];
const permRows = [
{ module:'View All Assets', sa:'✓',am:'✓',dh:'V',fi:'V',it:'V',em:'—' },
{ module:'Create / Edit Assets', sa:'✓',am:'✓',dh:'—',fi:'—',it:'E',em:'—' },
{ module:'Delete / Dispose', sa:'✓',am:'✓',dh:'—',fi:'—',it:'—',em:'—' },
{ module:'Assign Assets', sa:'✓',am:'✓',dh:'E',fi:'—',it:'E',em:'—' },
{ module:'Transfer Assets', sa:'✓',am:'✓',dh:'E',fi:'—',it:'E',em:'—' },
{ module:'View Financial Reports',sa:'✓',am:'✓',dh:'—',fi:'✓',it:'—',em:'—' },
{ module:'Approve Purchase Req.', sa:'✓',am:'✓',dh:'E',fi:'E',it:'—',em:'—' },
{ module:'Raise Tickets', sa:'✓',am:'✓',dh:'✓',fi:'V',it:'✓',em:'✓' },
{ module:'Manage Users', sa:'✓',am:'—',dh:'—',fi:'—',it:'V',em:'—' },
{ module:'System Settings', sa:'✓',am:'—',dh:'—',fi:'—',it:'—',em:'—' },
{ module:'View Own Assets', sa:'✓',am:'✓',dh:'✓',fi:'✓',it:'✓',em:'✓' }
];
document.addEventListener('DOMContentLoaded', () => {
renderUsers();
renderRoles();
renderPermMatrix();
renderSessions();
initTabs();
document.getElementById('userSearch').addEventListener('input', filterUsers);
});
function renderUsers(data) {
const users = data || AMS.users;
document.getElementById('userTbody').innerHTML = users.map(u=>`
<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>
</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><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>
<td>
<div class="flex gap-1">
<button class="btn btn-ghost btn-sm" onclick="editUser('${u.id}')">✏️</button>
<button class="btn btn-ghost btn-sm" onclick="revokeUser('${u.id}')" title="Revoke sessions">🔒</button>
<button class="btn btn-ghost btn-sm danger" onclick="toggleStatus('${u.id}')" style="color:var(--danger)">${u.status==='Active'?'🚫':'✅'}</button>
</div>
</td>
</tr>`).join('');
}
function filterUsers() {
const q = document.getElementById('userSearch').value.toLowerCase();
const filtered = AMS.users.filter(u => !q || u.name.toLowerCase().includes(q) || u.email.toLowerCase().includes(q) || u.role.toLowerCase().includes(q));
renderUsers(filtered);
}
function renderRoles() {
document.getElementById('roleCards').innerHTML = roles.map(r=>`
<div class="card">
<div class="card-body">
<div class="flex items-center gap-3 mb-3">
<div style="width:36px;height:36px;border-radius:var(--radius-md);background:${r.color}18;color:${r.color};display:flex;align-items:center;justify-content:center;font-size:16px">🔐</div>
<div><div style="font-weight:700;font-size:13.5px">${r.name}</div><div style="font-size:11px;color:var(--text-muted)">${r.users} user${r.users!==1?'s':''}</div></div>
</div>
<div style="font-size:12.5px;color:var(--text-secondary);margin-bottom:10px">${r.desc}</div>
<div style="font-size:11px;color:var(--text-muted)">Access: <span style="color:${r.color};font-weight:600">${r.perms}</span></div>
</div>
</div>`).join('');
}
function renderPermMatrix() {
const cols = ['sa','am','dh','fi','it','em'];
const colored = (v) => {
if(v==='✓') return `<span style="color:var(--success);font-size:15px"></span>`;
if(v==='E') return `<span class="badge badge-info" style="font-size:9px">Edit</span>`;
if(v==='V') return `<span class="badge badge-neutral" style="font-size:9px">View</span>`;
return `<span style="color:var(--text-muted)"></span>`;
};
document.getElementById('permMatrix').innerHTML = permRows.map(r=>`
<tr>
<td>${r.module}</td>
${cols.map(c=>`<td>${colored(r[c])}</td>`).join('')}
</tr>`).join('');
}
function renderSessions() {
const sessions = [
{ user:'Arjun Sharma', device:'Chrome 124 / macOS', ip:'192.168.1.42', loc:'Bengaluru, IN', login:'Today 09:12', last:'2 min ago', current:true },
{ user:'Priya Kumar', device:'Chrome 124 / Windows',ip:'192.168.1.55', loc:'Bengaluru, IN', login:'Today 08:45', last:'15 min ago', current:false },
{ user:'Anita Singh', device:'Safari / iPhone 15', ip:'10.0.0.23', loc:'Bengaluru, IN', login:'Today 10:02', last:'5 min ago', current:false },
{ user:'Kavya Nair', device:'Firefox 125 / Ubuntu',ip:'192.168.1.78', loc:'Bengaluru, IN', login:'Today 09:55', last:'1 min ago', current:false },
{ user:'Deepak Joshi', device:'Edge 124 / Windows', ip:'192.168.1.90', loc:'Bengaluru, IN', login:'Yesterday', last:'18 hrs ago', current:false }
];
document.getElementById('sessionTbody').innerHTML = sessions.map(s=>`
<tr>
<td style="font-weight:600">${s.user} ${s.current?'<span class="badge badge-success" style="font-size:9px">You</span>':''}</td>
<td>${s.device}</td>
<td><code style="font-size:11px">${s.ip}</code></td>
<td>${s.loc}</td>
<td style="font-size:12px;color:var(--text-muted)">${s.login}</td>
<td style="font-size:12px;color:var(--text-muted)">${s.last}</td>
<td><button class="btn btn-${s.current?'ghost':'danger'} btn-sm" ${s.current?'disabled':''} onclick="revokeSession('${s.user}')">${s.current?'Current':'🔒 Revoke'}</button></td>
</tr>`).join('');
}
function editUser(id) {
const u = AMS.users.find(x=>x.id===id);
if(!u) return;
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>`;
openModal('editUserModal');
}
function saveUser() { 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.',()=>{
showToast('Sessions Revoked','All sessions for user revoked','warning');
});
}
function toggleStatus(id) {
const u = AMS.users.find(x=>x.id===id);
if(!u) return;
u.status = u.status==='Active' ? 'Inactive' : 'Active';
renderUsers();
showToast('Status Updated',`${u.name} is now ${u.status}`, u.status==='Active'?'success':'warning');
}
function revokeSession(user) { showToast('Session Revoked',`${user}'s session has been terminated`,'warning'); }
function sendInvite() {
closeModal('inviteModal');
showToast('Invite Sent','Invitation email sent with 48-hour setup link','success');
}
function confirmAction(title, msg, cb) {
if(confirm(msg) && cb) cb();
}
</script>
</body>
</html>