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:
commit
2dfe150a5c
22
.gitignore
vendored
Normal file
22
.gitignore
vendored
Normal 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
BIN
AMS_Product_Plan_v1.0.docx
Normal file
Binary file not shown.
400
asset-create.html
Normal file
400
asset-create.html
Normal 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
494
asset-detail.html
Normal 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
415
assets.html
Normal 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 & Audit</a>
|
||||
<div class="nav-section-label">Administration</div>
|
||||
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users & Roles</a>
|
||||
<a href="settings.html" class="nav-item"><span class="nav-icon">⚙️</span> Settings</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer"><div class="user-card"><div class="user-av">AS</div><div style="flex:1;min-width:0"><div class="user-name">Arjun Sharma</div><div class="user-role">Asset Manager</div></div></div></div>
|
||||
</aside>
|
||||
|
||||
<!-- 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
475
css/styles.css
Normal 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
323
dashboard.html
Normal 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 & Audit</a>
|
||||
<div class="nav-section-label">Administration</div>
|
||||
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users & Roles</a>
|
||||
<a href="settings.html" class="nav-item"><span class="nav-icon">⚙️</span> Settings</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-card">
|
||||
<div class="user-av">AS</div>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="user-name">Arjun Sharma</div>
|
||||
<div class="user-role">Asset Manager</div>
|
||||
</div>
|
||||
<span style="color:var(--text-muted)">⋮</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ════ 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
138
index.html
Normal 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 · <a href="#">Privacy Policy</a> · <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
383
inventory.html
Normal 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
275
js/app.js
Normal 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
197
js/data.js
Normal 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
91
js/sidebar.js
Normal 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 & Audit</a>
|
||||
|
||||
<div class="nav-section-label">Administration</div>
|
||||
<a href="users.html" class="nav-item"><span class="nav-icon">👥</span> Users & Roles</a>
|
||||
<a href="settings.html" class="nav-item"><span class="nav-icon">⚙️</span> Settings</a>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-card" 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
368
maintenance.html
Normal 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 & 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
369
procurement.html
Normal 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
332
reports.html
Normal 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 & 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 & 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
324
settings.html
Normal 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
309
users.html
Normal 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 & 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 & 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>
|
||||
Loading…
x
Reference in New Issue
Block a user