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
822 lines
36 KiB
JavaScript
822 lines
36 KiB
JavaScript
// SettingsManager handles all settings modal logic, loading, saving, and dashboard order drag/drop
|
|
export class SettingsManager {
|
|
constructor({
|
|
settingsBtn,
|
|
settingsModal,
|
|
notificationForm,
|
|
saveSettings,
|
|
cancelSettings,
|
|
settingsClose,
|
|
testNotificationSettings,
|
|
setButtonLoading,
|
|
renderDashboard
|
|
}) {
|
|
this.localSettingsStorageKey = 'dumbAssetSettings';
|
|
this.localSettingsLastOpenedPaneKey = 'dumbAssetSettingsLastOpenedPane';
|
|
this.settingsBtn = settingsBtn;
|
|
this.settingsModal = settingsModal;
|
|
this.notificationForm = notificationForm;
|
|
this.saveSettings = saveSettings;
|
|
this.cancelSettings = cancelSettings;
|
|
this.settingsClose = settingsClose;
|
|
this.testNotificationSettings = testNotificationSettings;
|
|
this.setButtonLoading = setButtonLoading;
|
|
this.renderDashboard = renderDashboard;
|
|
this.selectedAssetId = null;
|
|
this.DEBUG = false;
|
|
this._bindEvents();
|
|
this.defaultSettings = window.appConfig?.defaultSettings || {
|
|
notificationSettings: {
|
|
notifyAdd: true,
|
|
notifyDelete: false,
|
|
notifyEdit: true,
|
|
notify1Month: true,
|
|
notify2Week: false,
|
|
notify7Day: true,
|
|
notify3Day: false,
|
|
notifyMaintenance: true // Default to true for compatibility
|
|
},
|
|
interfaceSettings: {
|
|
dashboardOrder: [],
|
|
dashboardVisibility: {
|
|
analytics: true,
|
|
totals: true,
|
|
warranties: true,
|
|
events: true
|
|
},
|
|
cardVisibility: {
|
|
assets: true,
|
|
components: true,
|
|
value: true,
|
|
warranties: true,
|
|
within60: true,
|
|
within30: true,
|
|
expired: true,
|
|
active: true
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
_bindEvents() {
|
|
this.settingsBtn.addEventListener('click', async () => {
|
|
await this.loadSettings();
|
|
this.settingsModal.style.display = 'block';
|
|
// Use last opened tab if available
|
|
const lastTab = localStorage.getItem(this.localSettingsLastOpenedPaneKey) || 'notifications';
|
|
this.showSettingsTab(lastTab);
|
|
});
|
|
this.settingsClose.addEventListener('click', () => this.closeSettingsModal());
|
|
this.cancelSettings.addEventListener('click', () => this.closeSettingsModal());
|
|
this.saveSettings.addEventListener('click', () => this._saveSettings());
|
|
this.testNotificationSettings.addEventListener('click', () => this._testNotificationSettings());
|
|
|
|
// Export button
|
|
const exportDataBtn = document.getElementById('exportDataBtn');
|
|
if (exportDataBtn) {
|
|
exportDataBtn.addEventListener('click', () => this._exportData());
|
|
}
|
|
|
|
// Export simple data button
|
|
const exportSimpleDataBtn = document.getElementById('exportSimpleDataBtn');
|
|
if (exportSimpleDataBtn) {
|
|
exportSimpleDataBtn.addEventListener('click', () => this._exportSimpleData());
|
|
}
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tabId = btn.getAttribute('data-tab');
|
|
// Save last opened tab to localStorage
|
|
localStorage.setItem(this.localSettingsLastOpenedPaneKey, tabId);
|
|
this.showSettingsTab(tabId);
|
|
});
|
|
});
|
|
}
|
|
|
|
closeSettingsModal() {
|
|
this.settingsModal.style.display = 'none';
|
|
}
|
|
|
|
showSettingsTab(tabId) {
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabId);
|
|
});
|
|
document.querySelectorAll('.tab-pane').forEach(pane => {
|
|
pane.classList.toggle('active', pane.id === `${tabId}-tab`);
|
|
});
|
|
if (tabId === 'notifications') {
|
|
this.testNotificationSettings.style.display = 'block';
|
|
} else {
|
|
this.testNotificationSettings.style.display = 'none';
|
|
}
|
|
if (tabId === 'interface') {
|
|
this.initSortable();
|
|
}
|
|
}
|
|
|
|
getDefaultSettings() {
|
|
return this.defaultSettings;
|
|
}
|
|
|
|
async fetchSettings() {
|
|
try {
|
|
const response = await fetch('/api/settings', { credentials: 'include' });
|
|
const responseValidation = await globalThis.validateResponse(response);
|
|
if (responseValidation.errorMessage) throw new Error(responseValidation.errorMessage);
|
|
|
|
const settings = await response.json();
|
|
const stringified = JSON.stringify(settings); // Deep clone to avoid mutation
|
|
localStorage.setItem(this.localSettingsStorageKey, stringified);
|
|
return JSON.parse(stringified); // Deep clone to avoid mutation
|
|
} catch (error) {
|
|
globalThis.logError('Failed to fetch settings:', error.message);
|
|
return this.getDefaultSettings(); // Return default settings on error
|
|
}
|
|
}
|
|
|
|
getSettingsFromLocalStorage() {
|
|
const localSettings = localStorage.getItem(this.localSettingsStorageKey);
|
|
if (localSettings) {
|
|
try {
|
|
const parsedSettings = JSON.parse(localSettings);
|
|
const mergedSettings = {
|
|
...this.getDefaultSettings(),
|
|
...parsedSettings
|
|
};
|
|
return { ...mergedSettings };
|
|
} catch (err) {
|
|
console.error('Error parsing settings from localStorage:', err);
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async loadSettings() {
|
|
try {
|
|
const settings = { ...await this.fetchSettings() };
|
|
const notificationSettings = settings.notificationSettings;
|
|
this.notificationForm.notifyAdd.checked = !!notificationSettings.notifyAdd;
|
|
this.notificationForm.notifyDelete.checked = !!notificationSettings.notifyDelete;
|
|
this.notificationForm.notifyEdit.checked = !!notificationSettings.notifyEdit;
|
|
this.notificationForm.notify1Month.checked = !!notificationSettings.notify1Month;
|
|
this.notificationForm.notify2Week.checked = !!notificationSettings.notify2Week;
|
|
this.notificationForm.notify7Day.checked = !!notificationSettings.notify7Day;
|
|
this.notificationForm.notify3Day.checked = !!notificationSettings.notify3Day;
|
|
this.notificationForm.notifyMaintenance.checked = (typeof notificationSettings.notifyMaintenance !== 'undefined')
|
|
? !!notificationSettings.notifyMaintenance
|
|
: (settings.notifyMaintenance !== false);
|
|
|
|
const interfaceSettings = settings.interfaceSettings;
|
|
// Dashboard order
|
|
if (interfaceSettings.dashboardOrder && Array.isArray(interfaceSettings.dashboardOrder)) {
|
|
const dashboardSectionsContainer = document.getElementById('dashboardSections');
|
|
if (dashboardSectionsContainer) {
|
|
const sections = dashboardSectionsContainer.querySelectorAll('.sortable-item');
|
|
const orderedSections = [];
|
|
let orderToUse = [...interfaceSettings.dashboardOrder];
|
|
|
|
orderToUse.forEach(sectionName => {
|
|
Array.from(sections).forEach(section => {
|
|
if (section.getAttribute('data-section') === sectionName) {
|
|
orderedSections.push(section);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add any sections that exist in HTML but not in saved order
|
|
Array.from(sections).forEach(section => {
|
|
if (!orderedSections.includes(section)) {
|
|
orderedSections.push(section);
|
|
}
|
|
});
|
|
|
|
dashboardSectionsContainer.innerHTML = '';
|
|
orderedSections.forEach(section => {
|
|
dashboardSectionsContainer.appendChild(section);
|
|
});
|
|
}
|
|
}
|
|
// Dashboard visibility - ensure Events defaults to true
|
|
const vis = interfaceSettings.dashboardVisibility || {};
|
|
// Set defaults for any missing values
|
|
const visibilityDefaults = { analytics: true, totals: true, warranties: true, events: true };
|
|
const finalVisibility = { ...visibilityDefaults, ...vis };
|
|
|
|
document.getElementById('toggleTotals').checked = finalVisibility.totals;
|
|
document.getElementById('toggleWarranties').checked = finalVisibility.warranties;
|
|
document.getElementById('toggleAnalytics').checked = finalVisibility.analytics;
|
|
document.getElementById('toggleEvents').checked = finalVisibility.events;
|
|
// Card visibility toggles
|
|
if (typeof window.renderCardVisibilityToggles === 'function') {
|
|
window.renderCardVisibilityToggles(settings);
|
|
}
|
|
localStorage.setItem(this.localSettingsStorageKey, JSON.stringify(settings));
|
|
return settings;
|
|
} catch (err) {
|
|
console.error('Error loading settings:', err);
|
|
// Set default values when loading fails
|
|
this.notificationForm.notifyAdd.checked = this.defaultSettings.notificationSettings.notifyAdd;
|
|
this.notificationForm.notifyDelete.checked = this.defaultSettings.notificationSettings.notifyDelete;
|
|
this.notificationForm.notifyEdit.checked = this.defaultSettings.notificationSettings.notifyEdit;
|
|
this.notificationForm.notify1Month.checked = this.defaultSettings.notificationSettings.notify1Month;
|
|
this.notificationForm.notify2Week.checked = this.defaultSettings.notificationSettings.notify2Week;
|
|
this.notificationForm.notify7Day.checked = this.defaultSettings.notificationSettings.notify7Day;
|
|
this.notificationForm.notify3Day.checked = this.defaultSettings.notificationSettings.notify3Day;
|
|
// Ensure Events toggle is enabled by default when loading fails
|
|
document.getElementById('toggleEvents').checked = true;
|
|
}
|
|
}
|
|
|
|
async _saveSettings() {
|
|
this.setButtonLoading(this.saveSettings, true);
|
|
const settings = {
|
|
notificationSettings: {
|
|
notifyAdd: this.notificationForm.notifyAdd.checked,
|
|
notifyDelete: this.notificationForm.notifyDelete.checked,
|
|
notifyEdit: this.notificationForm.notifyEdit.checked,
|
|
notify1Month: this.notificationForm.notify1Month.checked,
|
|
notify2Week: this.notificationForm.notify2Week.checked,
|
|
notify7Day: this.notificationForm.notify7Day.checked,
|
|
notify3Day: this.notificationForm.notify3Day.checked,
|
|
notifyMaintenance: this.notificationForm.notifyMaintenance.checked // Ensure this is always present
|
|
},
|
|
interfaceSettings: {
|
|
dashboardOrder: [],
|
|
dashboardVisibility: {
|
|
analytics: document.getElementById('toggleAnalytics').checked,
|
|
totals: document.getElementById('toggleTotals').checked,
|
|
warranties: document.getElementById('toggleWarranties').checked,
|
|
events: document.getElementById('toggleEvents').checked
|
|
},
|
|
cardVisibility: {
|
|
assets: document.getElementById('toggleCardTotalAssets')?.checked !== false,
|
|
components: document.getElementById('toggleCardTotalComponents')?.checked !== false,
|
|
value: document.getElementById('toggleCardTotalValue')?.checked !== false,
|
|
warranties: document.getElementById('toggleCardWarrantiesTotal')?.checked !== false,
|
|
within60: document.getElementById('toggleCardWarrantiesWithin60')?.checked !== false,
|
|
within30: document.getElementById('toggleCardWarrantiesWithin30')?.checked !== false,
|
|
expired: document.getElementById('toggleCardWarrantiesExpired')?.checked !== false,
|
|
active: document.getElementById('toggleCardWarrantiesActive')?.checked !== false
|
|
}
|
|
}
|
|
};
|
|
const dashboardSections = document.querySelectorAll('#dashboardSections .sortable-item');
|
|
dashboardSections.forEach(section => {
|
|
settings.interfaceSettings.dashboardOrder.push(section.getAttribute('data-section'));
|
|
});
|
|
|
|
try {
|
|
const response = await fetch('/api/settings', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(settings),
|
|
credentials: 'include'
|
|
});
|
|
const responseValidation = await globalThis.validateResponse(response);
|
|
if (responseValidation.errorMessage) throw new Error(responseValidation.errorMessage);
|
|
|
|
const settingsCopy = { ...settings };
|
|
localStorage.setItem(this.localSettingsStorageKey, JSON.stringify(settingsCopy));
|
|
this.closeSettingsModal();
|
|
globalThis.toaster.show('Settings saved');
|
|
if (!this.selectedAssetId && typeof this.renderDashboard === 'function') {
|
|
this.renderDashboard();
|
|
}
|
|
} catch (error) {
|
|
globalThis.logError('Failed to save settings:', error.message);
|
|
} finally {
|
|
this.setButtonLoading(this.saveSettings, false);
|
|
}
|
|
}
|
|
|
|
async _testNotificationSettings() {
|
|
if (this.DEBUG) {
|
|
console.log('[DEBUG] Test notification settings button clicked');
|
|
}
|
|
this.setButtonLoading(this.testNotificationSettings, true);
|
|
const enabledTypes = [];
|
|
const f = this.notificationForm;
|
|
if (f.notifyAdd.checked) enabledTypes.push('notifyAdd');
|
|
if (f.notifyDelete.checked) enabledTypes.push('notifyDelete');
|
|
if (f.notifyEdit.checked) enabledTypes.push('notifyEdit');
|
|
if (f.notify1Month.checked) enabledTypes.push('notify1Month');
|
|
if (f.notify2Week.checked) enabledTypes.push('notify2Week');
|
|
if (f.notify7Day.checked) enabledTypes.push('notify7Day');
|
|
if (f.notify3Day.checked) enabledTypes.push('notify3Day');
|
|
if (f.notifyMaintenance.checked) enabledTypes.push('notifyMaintenance');
|
|
if (enabledTypes.length === 0) enabledTypes.push('notifyAdd');
|
|
fetch('/api/notification-test', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ enabledTypes })
|
|
})
|
|
.then(async (response) => {
|
|
const responseValidation = await globalThis.validateResponse(response);
|
|
if (responseValidation.errorMessage) throw new Error(responseValidation.errorMessage);
|
|
globalThis.toaster.show('Test notifications sent successfully!');
|
|
})
|
|
.catch(error => {
|
|
globalThis.logError('Test Notification Failed:', error.message);
|
|
})
|
|
.finally(() => {
|
|
this.setButtonLoading(this.testNotificationSettings, false);
|
|
});
|
|
}
|
|
|
|
cleanupPlaceholders(container) {
|
|
const oldPlaceholders = container.querySelectorAll('.sortable-placeholder');
|
|
oldPlaceholders.forEach(el => el.parentNode.removeChild(el));
|
|
}
|
|
|
|
// Drag and drop for dashboard order
|
|
initSortable() {
|
|
const container = document.getElementById('dashboardSections');
|
|
if (!container) return;
|
|
let draggedItem = null;
|
|
let placeholder = null;
|
|
let initialX, initialY, startClientX, startClientY;
|
|
let itemHeight, itemWidth;
|
|
let isTouch = false;
|
|
this.cleanupPlaceholders(container);
|
|
// Remove all old event listeners by cloning each sortable-item
|
|
const oldItems = Array.from(container.querySelectorAll('.sortable-item'));
|
|
oldItems.forEach(item => {
|
|
const clone = item.cloneNode(true);
|
|
item.parentNode.replaceChild(clone, item);
|
|
});
|
|
// Now select the fresh clones
|
|
const items = container.querySelectorAll('.sortable-item');
|
|
const self = this;
|
|
items.forEach(item => {
|
|
// Desktop (mouse)
|
|
item.addEventListener('mousedown', (e) => {
|
|
const isInteractiveElement = e.target.closest('button') || e.target.closest('a') || e.target.closest('input') || e.target.closest('select');
|
|
if (e.target.closest('.sortable-handle') || (!isInteractiveElement)) {
|
|
e.preventDefault();
|
|
isTouch = false;
|
|
startDrag(item, e.clientX, e.clientY);
|
|
document.addEventListener('mousemove', onMouseMove);
|
|
document.addEventListener('mouseup', onMouseUp);
|
|
}
|
|
});
|
|
// Mobile (touch)
|
|
item.addEventListener('touchstart', (e) => {
|
|
const touch = e.touches[0];
|
|
const isInteractiveElement = e.target.closest('button') || e.target.closest('a') || e.target.closest('input') || e.target.closest('select');
|
|
if (e.target.closest('.sortable-handle') || (!isInteractiveElement)) {
|
|
e.preventDefault();
|
|
isTouch = true;
|
|
startDrag(item, touch.clientX, touch.clientY);
|
|
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
document.addEventListener('touchend', onTouchEnd);
|
|
}
|
|
});
|
|
});
|
|
function startDrag(item, clientX, clientY) {
|
|
self.cleanupPlaceholders(container);
|
|
draggedItem = item;
|
|
draggedItem.classList.add('dragging');
|
|
const rect = draggedItem.getBoundingClientRect();
|
|
itemHeight = rect.height;
|
|
itemWidth = rect.width;
|
|
placeholder = document.createElement('div');
|
|
placeholder.classList.add('sortable-placeholder');
|
|
placeholder.style.height = `${itemHeight}px`;
|
|
placeholder.style.width = `${itemWidth}px`;
|
|
const itemContent = draggedItem.textContent.trim();
|
|
if (itemContent) {
|
|
const ghostContent = document.createElement('span');
|
|
ghostContent.textContent = itemContent;
|
|
ghostContent.style.opacity = '0.5';
|
|
ghostContent.style.padding = '10px 15px';
|
|
ghostContent.style.display = 'flex';
|
|
ghostContent.style.alignItems = 'center';
|
|
placeholder.appendChild(ghostContent);
|
|
}
|
|
initialX = rect.left;
|
|
initialY = rect.top;
|
|
startClientX = clientX;
|
|
startClientY = clientY;
|
|
draggedItem.parentNode.insertBefore(placeholder, draggedItem);
|
|
draggedItem.style.position = 'fixed';
|
|
draggedItem.style.zIndex = '1000';
|
|
draggedItem.style.width = `${itemWidth}px`;
|
|
draggedItem.style.left = `${initialX}px`;
|
|
draggedItem.style.top = `${initialY}px`;
|
|
}
|
|
function onMouseMove(e) {
|
|
if (!draggedItem) return;
|
|
e.preventDefault();
|
|
moveDraggedItem(e.clientX, e.clientY);
|
|
}
|
|
function onTouchMove(e) {
|
|
if (!draggedItem) return;
|
|
e.preventDefault();
|
|
const touch = e.touches[0];
|
|
moveDraggedItem(touch.clientX, touch.clientY);
|
|
}
|
|
function moveDraggedItem(clientX, clientY) {
|
|
const deltaX = clientX - startClientX;
|
|
const deltaY = clientY - startClientY;
|
|
draggedItem.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
|
const draggingRect = draggedItem.getBoundingClientRect();
|
|
const draggingMiddleY = draggingRect.top + draggingRect.height / 2;
|
|
const siblings = Array.from(container.querySelectorAll('.sortable-item:not(.dragging)'));
|
|
if (siblings.length === 0) return;
|
|
const firstItem = siblings[0];
|
|
const lastItem = siblings[siblings.length - 1];
|
|
const firstRect = firstItem.getBoundingClientRect();
|
|
const lastRect = lastItem.getBoundingClientRect();
|
|
siblings.forEach(item => item.classList.remove('shift-up', 'shift-down'));
|
|
if (draggingMiddleY < firstRect.top + firstRect.height * 0.25) {
|
|
firstItem.classList.add('shift-down');
|
|
container.insertBefore(placeholder, firstItem);
|
|
return;
|
|
}
|
|
if (draggingMiddleY > lastRect.bottom - lastRect.height * 0.25) {
|
|
lastItem.classList.add('shift-up');
|
|
container.insertBefore(placeholder, lastItem.nextElementSibling);
|
|
return;
|
|
}
|
|
let closestItem = null;
|
|
let closestDistance = Infinity;
|
|
siblings.forEach(sibling => {
|
|
const rect = sibling.getBoundingClientRect();
|
|
const distance = Math.abs(rect.top + rect.height / 2 - draggingMiddleY);
|
|
if (distance < closestDistance) {
|
|
closestDistance = distance;
|
|
closestItem = sibling;
|
|
}
|
|
});
|
|
if (closestItem) {
|
|
const rect = closestItem.getBoundingClientRect();
|
|
const threshold = rect.top + rect.height * 0.4;
|
|
const isAfter = draggingMiddleY > threshold;
|
|
siblings.forEach(item => {
|
|
item.classList.remove('shift-up', 'shift-down');
|
|
});
|
|
if (isAfter && placeholder.nextElementSibling !== closestItem) {
|
|
closestItem.classList.add('shift-down');
|
|
container.insertBefore(placeholder, closestItem.nextElementSibling);
|
|
} else if (!isAfter && placeholder.previousElementSibling !== closestItem) {
|
|
closestItem.classList.add('shift-up');
|
|
container.insertBefore(placeholder, closestItem);
|
|
}
|
|
}
|
|
}
|
|
function onMouseUp() {
|
|
if (draggedItem) finishDrag();
|
|
document.removeEventListener('mousemove', onMouseMove);
|
|
document.removeEventListener('mouseup', onMouseUp);
|
|
}
|
|
function onTouchEnd() {
|
|
if (draggedItem) finishDrag();
|
|
document.removeEventListener('touchmove', onTouchMove);
|
|
document.removeEventListener('touchend', onTouchEnd);
|
|
}
|
|
function finishDrag() {
|
|
container.querySelectorAll('.shift-up, .shift-down').forEach(item => {
|
|
item.classList.remove('shift-up', 'shift-down');
|
|
});
|
|
if (placeholder && placeholder.parentNode) {
|
|
placeholder.parentNode.insertBefore(draggedItem, placeholder);
|
|
placeholder.parentNode.removeChild(placeholder);
|
|
}
|
|
self.cleanupPlaceholders(container);
|
|
draggedItem.classList.remove('dragging');
|
|
draggedItem.style.position = '';
|
|
draggedItem.style.top = '';
|
|
draggedItem.style.left = '';
|
|
draggedItem.style.width = '';
|
|
draggedItem.style.transform = '';
|
|
draggedItem.style.zIndex = '';
|
|
draggedItem.animate([
|
|
{ transform: 'scale(1.03)', backgroundColor: 'rgba(37, 100, 235, 0.1)' },
|
|
{ transform: 'scale(1)', backgroundColor: 'var(--file-label-color)' }
|
|
], {
|
|
duration: 300,
|
|
easing: 'ease-out'
|
|
});
|
|
draggedItem = null;
|
|
placeholder = null;
|
|
const newOrder = [];
|
|
document.querySelectorAll('#dashboardSections .sortable-item').forEach(item => {
|
|
newOrder.push(item.getAttribute('data-section'));
|
|
});
|
|
try {
|
|
const settings = self.getSettingsFromLocalStorage() || self.defaultSettings;
|
|
settings.interfaceSettings.dashboardOrder = newOrder;
|
|
localStorage.setItem(self.localSettingsStorageKey, JSON.stringify(settings));
|
|
} catch (err) {
|
|
console.error('Error updating local storage', err);
|
|
}
|
|
}
|
|
}
|
|
|
|
async _exportData() {
|
|
const exportBtn = document.getElementById('exportDataBtn');
|
|
if (!exportBtn) return;
|
|
|
|
this.setButtonLoading(exportBtn, true);
|
|
|
|
try {
|
|
const apiBaseUrl = globalThis.getApiBaseUrl();
|
|
|
|
// Fetch both assets and sub-assets
|
|
const [assetsResponse, subAssetsResponse] = await Promise.all([
|
|
fetch(`${apiBaseUrl}/api/assets`, { credentials: 'include' }),
|
|
fetch(`${apiBaseUrl}/api/subassets`, { credentials: 'include' })
|
|
]);
|
|
|
|
// Validate responses
|
|
const assetsValidation = await globalThis.validateResponse(assetsResponse);
|
|
if (assetsValidation.errorMessage) throw new Error(assetsValidation.errorMessage);
|
|
|
|
const subAssetsValidation = await globalThis.validateResponse(subAssetsResponse);
|
|
if (subAssetsValidation.errorMessage) throw new Error(subAssetsValidation.errorMessage);
|
|
|
|
const assets = await assetsResponse.json();
|
|
const subAssets = await subAssetsResponse.json();
|
|
|
|
// Generate CSV content
|
|
const csvContent = this._generateCSV(assets, subAssets);
|
|
|
|
// Create and download the file
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// Generate filename with current date
|
|
const now = new Date();
|
|
const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
const filename = `dumbAssets_export_${dateStr}.csv`;
|
|
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', filename);
|
|
link.style.visibility = 'hidden';
|
|
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
globalThis.toaster.show('Data exported successfully!');
|
|
|
|
} catch (error) {
|
|
globalThis.logError('Failed to export data:', error.message);
|
|
} finally {
|
|
this.setButtonLoading(exportBtn, false);
|
|
}
|
|
}
|
|
|
|
_generateCSV(assets, subAssets) {
|
|
// CSV headers
|
|
const headers = [
|
|
'Type',
|
|
'ID',
|
|
'Name',
|
|
'Manufacturer',
|
|
'Model Number',
|
|
'Serial Number',
|
|
'Purchase Date',
|
|
'Purchase Price',
|
|
'Currency',
|
|
'Location',
|
|
'URL',
|
|
'Notes',
|
|
'Tags',
|
|
'Warranty Scope',
|
|
'Warranty Expiration',
|
|
'Warranty Lifetime',
|
|
'Secondary Warranty Scope',
|
|
'Secondary Warranty Expiration',
|
|
'Secondary Warranty Lifetime',
|
|
'Maintenance Events',
|
|
'Photo Path',
|
|
'Receipt Path',
|
|
'Manual Path',
|
|
'Parent ID',
|
|
'Parent Sub ID',
|
|
'Created At',
|
|
'Updated At'
|
|
];
|
|
|
|
const rows = [headers];
|
|
|
|
// Helper function to escape CSV values
|
|
const escapeCsvValue = (value) => {
|
|
if (value === null || value === undefined) return '';
|
|
const str = String(value);
|
|
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
return `"${str.replace(/"/g, '""')}"`;
|
|
}
|
|
return str;
|
|
};
|
|
|
|
// Helper function to format maintenance events
|
|
const formatMaintenanceEvents = (events) => {
|
|
if (!events || !events.length) return '';
|
|
return events.map(event => {
|
|
let eventStr = `${event.name}`;
|
|
if (event.type === 'frequency') {
|
|
eventStr += ` (Every ${event.frequency} ${event.frequencyUnit})`;
|
|
if (event.nextDueDate) {
|
|
eventStr += ` - Next: ${event.nextDueDate}`;
|
|
}
|
|
} else if (event.type === 'specific' && event.specificDate) {
|
|
eventStr += ` - Date: ${event.specificDate}`;
|
|
}
|
|
if (event.notes) {
|
|
eventStr += ` - Notes: ${event.notes}`;
|
|
}
|
|
return eventStr;
|
|
}).join('; ');
|
|
};
|
|
|
|
// Add assets
|
|
assets.forEach(asset => {
|
|
const row = [
|
|
'Asset',
|
|
asset.id || '',
|
|
asset.name || '',
|
|
asset.manufacturer || '',
|
|
asset.modelNumber || '',
|
|
asset.serialNumber || '',
|
|
asset.purchaseDate || '',
|
|
asset.price || '',
|
|
asset.currency || '',
|
|
asset.location || '',
|
|
asset.url || '',
|
|
asset.notes || '',
|
|
(asset.tags && asset.tags.length > 0) ? asset.tags.join('; ') : '',
|
|
asset.warranty?.scope || '',
|
|
asset.warranty?.expirationDate || '',
|
|
asset.warranty?.isLifetime ? 'Yes' : 'No',
|
|
asset.secondaryWarranty?.scope || '',
|
|
asset.secondaryWarranty?.expirationDate || '',
|
|
asset.secondaryWarranty?.isLifetime ? 'Yes' : 'No',
|
|
formatMaintenanceEvents(asset.maintenanceEvents),
|
|
asset.photoPath || '',
|
|
asset.receiptPath || '',
|
|
asset.manualPath || '',
|
|
'', // Parent ID (empty for assets)
|
|
'', // Parent Sub ID (empty for assets)
|
|
asset.createdAt || '',
|
|
asset.updatedAt || ''
|
|
];
|
|
rows.push(row.map(escapeCsvValue));
|
|
});
|
|
|
|
// Add sub-assets
|
|
subAssets.forEach(subAsset => {
|
|
const row = [
|
|
subAsset.parentSubId ? 'Sub-Component' : 'Component',
|
|
subAsset.id || '',
|
|
subAsset.name || '',
|
|
subAsset.manufacturer || '',
|
|
subAsset.modelNumber || '',
|
|
subAsset.serialNumber || '',
|
|
subAsset.purchaseDate || '',
|
|
subAsset.purchasePrice || '',
|
|
subAsset.currency || '',
|
|
subAsset.location || '',
|
|
subAsset.url || '',
|
|
subAsset.notes || '',
|
|
(subAsset.tags && subAsset.tags.length > 0) ? subAsset.tags.join('; ') : '',
|
|
subAsset.warranty?.scope || '',
|
|
subAsset.warranty?.expirationDate || '',
|
|
subAsset.warranty?.isLifetime ? 'Yes' : 'No',
|
|
'', // Secondary warranty scope (sub-assets don't have secondary warranties)
|
|
'', // Secondary warranty expiration
|
|
'', // Secondary warranty lifetime
|
|
formatMaintenanceEvents(subAsset.maintenanceEvents),
|
|
subAsset.photoPath || '',
|
|
subAsset.receiptPath || '',
|
|
subAsset.manualPath || '',
|
|
subAsset.parentId || '',
|
|
subAsset.parentSubId || '',
|
|
subAsset.createdAt || '',
|
|
subAsset.updatedAt || ''
|
|
];
|
|
rows.push(row.map(escapeCsvValue));
|
|
});
|
|
|
|
// Convert to CSV string
|
|
return rows.map(row => row.join(',')).join('\n');
|
|
}
|
|
|
|
async _exportSimpleData() {
|
|
const exportBtn = document.getElementById('exportSimpleDataBtn');
|
|
if (!exportBtn) return;
|
|
|
|
this.setButtonLoading(exportBtn, true);
|
|
|
|
try {
|
|
const apiBaseUrl = globalThis.getApiBaseUrl();
|
|
|
|
// Fetch both assets and sub-assets
|
|
const [assetsResponse, subAssetsResponse] = await Promise.all([
|
|
fetch(`${apiBaseUrl}/api/assets`, { credentials: 'include' }),
|
|
fetch(`${apiBaseUrl}/api/subassets`, { credentials: 'include' })
|
|
]);
|
|
|
|
// Validate responses
|
|
const assetsValidation = await globalThis.validateResponse(assetsResponse);
|
|
if (assetsValidation.errorMessage) throw new Error(assetsValidation.errorMessage);
|
|
|
|
const subAssetsValidation = await globalThis.validateResponse(subAssetsResponse);
|
|
if (subAssetsValidation.errorMessage) throw new Error(subAssetsValidation.errorMessage);
|
|
|
|
const assets = await assetsResponse.json();
|
|
const subAssets = await subAssetsResponse.json();
|
|
|
|
// Generate simplified CSV content
|
|
const csvContent = this._generateSimpleCSV(assets, subAssets);
|
|
|
|
// Create and download the file
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// Generate filename with current date
|
|
const now = new Date();
|
|
const dateStr = now.toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
const filename = `dumbAssets_simple_export_${dateStr}.csv`;
|
|
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', filename);
|
|
link.style.visibility = 'hidden';
|
|
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
globalThis.toaster.show('Simple data exported successfully!');
|
|
|
|
} catch (error) {
|
|
globalThis.logError('Failed to export simple data:', error.message);
|
|
} finally {
|
|
this.setButtonLoading(exportBtn, false);
|
|
}
|
|
}
|
|
|
|
_generateSimpleCSV(assets, subAssets) {
|
|
// Simple CSV headers - only basic fields
|
|
const headers = [
|
|
'Name',
|
|
'Manufacturer',
|
|
'Model',
|
|
'Serial',
|
|
'Purchase Date',
|
|
'Purchase Price',
|
|
'Notes',
|
|
'URL'
|
|
];
|
|
|
|
const rows = [headers];
|
|
|
|
// Helper function to escape CSV values
|
|
const escapeCsvValue = (value) => {
|
|
if (value === null || value === undefined) return '';
|
|
const str = String(value);
|
|
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
return `"${str.replace(/"/g, '""')}"`;
|
|
}
|
|
return str;
|
|
};
|
|
|
|
// Add assets
|
|
assets.forEach(asset => {
|
|
const row = [
|
|
asset.name || '',
|
|
asset.manufacturer || '',
|
|
asset.modelNumber || '',
|
|
asset.serialNumber || '',
|
|
asset.purchaseDate || '',
|
|
asset.price || '',
|
|
asset.notes || '',
|
|
asset.url || ''
|
|
];
|
|
rows.push(row.map(escapeCsvValue));
|
|
});
|
|
|
|
// Add sub-assets
|
|
subAssets.forEach(subAsset => {
|
|
const row = [
|
|
subAsset.name || '',
|
|
subAsset.manufacturer || '',
|
|
subAsset.modelNumber || '',
|
|
subAsset.serialNumber || '',
|
|
subAsset.purchaseDate || '',
|
|
subAsset.purchasePrice || '',
|
|
subAsset.notes || '',
|
|
subAsset.url || ''
|
|
];
|
|
rows.push(row.map(escapeCsvValue));
|
|
});
|
|
|
|
// Convert to CSV string
|
|
return rows.map(row => row.join(',')).join('\n');
|
|
}
|
|
}
|