mirror of
https://github.com/DumbWareio/DumbAssets.git
synced 2026-01-09 06:10:52 +08:00
Add quantity field (#74)
* Added Quantity Field ✅ Completed Changes 1. HTML Forms Added quantity input fields to both asset and sub-asset forms Fields default to value="1" with min="1" validation Positioned after price fields for logical flow 2. Modal Manager (Frontend) Updated form data collection to capture quantity values Added quantity to form population for editing existing assets Ensures quantity defaults to 1 if not provided 3. Asset Details Display Added quantity field to asset info display in assetRenderer.js Shows "Quantity: X" in the asset details section Includes fallback to 1 for backwards compatibility 4. Server-side Backwards Compatibility Asset Creation: Ensures quantity defaults to 1 if not provided Asset Updates: Preserves existing quantity or defaults to 1 Sub-asset Creation: Ensures quantity defaults to 1 if not provided Sub-asset Updates: Preserves existing quantity or defaults to 1 Data Loading: All assets and sub-assets get quantity field when loaded from API 5. Import Functionality Added quantity column mapping option in import modal Updated server-side import logic to handle quantity with default of 1 Updated template CSV download to include quantity example Added auto-mapping for "quantity" and "qty" column headers 🔒 Backwards Compatibility The implementation ensures that: Existing assets without quantity will automatically show quantity: 1 All API endpoints handle missing quantity gracefully Import functionality works with or without quantity columns No data migration is required - compatibility is handled at runtime * Add "Total Value" to asset details ✅ Total Value Field Features Conditional Display Only shows when quantity > 1 Only shows when there's a valid price (either price or purchasePrice) Calculation Calculates: price × quantity = total value Uses the same currency formatting as other price fields Handles both asset.price and asset.purchasePrice (for backwards compatibility) * Updated Dashboard Value Calculation logic Updated Calculation Logic to account for quantity Assets: price × quantity for each asset Defaults to quantity = 1 for backwards compatibility Sub-Assets: purchasePrice × quantity for each sub-asset Defaults to quantity = 1 for backwards compatibility
This commit is contained in:
parent
52f3be257f
commit
08b8d771a2
@ -170,6 +170,10 @@
|
||||
<label for="assetPrice">Price</label>
|
||||
<input type="number" id="assetPrice" name="price" step="0.01" min="0">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="assetQuantity">Quantity</label>
|
||||
<input type="number" id="assetQuantity" name="quantity" min="1" value="1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="assetWarrantyScope">Warranty Scope</label>
|
||||
<input type="text" id="assetWarrantyScope" name="warrantyScope">
|
||||
@ -359,6 +363,11 @@
|
||||
<input type="number" id="subAssetPurchasePrice" name="purchasePrice" step="0.01">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="subAssetQuantity">Quantity</label>
|
||||
<input type="number" id="subAssetQuantity" name="quantity" min="1" value="1">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="subAssetLink">Link</label>
|
||||
<input type="url" id="subAssetLink" name="link">
|
||||
@ -536,6 +545,10 @@
|
||||
<label>Purchase Price:</label>
|
||||
<select id="purchasePriceColumn" class="column-select"></select>
|
||||
</div>
|
||||
<div class="mapping-row">
|
||||
<label>Quantity:</label>
|
||||
<select id="quantityColumn" class="column-select"></select>
|
||||
</div>
|
||||
<div class="mapping-row">
|
||||
<label>Notes:</label>
|
||||
<select id="notesColumn" class="column-select"></select>
|
||||
|
||||
@ -147,8 +147,16 @@ export class DashboardManager {
|
||||
const totalComponents = totalSubAssets;
|
||||
|
||||
// Calculate total value including sub-assets
|
||||
const totalAssetsValue = assets.reduce((sum, a) => sum + (parseFloat(a.price) || 0), 0);
|
||||
const totalSubAssetsValue = subAssets.reduce((sum, sa) => sum + (parseFloat(sa.purchasePrice) || 0), 0);
|
||||
const totalAssetsValue = assets.reduce((sum, a) => {
|
||||
const price = parseFloat(a.price) || 0;
|
||||
const quantity = a.quantity || 1;
|
||||
return sum + (price * quantity);
|
||||
}, 0);
|
||||
const totalSubAssetsValue = subAssets.reduce((sum, sa) => {
|
||||
const price = parseFloat(sa.purchasePrice) || 0;
|
||||
const quantity = sa.quantity || 1;
|
||||
return sum + (price * quantity);
|
||||
}, 0);
|
||||
const totalValue = totalAssetsValue + totalSubAssetsValue;
|
||||
|
||||
|
||||
|
||||
@ -146,7 +146,8 @@ export class ImportManager {
|
||||
const warrantyExpirationColumn = document.getElementById('warrantyExpirationColumn');
|
||||
const lifetimeColumn = document.getElementById('lifetimeColumn');
|
||||
const tagsColumn = document.getElementById('tagsColumn');
|
||||
[urlColumn, warrantyColumn, warrantyExpirationColumn, lifetimeColumn, tagsColumn].forEach(select => {
|
||||
const quantityColumn = document.getElementById('quantityColumn');
|
||||
[urlColumn, warrantyColumn, warrantyExpirationColumn, lifetimeColumn, tagsColumn, quantityColumn].forEach(select => {
|
||||
if (!select) return;
|
||||
select.innerHTML = '<option value="">Select Column</option>';
|
||||
headers.forEach((header, index) => {
|
||||
@ -194,7 +195,8 @@ export class ImportManager {
|
||||
lifetime: document.getElementById('lifetimeColumn').value,
|
||||
secondaryWarranty: document.getElementById('secondaryWarrantyColumn') ? document.getElementById('secondaryWarrantyColumn').value : '',
|
||||
secondaryWarrantyExpiration: document.getElementById('secondaryWarrantyExpirationColumn') ? document.getElementById('secondaryWarrantyExpirationColumn').value : '',
|
||||
tags: document.getElementById('tagsColumn') ? document.getElementById('tagsColumn').value : ''
|
||||
tags: document.getElementById('tagsColumn') ? document.getElementById('tagsColumn').value : '',
|
||||
quantity: document.getElementById('quantityColumn') ? document.getElementById('quantityColumn').value : ''
|
||||
};
|
||||
if (!mappings.name) {
|
||||
globalThis.toaster.show('Please map the Name column', 'error');
|
||||
@ -286,7 +288,8 @@ export class ImportManager {
|
||||
lifetimeColumn: ["lifetime", "lifetime warranty", "is lifetime", "islifetime", "permanent"],
|
||||
secondaryWarrantyColumn: ["secondary warranty", "secondary warranty scope", "warranty 2", "warranty2", "warranty scope 2"],
|
||||
secondaryWarrantyExpirationColumn: ["secondary warranty expiration", "secondary warranty expiry", "secondary warranty end", "secondary warranty end date", "warranty 2 expiration", "warranty2 expiration", "warranty expiration 2", "warranty expiry 2"],
|
||||
tagsColumn: ["tags", "tag", "labels", "categories"]
|
||||
tagsColumn: ["tags", "tag", "labels", "categories"],
|
||||
quantityColumn: ["quantity", "qty"]
|
||||
};
|
||||
function normalize(str) {
|
||||
return str.toLowerCase().replace(/[^a-z0-9]/g, "");
|
||||
@ -342,7 +345,8 @@ export class ImportManager {
|
||||
'lifetimeColumn',
|
||||
'secondaryWarrantyColumn',
|
||||
'secondaryWarrantyExpirationColumn',
|
||||
'tagsColumn'
|
||||
'tagsColumn',
|
||||
'quantityColumn'
|
||||
];
|
||||
columnIds.forEach(id => {
|
||||
const select = document.getElementById(id);
|
||||
@ -373,7 +377,8 @@ export class ImportManager {
|
||||
'Lifetime',
|
||||
'Secondary Warranty',
|
||||
'Secondary Warranty Expiration',
|
||||
'Tags'
|
||||
'Tags',
|
||||
'Quantity'
|
||||
];
|
||||
// Generate test data row
|
||||
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
||||
@ -383,6 +388,7 @@ export class ImportManager {
|
||||
if (lower === 'url') return 'https://example.com';
|
||||
if (lower === 'tags') return '"tag1,tag2,tag3"'; // CSV string for tags
|
||||
if (lower === 'purchase price') return '123.45';
|
||||
if (lower === 'quantity') return '1'; // Default quantity
|
||||
if (lower === 'lifetime') return 'false'; // Boolean value for lifetime warranty
|
||||
return `Test ${h}`;
|
||||
});
|
||||
|
||||
@ -374,6 +374,7 @@ export class ModalManager {
|
||||
'assetSerial': asset.serialNumber || '',
|
||||
'assetPurchaseDate': asset.purchaseDate || '',
|
||||
'assetPrice': asset.price || '',
|
||||
'assetQuantity': asset.quantity || 1,
|
||||
'assetWarrantyScope': asset.warranty?.scope || '',
|
||||
'assetWarrantyLifetime': asset.warranty?.isLifetime || false,
|
||||
'assetWarrantyExpiration': asset.warranty?.expirationDate ? new Date(formatDate(asset.warranty.expirationDate)).toISOString().split('T')[0] : '',
|
||||
@ -408,6 +409,7 @@ export class ModalManager {
|
||||
'subAssetSerial': subAsset.serialNumber || '',
|
||||
'subAssetPurchaseDate': subAsset.purchaseDate || '',
|
||||
'subAssetPurchasePrice': subAsset.purchasePrice || '',
|
||||
'subAssetQuantity': subAsset.quantity || 1,
|
||||
'subAssetLink': subAsset.link || '',
|
||||
'subAssetNotes': subAsset.notes || '',
|
||||
'subAssetWarrantyScope': subAsset.warranty?.scope || '',
|
||||
@ -776,6 +778,7 @@ export class ModalManager {
|
||||
serialNumber: document.getElementById('assetSerial')?.value || '',
|
||||
purchaseDate: document.getElementById('assetPurchaseDate')?.value || '',
|
||||
price: parseFloat(document.getElementById('assetPrice')?.value) || null,
|
||||
quantity: parseInt(document.getElementById('assetQuantity')?.value) || 1,
|
||||
warranty: {
|
||||
scope: document.getElementById('assetWarrantyScope')?.value || '',
|
||||
expirationDate: document.getElementById('assetWarrantyLifetime')?.checked ? null : (document.getElementById('assetWarrantyExpiration')?.value || ''),
|
||||
@ -843,6 +846,7 @@ export class ModalManager {
|
||||
serialNumber: document.getElementById('subAssetSerial')?.value || '',
|
||||
purchaseDate: document.getElementById('subAssetPurchaseDate')?.value || '',
|
||||
purchasePrice: parseFloat(document.getElementById('subAssetPurchasePrice')?.value) || null,
|
||||
quantity: parseInt(document.getElementById('subAssetQuantity')?.value) || 1,
|
||||
parentId: document.getElementById('parentAssetId')?.value || '',
|
||||
parentSubId: document.getElementById('parentSubAssetId')?.value || '',
|
||||
link: document.getElementById('subAssetLink')?.value || '',
|
||||
|
||||
39
server.js
39
server.js
@ -622,13 +622,27 @@ if (!fs.existsSync(subAssetsFilePath)) {
|
||||
// Get all assets
|
||||
app.get('/api/assets', (req, res) => {
|
||||
const assets = readJsonFile(assetsFilePath);
|
||||
res.json(assets);
|
||||
|
||||
// Ensure backwards compatibility for quantity field
|
||||
const assetsWithQuantity = assets.map(asset => ({
|
||||
...asset,
|
||||
quantity: asset.quantity || 1
|
||||
}));
|
||||
|
||||
res.json(assetsWithQuantity);
|
||||
});
|
||||
|
||||
// Get all sub-assets
|
||||
app.get('/api/subassets', (req, res) => {
|
||||
const subAssets = readJsonFile(subAssetsFilePath);
|
||||
res.json(subAssets);
|
||||
|
||||
// Ensure backwards compatibility for quantity field
|
||||
const subAssetsWithQuantity = subAssets.map(subAsset => ({
|
||||
...subAsset,
|
||||
quantity: subAsset.quantity || 1
|
||||
}));
|
||||
|
||||
res.json(subAssetsWithQuantity);
|
||||
});
|
||||
|
||||
// Create a new asset
|
||||
@ -639,6 +653,11 @@ app.post('/api/asset', async (req, res) => {
|
||||
// Ensure maintenanceEvents is always present (even if empty)
|
||||
newAsset.maintenanceEvents = newAsset.maintenanceEvents || [];
|
||||
|
||||
// Ensure quantity is present for backwards compatibility
|
||||
if (typeof newAsset.quantity === 'undefined' || newAsset.quantity === null) {
|
||||
newAsset.quantity = 1;
|
||||
}
|
||||
|
||||
// Ensure required fields
|
||||
if (!newAsset.name) {
|
||||
return res.status(400).json({ error: 'Asset name is required' });
|
||||
@ -713,6 +732,11 @@ app.put('/api/assets/:id', async (req, res) => {
|
||||
|
||||
const existingAsset = assets[assetIndex];
|
||||
|
||||
// Ensure quantity is present for backwards compatibility
|
||||
if (typeof updatedAssetData.quantity === 'undefined' || updatedAssetData.quantity === null) {
|
||||
updatedAssetData.quantity = existingAsset.quantity || 1;
|
||||
}
|
||||
|
||||
if (updatedAssetData.filesToDelete && updatedAssetData.filesToDelete.length > 0) {
|
||||
await deleteAssetFiles(updatedAssetData.filesToDelete);
|
||||
}
|
||||
@ -845,6 +869,11 @@ app.post('/api/subasset', async (req, res) => {
|
||||
// Ensure maintenanceEvents is always present (even if empty)
|
||||
newSubAsset.maintenanceEvents = newSubAsset.maintenanceEvents || [];
|
||||
|
||||
// Ensure quantity is present for backwards compatibility
|
||||
if (typeof newSubAsset.quantity === 'undefined' || newSubAsset.quantity === null) {
|
||||
newSubAsset.quantity = 1;
|
||||
}
|
||||
|
||||
// Ensure required fields
|
||||
if (!newSubAsset.name || !newSubAsset.parentId) {
|
||||
return res.status(400).json({ error: 'Sub-asset name and parent ID are required' });
|
||||
@ -920,6 +949,11 @@ app.put('/api/subassets/:id', async (req, res) => {
|
||||
|
||||
const existingSubAsset = subAssets[subAssetIndex];
|
||||
|
||||
// Ensure quantity is present for backwards compatibility
|
||||
if (typeof updatedSubAssetData.quantity === 'undefined' || updatedSubAssetData.quantity === null) {
|
||||
updatedSubAssetData.quantity = existingSubAsset.quantity || 1;
|
||||
}
|
||||
|
||||
if (updatedSubAssetData.filesToDelete && updatedSubAssetData.filesToDelete.length > 0) {
|
||||
await deleteAssetFiles(updatedSubAssetData.filesToDelete);
|
||||
}
|
||||
@ -1285,6 +1319,7 @@ app.post('/api/import-assets', upload.single('file'), (req, res) => {
|
||||
serialNumber: get('serial'),
|
||||
purchaseDate: parseExcelDate(get('purchaseDate')),
|
||||
price: get('purchasePrice'),
|
||||
quantity: parseInt(get('quantity')) || 1,
|
||||
description: get('notes'),
|
||||
link: get('url'),
|
||||
warranty: {
|
||||
|
||||
@ -184,6 +184,16 @@ function generateAssetInfoHTML(asset) {
|
||||
<div class="info-label">Price</div>
|
||||
<div>${formatCurrency(asset.price || asset.purchasePrice)}</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Quantity</div>
|
||||
<div>${asset.quantity || 1}</div>
|
||||
</div>
|
||||
${(asset.quantity > 1 && (asset.price || asset.purchasePrice)) ? `
|
||||
<div class="info-item">
|
||||
<div class="info-label">Total Value</div>
|
||||
<div>${formatCurrency((asset.price || asset.purchasePrice) * asset.quantity)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
${asset.warranty?.expirationDate || asset.warranty?.isLifetime ? `
|
||||
<div class="info-item">
|
||||
<div class="info-label">Warranty</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user