mirror of
https://github.com/DumbWareio/DumbAssets.git
synced 2026-01-09 06:10:52 +08:00
* Update README.md Added maintenance feature Added tagging feature Added dependencies * Include Global filters for Event List Enhanced updateEventsDisplay() method - Now respects both local events filters (All, Warranty, Maintenance) AND global dashboard filters (Components, Warranties, Expired, Within 30 days, Within 60 days, Active) Updated Dashboard Card Click Handlers - Now call updateEventsDisplay() when dashboard filters are applied Enhanced initializeEventsSection() - Ensures events are properly filtered from the start Added refreshEventsDisplay() method - Public method for external refreshing of events * Updated/Fixed "Active" filtering logic The "Active" filter had an incorrect third condition that was including assets with ANY component warranties, regardless of whether those warranties were actually active or expired. * Fixed EventList updating The issue was that sectionVisibility was a local variable in the renderDashboard method, so it wasn't accessible in the click handler scope when the dashboard cards were clicked later. * add logging for event list debug * Fix Event List updating The Problem The Events list wasn't updating when global dashboard filters were clicked because of a variable scope issue: Two separate dashboardFilter variables existed: One in main script.js (returned by getDashboardFilter()) One in listRenderer.js (updated by updateDashboardFilter()) The dashboard manager was reading from one variable but updating another: getDashboardFilter() returned the main script's dashboardFilter (always stayed "all") updateDashboardFilter() updated the list renderer's dashboardFilter The Fix I created a local updateDashboardFilter function in the main script that: Updates the local dashboardFilter variable (the one getDashboardFilter() returns) Calls the list renderer's updateDashboardFilter to keep both in sync * Implement Export Function Add export CSV functionality into Settings > System modal * Fixed export button Problem: The middleware/demo.js file was using ES6 export syntax, but the server was importing it using CommonJS require() * export UI update * Add simple csv export * Removed placeholder logos with correct Removed placeholder logos and placed real logo into public > assets > images * remove white background from logo svg * enlarge svg logo * Include asset name with Warranty notifications Add asset bane to warranty notifications so its formatted as: ⏰ Warranty Expiring in 7 days Asset: Dell OptiPlex 7010 Model #: OptiPlex-7010 Warranty Expires: 2024-01-15 🔗 View Asset: http://localhost:3000?ass=12345
438 lines
16 KiB
JavaScript
438 lines
16 KiB
JavaScript
/**
|
|
* Asset List Renderer Service
|
|
* Handles rendering of the asset list sidebar with search and filter functionality
|
|
*/
|
|
|
|
// These functions from other modules will be injected
|
|
let updateSelectedIds;
|
|
let renderAssetDetails;
|
|
let handleSidebarNav;
|
|
|
|
// Global state references - will be passed from main script
|
|
let assets = [];
|
|
let subAssets = [];
|
|
let selectedAssetId = null;
|
|
let dashboardFilter = null;
|
|
let currentSort = { field: null, direction: 'asc' };
|
|
let searchInput;
|
|
|
|
// DOM element references
|
|
let assetList;
|
|
|
|
/**
|
|
* Initialize the list renderer with required dependencies
|
|
*
|
|
* @param {Object} config Configuration object with dependencies
|
|
*/
|
|
function initListRenderer(config) {
|
|
// Store references to other module functions
|
|
updateSelectedIds = config.updateSelectedIds;
|
|
renderAssetDetails = config.renderAssetDetails;
|
|
handleSidebarNav = config.handleSidebarNav;
|
|
|
|
// Store references to global state
|
|
assets = config.assets;
|
|
subAssets = config.subAssets;
|
|
selectedAssetId = config.selectedAssetId;
|
|
dashboardFilter = config.dashboardFilter;
|
|
currentSort = config.currentSort;
|
|
searchInput = config.searchInput;
|
|
|
|
// Store references to DOM elements
|
|
assetList = config.assetList;
|
|
}
|
|
|
|
/**
|
|
* Update the global state references
|
|
*
|
|
* @param {Array} newAssets Updated assets array
|
|
* @param {Array} newSubAssets Updated sub-assets array
|
|
* @param {String} newSelectedAssetId Selected asset ID
|
|
*/
|
|
function updateListState(newAssets, newSubAssets, newSelectedAssetId) {
|
|
assets = newAssets;
|
|
subAssets = newSubAssets;
|
|
selectedAssetId = newSelectedAssetId;
|
|
}
|
|
|
|
/**
|
|
* Update dashboard filter
|
|
*
|
|
* @param {String} newFilter New dashboard filter value
|
|
*/
|
|
function updateDashboardFilter(newFilter) {
|
|
dashboardFilter = newFilter;
|
|
}
|
|
|
|
/**
|
|
* Update sort settings
|
|
*
|
|
* @param {Object} newSort New sort settings
|
|
*/
|
|
function updateSort(newSort) {
|
|
currentSort = newSort;
|
|
}
|
|
|
|
/**
|
|
* Get the appropriate warranty dot type based on expiration date
|
|
*
|
|
* @param {Object} asset Asset to check for warranty
|
|
* @returns {String|null} Dot type ('red', 'yellow', 'expired', or null)
|
|
*/
|
|
function getWarrantyDotType(asset) {
|
|
// Check both primary and secondary warranties
|
|
const warranties = [
|
|
{ exp: asset?.warranty?.expirationDate, isLifetime: asset?.warranty?.isLifetime },
|
|
{ exp: asset?.secondaryWarranty?.expirationDate, isLifetime: asset?.secondaryWarranty?.isLifetime }
|
|
].filter(w => w.exp || w.isLifetime);
|
|
|
|
if (warranties.length === 0) return null;
|
|
|
|
const now = new Date();
|
|
let hasExpiring = false;
|
|
let hasWarning = false;
|
|
let hasExpired = false;
|
|
|
|
warranties.forEach(warranty => {
|
|
// Skip lifetime warranties
|
|
if (warranty.isLifetime) return;
|
|
|
|
const expDate = new Date(warranty.exp);
|
|
if (isNaN(expDate)) return;
|
|
const diff = (expDate - now) / (1000 * 60 * 60 * 24);
|
|
if (diff < 0) {
|
|
// Warranty has expired
|
|
hasExpired = true;
|
|
} else if (diff >= 0 && diff <= 30) {
|
|
// Expiring within 30 days
|
|
hasExpiring = true;
|
|
} else if (diff > 30 && diff <= 60) {
|
|
// Warning (31-60 days)
|
|
hasWarning = true;
|
|
}
|
|
});
|
|
|
|
if (hasExpired) return 'expired';
|
|
if (hasExpiring) return 'red';
|
|
if (hasWarning) return 'yellow';
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Render the asset list in the sidebar with filtering and searching
|
|
*
|
|
* @param {String} searchQuery Search query to filter assets by
|
|
*/
|
|
function renderAssetList(searchQuery = '') {
|
|
if (!assetList) return;
|
|
assetList.innerHTML = '';
|
|
|
|
if (assets.length === 0) {
|
|
assetList.innerHTML = '<div class="empty-state">No assets found</div>';
|
|
return;
|
|
}
|
|
|
|
let filteredAssets = searchQuery
|
|
? assets.filter(asset =>
|
|
asset.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
asset.modelNumber?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
asset.serialNumber?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
asset.location?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
asset.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
|
|
: assets;
|
|
|
|
// Apply dashboard filter
|
|
if (dashboardFilter) {
|
|
const now = new Date();
|
|
|
|
// Special case for components filter
|
|
if (dashboardFilter === 'components') {
|
|
// Only show assets that have sub-assets associated with them
|
|
filteredAssets = filteredAssets.filter(a =>
|
|
subAssets.some(sa => sa.parentId === a.id)
|
|
);
|
|
}
|
|
else if (dashboardFilter === 'warranties') {
|
|
// Assets with warranties
|
|
filteredAssets = filteredAssets.filter(a => a.warranty && a.warranty.expirationDate);
|
|
} else if (dashboardFilter === 'expired') {
|
|
// Assets with expired warranties
|
|
filteredAssets = filteredAssets.filter(a => {
|
|
const exp = a.warranty?.expirationDate;
|
|
if (!exp) return false;
|
|
return new Date(exp) < now;
|
|
});
|
|
|
|
// Also include assets with sub-assets that have expired warranties
|
|
const assetsWithExpiredComponents = assets.filter(a =>
|
|
!filteredAssets.includes(a) && // Don't duplicate
|
|
subAssets.some(sa => {
|
|
if (sa.parentId !== a.id) return false;
|
|
const exp = sa.warranty?.expirationDate;
|
|
if (!exp) return false;
|
|
return new Date(exp) < now;
|
|
})
|
|
);
|
|
|
|
filteredAssets = [...filteredAssets, ...assetsWithExpiredComponents];
|
|
} else if (dashboardFilter === 'within30') {
|
|
// Assets with warranties expiring within 30 days
|
|
filteredAssets = filteredAssets.filter(a => {
|
|
const exp = a.warranty?.expirationDate;
|
|
if (!exp) return false;
|
|
const diff = (new Date(exp) - now) / (1000 * 60 * 60 * 24);
|
|
return diff >= 0 && diff <= 30;
|
|
});
|
|
|
|
// Also include assets with sub-assets expiring within 30 days
|
|
const assetsWithExpiringComponents = assets.filter(a =>
|
|
!filteredAssets.includes(a) && // Don't duplicate
|
|
subAssets.some(sa => {
|
|
if (sa.parentId !== a.id) return false;
|
|
const exp = sa.warranty?.expirationDate;
|
|
if (!exp) return false;
|
|
const diff = (new Date(exp) - now) / (1000 * 60 * 60 * 24);
|
|
return diff >= 0 && diff <= 30;
|
|
})
|
|
);
|
|
|
|
filteredAssets = [...filteredAssets, ...assetsWithExpiringComponents];
|
|
} else if (dashboardFilter === 'within60') {
|
|
// Assets with warranties expiring between 31-60 days
|
|
filteredAssets = filteredAssets.filter(a => {
|
|
const exp = a.warranty?.expirationDate;
|
|
if (!exp) return false;
|
|
const diff = (new Date(exp) - now) / (1000 * 60 * 60 * 24);
|
|
return diff > 30 && diff <= 60;
|
|
});
|
|
|
|
// Also include assets with sub-assets expiring within 31-60 days
|
|
const assetsWithWarningComponents = assets.filter(a =>
|
|
!filteredAssets.includes(a) && // Don't duplicate
|
|
subAssets.some(sa => {
|
|
if (sa.parentId !== a.id) return false;
|
|
const exp = sa.warranty?.expirationDate;
|
|
if (!exp) return false;
|
|
const diff = (new Date(exp) - now) / (1000 * 60 * 60 * 24);
|
|
return diff > 30 && diff <= 60;
|
|
})
|
|
);
|
|
|
|
filteredAssets = [...filteredAssets, ...assetsWithWarningComponents];
|
|
} else if (dashboardFilter === 'active') {
|
|
// Assets with active warranties (more than 60 days)
|
|
filteredAssets = filteredAssets.filter(a => {
|
|
const exp = a.warranty?.expirationDate;
|
|
const isLifetime = a.warranty?.isLifetime;
|
|
if (!exp && !isLifetime) return false;
|
|
if (isLifetime) return true;
|
|
const diff = (new Date(exp) - now) / (1000 * 60 * 60 * 24);
|
|
return diff > 60;
|
|
});
|
|
|
|
// Also include assets with sub-assets having active warranties
|
|
const assetsWithActiveComponents = assets.filter(a =>
|
|
!filteredAssets.includes(a) && // Don't duplicate
|
|
subAssets.some(sa => {
|
|
if (sa.parentId !== a.id) return false;
|
|
const exp = sa.warranty?.expirationDate;
|
|
const isLifetime = sa.warranty?.isLifetime;
|
|
if (!exp && !isLifetime) return false;
|
|
if (isLifetime) return true;
|
|
const diff = (new Date(exp) - now) / (1000 * 60 * 60 * 24);
|
|
return diff > 60;
|
|
})
|
|
);
|
|
|
|
filteredAssets = [...filteredAssets, ...assetsWithActiveComponents];
|
|
}
|
|
}
|
|
|
|
// Apply sorting if a sort field is selected
|
|
if (currentSort.field) {
|
|
filteredAssets = sortAssets(filteredAssets, currentSort.field, currentSort.direction);
|
|
}
|
|
|
|
filteredAssets.forEach(asset => {
|
|
const assetItem = document.createElement('div');
|
|
assetItem.className = 'asset-item';
|
|
assetItem.dataset.id = asset.id; // Store ID in dataset
|
|
|
|
// Set active class if this is the currently selected asset
|
|
if (selectedAssetId && asset.id === selectedAssetId) {
|
|
assetItem.classList.add('active');
|
|
}
|
|
|
|
// Add warranty dot or expired icon if needed
|
|
const dotType = getWarrantyDotType(asset);
|
|
if (dotType === 'red') {
|
|
const dot = document.createElement('div');
|
|
dot.className = 'warranty-expiring-dot';
|
|
assetItem.appendChild(dot);
|
|
} else if (dotType === 'yellow') {
|
|
const dot = document.createElement('div');
|
|
dot.className = 'warranty-warning-dot';
|
|
assetItem.appendChild(dot);
|
|
} else if (dotType === 'expired') {
|
|
const expiredIcon = document.createElement('div');
|
|
expiredIcon.className = 'warranty-expired-icon';
|
|
expiredIcon.innerHTML = `
|
|
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10" />
|
|
<line x1="15" y1="9" x2="9" y2="15" />
|
|
<line x1="9" y1="9" x2="15" y2="15" />
|
|
</svg>
|
|
`;
|
|
assetItem.appendChild(expiredIcon);
|
|
}
|
|
|
|
// Format asset item with name, model, and tags
|
|
assetItem.innerHTML += `
|
|
<div class="asset-item-name">${asset.name || 'Unnamed Asset'}</div>
|
|
${asset.modelNumber ? `<div class="asset-item-model">${asset.modelNumber}</div>` : ''}
|
|
${asset.tags && asset.tags.length > 0 ? `
|
|
<div class="asset-item-tags">
|
|
${asset.tags.map(tag => `<span class="asset-tag" data-tag="${tag}">${tag}</span>`).join('')}
|
|
</div>
|
|
` : ''}
|
|
`;
|
|
|
|
assetItem.addEventListener('click', (e) => {
|
|
// Don't trigger asset click if clicking on a tag
|
|
if (e.target.classList.contains('asset-tag')) {
|
|
return;
|
|
}
|
|
|
|
// Remove active class from all asset items
|
|
document.querySelectorAll('.asset-item').forEach(item => {
|
|
item.classList.remove('active');
|
|
});
|
|
|
|
// Add active class to clicked item
|
|
assetItem.classList.add('active');
|
|
|
|
// Set selectedAssetId before rendering details
|
|
updateSelectedIds(asset.id, null);
|
|
|
|
renderAssetDetails(asset.id);
|
|
handleSidebarNav();
|
|
});
|
|
|
|
// Add click event listeners to tags
|
|
const tagElements = assetItem.querySelectorAll('.asset-tag');
|
|
tagElements.forEach(tagElement => {
|
|
tagElement.addEventListener('click', (e) => {
|
|
e.stopPropagation(); // Prevent asset click
|
|
const tagName = tagElement.dataset.tag;
|
|
|
|
// Set the search input value to the tag name
|
|
if (searchInput) {
|
|
searchInput.value = tagName;
|
|
|
|
// Show the clear search button
|
|
const clearSearchBtn = document.getElementById('clearSearchBtn');
|
|
if (clearSearchBtn) {
|
|
clearSearchBtn.style.display = 'flex';
|
|
}
|
|
|
|
// Trigger the search by calling renderAssetList with the tag
|
|
renderAssetList(tagName);
|
|
|
|
// Focus the search input
|
|
searchInput.focus();
|
|
}
|
|
});
|
|
|
|
// Add cursor pointer style to make it clear tags are clickable
|
|
tagElement.style.cursor = 'pointer';
|
|
});
|
|
|
|
assetList.appendChild(assetItem);
|
|
});
|
|
|
|
// Return whether any assets were found (for determining if we should render empty state)
|
|
return filteredAssets.length > 0;
|
|
}
|
|
|
|
/**
|
|
* Sort assets based on specified field and direction
|
|
*
|
|
* @param {Array} assets Array of assets to sort
|
|
* @param {String} field Field to sort by
|
|
* @param {String} direction Sort direction ('asc' or 'desc')
|
|
* @returns {Array} Sorted assets array
|
|
*/
|
|
function sortAssets(assets, field, direction) {
|
|
if (!assets || !Array.isArray(assets)) {
|
|
console.warn('sortAssets called with invalid assets array');
|
|
return [];
|
|
}
|
|
|
|
return [...assets].sort((a, b) => {
|
|
let valueA, valueB;
|
|
|
|
// Safely extract values based on field
|
|
if (field === 'name') {
|
|
// Handle name field (as string)
|
|
valueA = (a.name ? a.name.toLowerCase() : '');
|
|
valueB = (b.name ? b.name.toLowerCase() : '');
|
|
|
|
// Compare as strings
|
|
if (direction === 'asc') {
|
|
return valueA.localeCompare(valueB);
|
|
} else {
|
|
return valueB.localeCompare(valueA);
|
|
}
|
|
}
|
|
else if (field === 'warranty') {
|
|
// Handle warranty expiration dates
|
|
const dateA = a.warranty?.expirationDate ? new Date(a.warranty.expirationDate) : null;
|
|
const dateB = b.warranty?.expirationDate ? new Date(b.warranty.expirationDate) : null;
|
|
|
|
// Handle cases with null dates (always put null dates at the end)
|
|
if (!dateA && !dateB) return 0;
|
|
if (!dateA) return direction === 'asc' ? 1 : -1;
|
|
if (!dateB) return direction === 'asc' ? -1 : 1;
|
|
|
|
// Compare valid dates
|
|
return direction === 'asc'
|
|
? dateA.getTime() - dateB.getTime()
|
|
: dateB.getTime() - dateA.getTime();
|
|
}
|
|
else if (field === 'updatedAt') {
|
|
// Handle updatedAt dates
|
|
const dateA = a.updatedAt ? new Date(a.updatedAt) : null;
|
|
const dateB = b.updatedAt ? new Date(b.updatedAt) : null;
|
|
|
|
// Handle cases with null dates (always put null dates at the end)
|
|
if (!dateA && !dateB) return 0;
|
|
if (!dateA) return direction === 'asc' ? 1 : -1;
|
|
if (!dateB) return direction === 'asc' ? -1 : 1;
|
|
|
|
// Compare valid dates
|
|
return direction === 'asc'
|
|
? dateA.getTime() - dateB.getTime()
|
|
: dateB.getTime() - dateA.getTime();
|
|
}
|
|
else {
|
|
// Default to sorting by name for unknown fields
|
|
console.warn(`Unknown sort field: ${field}, defaulting to name`);
|
|
valueA = (a.name ? a.name.toLowerCase() : '');
|
|
valueB = (b.name ? a.name.toLowerCase() : '');
|
|
|
|
return direction === 'asc'
|
|
? valueA.localeCompare(valueB)
|
|
: valueB.localeCompare(valueA);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Export the module functions
|
|
export {
|
|
initListRenderer,
|
|
updateListState,
|
|
updateDashboardFilter,
|
|
updateSort,
|
|
renderAssetList,
|
|
sortAssets
|
|
}; |