diff --git a/static/scripts/add.js b/static/scripts/add.js new file mode 100644 index 0000000..9d55544 --- /dev/null +++ b/static/scripts/add.js @@ -0,0 +1,80 @@ +// Add page - handles both sets and individual minifigures +document.addEventListener("DOMContentLoaded", () => { + // Get template data from data attributes + const addContainer = document.getElementById('add-set'); + if (!addContainer) return; + + // Read data from data attributes + const templateData = { + path: addContainer.dataset.path, + namespace: addContainer.dataset.namespace, + messages: { + COMPLETE: addContainer.dataset.msgComplete, + FAIL: addContainer.dataset.msgFail, + IMPORT_SET: addContainer.dataset.msgImportSet, + LOAD_SET: addContainer.dataset.msgLoadSet, + PROGRESS: addContainer.dataset.msgProgress, + SET_LOADED: addContainer.dataset.msgSetLoaded, + IMPORT_MINIFIGURE: addContainer.dataset.msgImportMinifigure, + LOAD_MINIFIGURE: addContainer.dataset.msgLoadMinifigure, + MINIFIGURE_LOADED: addContainer.dataset.msgMinifigureLoaded, + } + }; + + // Default: create set socket + const setSocket = new BrickSetSocket( + 'add', + templateData.path, + templateData.namespace, + { + COMPLETE: templateData.messages.COMPLETE, + FAIL: templateData.messages.FAIL, + IMPORT_SET: templateData.messages.IMPORT_SET, + LOAD_SET: templateData.messages.LOAD_SET, + PROGRESS: templateData.messages.PROGRESS, + SET_LOADED: templateData.messages.SET_LOADED, + }, + false, + false + ); + + // Override the execute method to check for minifigures + const originalExecute = setSocket.execute.bind(setSocket); + let minifigSocket = null; + + setSocket.execute = function() { + const inputValue = document.getElementById('add-set').value.trim(); + + if (inputValue.startsWith('fig-') || inputValue.match(/^fig\d/i)) { + // It's a minifigure - create minifig socket if needed and execute when ready + if (!minifigSocket) { + minifigSocket = new BrickMinifigureSocket( + 'add', + templateData.path, + templateData.namespace, + { + COMPLETE: templateData.messages.COMPLETE, + FAIL: templateData.messages.FAIL, + IMPORT_MINIFIGURE: templateData.messages.IMPORT_MINIFIGURE, + LOAD_MINIFIGURE: templateData.messages.LOAD_MINIFIGURE, + MINIFIGURE_LOADED: templateData.messages.MINIFIGURE_LOADED, + PROGRESS: templateData.messages.PROGRESS, + } + ); + + // Wait for socket to connect before executing + const checkConnection = setInterval(() => { + if (minifigSocket.socket && minifigSocket.socket.connected) { + clearInterval(checkConnection); + minifigSocket.execute(); + } + }, 100); + } else { + minifigSocket.execute(); + } + } else { + // It's a set - use original execute + originalExecute(); + } + }; +}); diff --git a/static/scripts/add_parts.js b/static/scripts/add_parts.js new file mode 100644 index 0000000..920be2f --- /dev/null +++ b/static/scripts/add_parts.js @@ -0,0 +1,802 @@ +// Add parts page - handles individual parts and lots +document.addEventListener("DOMContentLoaded", () => { + // Get template data from data attributes + const addPartInput = document.getElementById('add-part-input'); + if (!addPartInput) return; + + // Read data from data attributes + const templateData = { + path: addPartInput.dataset.path, + namespace: addPartInput.dataset.namespace, + messages: { + COMPLETE: addPartInput.dataset.msgComplete, + CREATE_LOT: addPartInput.dataset.msgCreateLot, + FAIL: addPartInput.dataset.msgFail, + LOAD_PART: addPartInput.dataset.msgLoadPart, + LOAD_PART_COLORS: addPartInput.dataset.msgLoadPartColors, + PART_COLORS_LOADED: addPartInput.dataset.msgPartColorsLoaded, + PROGRESS: addPartInput.dataset.msgProgress, + PART_LOADED: addPartInput.dataset.msgPartLoaded, + } + }; + + // Initialize the socket + const partSocket = new BrickPartColorSocket( + 'add-part', + templateData.path, + templateData.namespace, + { + COMPLETE: templateData.messages.COMPLETE, + CREATE_LOT: templateData.messages.CREATE_LOT, + CREATE_BULK_INDIVIDUAL_PARTS: 'create_bulk_individual_parts', + FAIL: templateData.messages.FAIL, + LOAD_PART: templateData.messages.LOAD_PART, + LOAD_PART_COLORS: templateData.messages.LOAD_PART_COLORS, + PART_COLORS_LOADED: templateData.messages.PART_COLORS_LOADED, + PROGRESS: templateData.messages.PROGRESS, + PART_LOADED: templateData.messages.PART_LOADED, + } + ); +}); + +// Individual Part Color Selection Socket class +class BrickPartColorSocket extends BrickSocket { + constructor(id, path, namespace, messages) { + super(id, path, namespace, messages, false); + + // Form elements + this.html_button = document.getElementById(id + '-lookup'); + this.html_input = document.getElementById(`${id}-input`); + this.html_owners = document.getElementById(`${id}-owners`); + this.html_purchase_location = document.getElementById(`${id}-purchase-location`); + this.html_purchase_date = document.getElementById(`${id}-purchase-date`); + this.html_purchase_price = document.getElementById(`${id}-purchase-price`); + this.html_storage = document.getElementById(`${id}-storage`); + this.html_tags = document.getElementById(`${id}-tags`); + + // Color selection elements + this.html_colors_section = document.getElementById(`${id}-colors-section`); + this.html_colors_grid = document.getElementById(`${id}-colors-grid`); + this.html_metadata_section = document.getElementById(`${id}-metadata-section`); + + // Add mode elements (radio buttons) + this.html_single_mode = document.getElementById(`${id}-single-mode`); + this.html_bulk_mode = document.getElementById(`${id}-bulk-mode`); + this.html_lot_mode = document.getElementById(`${id}-lot-mode`); + this.html_cart_section = document.getElementById(`${id}-cart-section`); + this.html_cart_items = document.getElementById(`${id}-cart-items`); + this.html_cart_count = document.getElementById(`${id}-cart-count`); + this.html_complete_lot = document.getElementById(`${id}-complete-lot`); + this.html_complete_button_text = document.getElementById(`${id}-complete-button-text`); + this.html_clear_cart = document.getElementById(`${id}-clear-cart`); + + // State + this.current_part = null; + this.current_part_name = null; + this.current_colors = null; + this.selected_color = null; + + // Cart state + this.add_mode = 'single'; // 'single', 'bulk', or 'lot' + this.cart = []; // Array of {part, part_name, color_id, color_name, quantity, color_info} + + if (this.html_button) { + this.html_button.addEventListener("click", ((bricksocket) => (e) => { + bricksocket.lookup_part(); + })(this)); + } + + if (this.html_input) { + this.html_input.addEventListener("keyup", ((bricksocket) => (e) => { + if (e.key === 'Enter') { + bricksocket.lookup_part(); + } + })(this)); + } + + // Add mode radio buttons + if (this.html_single_mode) { + this.html_single_mode.addEventListener("change", ((bricksocket) => (e) => { + if (e.target.checked) bricksocket.update_add_mode('single'); + })(this)); + } + if (this.html_bulk_mode) { + this.html_bulk_mode.addEventListener("change", ((bricksocket) => (e) => { + if (e.target.checked) bricksocket.update_add_mode('bulk'); + })(this)); + } + if (this.html_lot_mode) { + this.html_lot_mode.addEventListener("change", ((bricksocket) => (e) => { + if (e.target.checked) bricksocket.update_add_mode('lot'); + })(this)); + } + + // Clear cart button + if (this.html_clear_cart) { + this.html_clear_cart.addEventListener("click", ((bricksocket) => (e) => { + bricksocket.clear_cart(); + })(this)); + } + + // Complete lot button + if (this.html_complete_lot) { + this.html_complete_lot.addEventListener("click", ((bricksocket) => (e) => { + bricksocket.complete_lot(); + })(this)); + } + + // CSV file input + this.html_csv_file = document.getElementById(`${id}-csv-file`); + this.html_import_csv = document.getElementById(`${id}-import-csv`); + + if (this.html_csv_file) { + this.html_csv_file.addEventListener("change", ((bricksocket) => (e) => { + // Enable/disable import button based on file selection + if (bricksocket.html_import_csv) { + bricksocket.html_import_csv.disabled = !e.target.files || e.target.files.length === 0; + } + })(this)); + } + + if (this.html_import_csv) { + this.html_import_csv.addEventListener("click", ((bricksocket) => () => { + bricksocket.import_csv(); + })(this)); + } + + // Setup the socket + this.setup(); + } + + // Clear form + clear() { + super.clear(); + + if (this.html_colors_section) { + this.html_colors_section.classList.add("d-none"); + } + + if (this.html_colors_grid) { + this.html_colors_grid.innerHTML = ''; + } + + if (this.html_metadata_section) { + this.html_metadata_section.classList.add("d-none"); + } + + this.current_part = null; + this.current_part_name = null; + this.current_colors = null; + this.selected_color = null; + } + + // Look up part and load available colors + lookup_part() { + if (this.disabled) { + return; + } + + const part = this.html_input.value.trim(); + + if (!part) { + this.fail({ message: 'Please enter a part number' }); + return; + } + + // Clear previous results + this.clear(); + + this.clear_status(); + this.toggle(false); + this.spinner(true); + + console.log('Emitting LOAD_PART_COLORS event with part:', part); + this.socket.emit(this.messages.LOAD_PART_COLORS, { part: part }); + } + + // Create a color card element + create_color_card(color) { + const col = document.createElement('div'); + col.className = 'col'; + + const card = document.createElement('div'); + card.className = 'card h-100'; + + // Card image + const imgContainer = document.createElement('div'); + imgContainer.className = 'card-img-top'; + imgContainer.style.height = '150px'; + imgContainer.style.backgroundImage = `url(${color.part_img_url || ''})`; + imgContainer.style.backgroundSize = 'contain'; + imgContainer.style.backgroundRepeat = 'no-repeat'; + imgContainer.style.backgroundPosition = 'center'; + + const img = document.createElement('img'); + img.src = color.part_img_url || ''; + img.alt = color.color_name; + img.className = 'd-none'; + img.loading = 'lazy'; + imgContainer.appendChild(img); + + // Card body + const cardBody = document.createElement('div'); + cardBody.className = 'card-body p-2'; + + const colorName = document.createElement('h6'); + colorName.className = 'card-title mb-1'; + colorName.textContent = color.color_name; + + const colorId = document.createElement('small'); + colorId.className = 'text-muted'; + colorId.textContent = `ID: ${color.color_id}`; + + cardBody.appendChild(colorName); + cardBody.appendChild(colorId); + + // Card footer with quantity input and add button + const cardFooter = document.createElement('div'); + cardFooter.className = 'card-footer p-2'; + + const inputGroup = document.createElement('div'); + inputGroup.className = 'input-group input-group-sm'; + + const quantityInput = document.createElement('input'); + quantityInput.type = 'number'; + quantityInput.className = 'form-control'; + quantityInput.placeholder = 'Qty'; + quantityInput.value = '1'; + quantityInput.min = '1'; + quantityInput.id = `qty-${color.color_id}`; + + const addButton = document.createElement('button'); + addButton.className = 'btn btn-primary'; + addButton.innerHTML = ' Add'; + addButton.onclick = ((bricksocket, colorData, qtyInput) => () => { + bricksocket.add_part_with_color(colorData, parseInt(qtyInput.value) || 1); + })(this, color, quantityInput); + + inputGroup.appendChild(quantityInput); + inputGroup.appendChild(addButton); + cardFooter.appendChild(inputGroup); + + // Assemble card + card.appendChild(imgContainer); + card.appendChild(cardBody); + card.appendChild(cardFooter); + col.appendChild(card); + + return col; + } + + // Add part with selected color + add_part_with_color(color, quantity) { + if (this.disabled) { + return; + } + + console.log('Adding part with color:', color, 'quantity:', quantity); + + // Clear previous status messages + this.clear_status(); + + // If in bulk or lot mode, add to cart instead of adding immediately + if (this.add_mode === 'bulk' || this.add_mode === 'lot') { + this.add_to_cart(color, quantity); + return; + } + + // Collect owners + const owners = []; + if (this.html_owners) { + this.html_owners.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => { + owners.push(cb.value); + }); + } + + // Collect tags + const tags = []; + if (this.html_tags) { + this.html_tags.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => { + tags.push(cb.value); + }); + } + + const data = { + part: this.current_part, + part_name: this.current_part_name, + color: color.color_id, + color_info: color, + quantity: quantity, + description: '', // No description field in this UI + owners: owners, + tags: tags, + storage: this.html_storage ? this.html_storage.value : null, + purchase_location: this.html_purchase_location ? this.html_purchase_location.value : null, + purchase_date: this.html_purchase_date ? this.html_purchase_date.value : null, + purchase_price: this.html_purchase_price ? parseFloat(this.html_purchase_price.value) : null + }; + + this.clear_status(); + this.toggle(false); + this.spinner(true); + + console.log('Emitting LOAD_PART event with data:', data); + this.socket.emit(this.messages.LOAD_PART, data); + } + + // Update add mode (single, bulk, or lot) + update_add_mode(mode) { + this.add_mode = mode; + + // Show/hide cart section based on mode + if (this.html_cart_section) { + if (mode === 'bulk' || mode === 'lot') { + this.html_cart_section.classList.remove("d-none"); + } else { + this.html_cart_section.classList.add("d-none"); + // Clear cart when switching to single mode + this.clear_cart(); + } + } + + // Update button text based on mode + if (this.html_complete_button_text) { + if (mode === 'bulk') { + this.html_complete_button_text.textContent = 'Add All Parts'; + } else if (mode === 'lot') { + this.html_complete_button_text.textContent = 'Complete Lot & Add All Parts'; + } + } + } + + // Add part to cart + add_to_cart(color, quantity) { + const cart_item = { + part: this.current_part, + part_name: this.current_part_name, + color_id: color.color_id, + color_name: color.color_name, + quantity: quantity, + color_info: color + }; + + this.cart.push(cart_item); + this.render_cart(); + this.progress_message(`Added ${this.current_part} in ${color.color_name} to cart (${this.cart.length} items)`); + } + + // Remove item from cart + remove_from_cart(index) { + this.cart.splice(index, 1); + this.render_cart(); + } + + // Clear entire cart + clear_cart() { + this.cart = []; + this.render_cart(); + this.clear_status(); + } + + // Render cart items + render_cart() { + if (!this.html_cart_items) return; + + this.html_cart_items.innerHTML = ''; + + if (this.cart.length === 0) { + this.html_cart_items.innerHTML = '

Cart is empty

'; + if (this.html_complete_lot) { + this.html_complete_lot.disabled = true; + } + } else { + this.cart.forEach((item, index) => { + const cartItem = document.createElement('div'); + cartItem.className = 'card mb-2'; + + const cardBody = document.createElement('div'); + cardBody.className = 'card-body p-2 d-flex justify-content-between align-items-center'; + + const info = document.createElement('div'); + info.innerHTML = ` + ${item.part} - ${item.part_name}
+ Color: ${item.color_name}, Qty: ${item.quantity} + `; + + const removeBtn = document.createElement('button'); + removeBtn.className = 'btn btn-sm btn-outline-danger'; + removeBtn.innerHTML = ''; + removeBtn.onclick = () => this.remove_from_cart(index); + + cardBody.appendChild(info); + cardBody.appendChild(removeBtn); + cartItem.appendChild(cardBody); + this.html_cart_items.appendChild(cartItem); + }); + + if (this.html_complete_lot) { + this.html_complete_lot.disabled = false; + } + } + + // Update cart count badge + if (this.html_cart_count) { + this.html_cart_count.textContent = this.cart.length; + } + } + + // Complete lot and add all parts + complete_lot() { + if (this.cart.length === 0) { + this.fail({message: 'Cart is empty. Add parts before completing the lot.'}); + return; + } + + this.clear_status(); + this.toggle(false); + this.spinner(true); + + // Prepare cart data - convert to format expected by backend + const cart_data = this.cart.map(item => ({ + part: item.part, + part_name: item.part_name, + color_id: item.color_id, + color_name: item.color_name, + quantity: item.quantity, + color_info: item.color_info + })); + + // Gather metadata from form + const data = { + cart: cart_data, + name: null, // Could add optional lot name field + description: null, // Could add optional lot description field + storage: this.html_storage ? (this.html_storage.value || '') : '', + purchase_location: this.html_purchase_location ? (this.html_purchase_location.value || '') : '', + purchase_date: this.html_purchase_date && this.html_purchase_date.value ? new Date(this.html_purchase_date.value).getTime() / 1000 : null, + purchase_price: this.html_purchase_price && this.html_purchase_price.value ? parseFloat(this.html_purchase_price.value) : null, + owners: this.html_owners ? Array.from(this.html_owners.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value) : [], + tags: this.html_tags ? Array.from(this.html_tags.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value) : [] + }; + + // Emit different event based on mode + if (this.add_mode === 'bulk') { + // Bulk mode: add individual parts (no lot) + this.socket.emit(this.messages.CREATE_BULK_INDIVIDUAL_PARTS, data); + } else if (this.add_mode === 'lot') { + // Lot mode: create lot with parts + this.socket.emit(this.messages.CREATE_LOT, data); + } + } + + // Import CSV file and add parts to cart + import_csv() { + if (!this.html_csv_file || !this.html_csv_file.files || this.html_csv_file.files.length === 0) { + this.fail({message: 'Please select a CSV file to import'}); + return; + } + + const file = this.html_csv_file.files[0]; + const reader = new FileReader(); + + reader.onload = ((bricksocket) => (e) => { + try { + const text = e.target.result; + const lines = text.split('\n').map(line => line.trim()).filter(line => line.length > 0); + + if (lines.length === 0) { + bricksocket.fail({message: 'CSV file is empty'}); + return; + } + + // Parse CSV header + const header = lines[0].split(',').map(h => h.trim()); + const partIndex = header.findIndex(h => h.toLowerCase() === 'part'); + const colorIndex = header.findIndex(h => h.toLowerCase() === 'color'); + const quantityIndex = header.findIndex(h => h.toLowerCase() === 'quantity'); + + if (partIndex === -1 || colorIndex === -1 || quantityIndex === -1) { + bricksocket.fail({message: 'CSV must have columns: Part, Color, Quantity'}); + return; + } + + // Parse rows + const parts = []; + for (let i = 1; i < lines.length; i++) { + const values = lines[i].split(',').map(v => v.trim()); + + if (values.length < 3) continue; // Skip incomplete rows + + const part = values[partIndex]; + const color = parseInt(values[colorIndex]); + const quantity = parseInt(values[quantityIndex]); + + if (part && !isNaN(color) && !isNaN(quantity) && quantity > 0) { + parts.push({part, color, quantity}); + } + } + + if (parts.length === 0) { + bricksocket.fail({message: 'No valid parts found in CSV file'}); + return; + } + + // Enable lot mode if not already in bulk/lot mode + if (bricksocket.add_mode === 'single') { + if (bricksocket.html_lot_mode) { + bricksocket.html_lot_mode.checked = true; + bricksocket.update_add_mode('lot'); + } + } + + // Clear previous status and progress + bricksocket.clear_status(); + bricksocket.progress_message(`Importing ${parts.length} parts from CSV...`); + + // Process each part + bricksocket.import_csv_parts(parts, 0); + + } catch (error) { + bricksocket.fail({message: `Error parsing CSV: ${error.message}`}); + } + })(this); + + reader.onerror = ((bricksocket) => () => { + bricksocket.fail({message: 'Error reading CSV file'}); + })(this); + + reader.readAsText(file); + } + + // Import CSV parts one by one + import_csv_parts(parts, index) { + if (index >= parts.length) { + // All parts processed - clear CSV import state + this.csv_import_parts = null; + this.csv_import_index = undefined; + + // Final cart render + this.render_cart(); + + // Show metadata section + if (this.html_metadata_section) { + this.html_metadata_section.classList.remove('d-none'); + } + + // Set progress bar to 100% + if (this.html_progress_bar) { + this.html_progress.setAttribute('aria-valuenow', '100'); + this.html_progress_bar.setAttribute('style', 'width: 100%'); + this.html_progress_bar.textContent = '100%'; + } + + // Show success message + this.clear_status(); + this.spinner(false); + this.toggle(true); + + const successDiv = document.getElementById('add-part-complete'); + if (successDiv) { + successDiv.textContent = `Successfully imported ${parts.length} parts to cart! You can now apply metadata and click "Complete Lot & Add All Parts".`; + successDiv.classList.remove('d-none'); + } + + // Clear the file input + if (this.html_csv_file) { + this.html_csv_file.value = ''; + } + if (this.html_import_csv) { + this.html_import_csv.disabled = true; + } + + return; + } + + const part = parts[index]; + + // Fetch colors for this part (backend will handle image downloads) + this.socket.emit(this.messages.LOAD_PART_COLORS, {part: part.part}); + + // Store CSV import state + this.csv_import_parts = parts; + this.csv_import_index = index; + } + + // Override part_colors_loaded to handle CSV import + part_colors_loaded(data) { + // Check if we're in CSV import mode + if (this.csv_import_parts && this.csv_import_index !== undefined) { + const parts = this.csv_import_parts; + const index = this.csv_import_index; + const part = parts[index]; + + // Find the color in the loaded colors + const color = data.colors.find(c => c.color_id === part.color); + + if (color) { + // Add to cart (silently, without rendering each time for performance) + const cart_item = { + part: data.part, + part_name: data.part_name, + color_id: color.color_id, + color_name: color.color_name, + quantity: part.quantity, + color_info: color + }; + + this.cart.push(cart_item); + + // Only render cart at the end or every 5 parts for performance + if ((index + 1) % 5 === 0 || (index + 1) === parts.length) { + this.render_cart(); + } + + // Update progress bar manually + const progress = ((index + 1) / parts.length) * 100; + if (this.html_progress_bar) { + this.html_progress.setAttribute('aria-valuenow', progress); + this.html_progress_bar.setAttribute('style', `width: ${progress}%`); + this.html_progress_bar.textContent = `${progress.toFixed(0)}%`; + } + + // Update simple status without showing the backend progress + this.clear_status(); + this.progress_message(`Added ${index + 1}/${parts.length} parts to cart`); + + // Process next part + this.import_csv_parts(parts, index + 1); + } else { + this.fail({message: `Color ${part.color} not found for part ${part.part}. Import stopped at part ${index + 1}/${parts.length}.`}); + this.csv_import_parts = null; + this.csv_import_index = undefined; + } + + return; + } + + // Normal part colors loaded flow + console.log('Received part colors:', data); + + this.current_part = data.part; + this.current_part_name = data.part_name; + this.current_colors = data.colors; + + // Show the colors section + if (this.html_colors_section) { + this.html_colors_section.classList.remove("d-none"); + } + + // Render color cards + if (this.html_colors_grid && this.current_colors) { + this.html_colors_grid.innerHTML = ''; + + this.current_colors.forEach((color) => { + const card = this.create_color_card(color); + this.html_colors_grid.appendChild(card); + }); + } + + // Show metadata section + if (this.html_metadata_section) { + this.html_metadata_section.classList.remove("d-none"); + } + + // Set progress bar to 100% for single part lookup + if (this.html_progress_bar) { + this.html_progress.setAttribute('aria-valuenow', '100'); + this.html_progress_bar.setAttribute('style', 'width: 100%'); + this.html_progress_bar.textContent = '100%'; + } + + this.spinner(false); + this.toggle(true); + this.progress_message(`Found ${data.count} colors for ${this.current_part_name} (${this.current_part})`); + } + + // Override progress to suppress backend progress updates during CSV import + progress(data={}) { + // Ignore backend progress updates when importing CSV + if (this.csv_import_parts && this.csv_import_index !== undefined) { + return; + } + + // Otherwise, use the default progress behavior + super.progress(data); + } + + // Setup socket listeners + setup() { + super.setup(); + + if (this.socket) { + // Listen for part colors loaded + this.socket.on(this.messages.PART_COLORS_LOADED, ((bricksocket) => (data) => { + bricksocket.part_colors_loaded(data); + })(this)); + } + } + + // Override complete to clear the form but keep success message + complete(data) { + // Custom success display with green alert box + if (this.html_progress_bar) { + this.html_progress.setAttribute("aria-valuenow", "100"); + this.html_progress_bar.setAttribute("style", "width: 100%"); + this.html_progress_bar.textContent = "100%"; + } + + this.spinner(false); + + // Show success message in green alert box + if (this.html_complete) { + this.html_complete.classList.remove("d-none"); + this.html_complete.innerHTML = `Success: ${data.message}`; + } + + if (this.html_fail) { + this.html_fail.classList.add("d-none"); + } + + // If lot was created, clear the cart (but don't clear status message) + if (data && (data.lot_id || data.parts_added)) { + // Clear cart without clearing status + this.cart = []; + this.render_cart(); + + // Reset to single mode after successful add + if (this.html_single_mode) { + this.html_single_mode.checked = true; + // Don't call update_add_mode('single') as it would call clear_cart() again + // Just update the mode and hide cart section manually + this.add_mode = 'single'; + if (this.html_cart_section) { + this.html_cart_section.classList.add("d-none"); + } + } + } + + // Clear the form after successful add (but don't call this.clear() as it clears status) + if (this.html_input) { + this.html_input.value = ''; + } + + // Hide color selection section without clearing status + if (this.html_colors_section) { + this.html_colors_section.classList.add("d-none"); + } + + if (this.html_colors_grid) { + this.html_colors_grid.innerHTML = ''; + } + + if (this.html_metadata_section) { + this.html_metadata_section.classList.add("d-none"); + } + + // Clear state + this.current_part = null; + this.current_part_name = null; + this.current_colors = null; + this.selected_color = null; + + // Uncheck all metadata + if (this.html_owners) { + this.html_owners.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false); + } + if (this.html_tags) { + this.html_tags.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false); + } + if (this.html_storage) { + this.html_storage.value = ''; + } + if (this.html_purchase_location) { + this.html_purchase_location.value = ''; + } + if (this.html_purchase_date) { + this.html_purchase_date.value = ''; + } + if (this.html_purchase_price) { + this.html_purchase_price.value = ''; + } + } +} diff --git a/static/scripts/minifigures.js b/static/scripts/minifigures.js index 8924cb4..4889127 100644 --- a/static/scripts/minifigures.js +++ b/static/scripts/minifigures.js @@ -93,6 +93,9 @@ document.addEventListener("DOMContentLoaded", () => { // Initialize collapsible states (filter and sort) initializeCollapsibleStates(); + // Initialize individuals filter + initializeIndividualsFilter(); + if (searchInput && searchClear) { if (isPaginationMode()) { // PAGINATION MODE - Server-side search @@ -174,7 +177,7 @@ document.addEventListener("DOMContentLoaded", () => { const clearButton = document.getElementById('table-filter-clear'); if (clearButton) { clearButton.addEventListener('click', () => { - window.clearPageFilters('minifigures', ['owner', 'problems', 'theme', 'year']); + window.clearPageFilters('minifigures', ['owner', 'problems', 'theme', 'year', 'individuals']); }); } }); @@ -190,4 +193,51 @@ function setupSortButtons() { }; // Use shared sort buttons setup from collapsible-state.js window.setupSharedSortButtons('minifigures', 'brickTableInstance', columnMap); +} + +// Initialize individuals filter functionality +function initializeIndividualsFilter() { + const individualsFilterButton = document.getElementById('individuals-filter-toggle'); + if (!individualsFilterButton) return; + + // Check if the filter should be active from URL parameters + const urlParams = new URLSearchParams(window.location.search); + const isIndividualsFilterActive = urlParams.get('individuals') === 'only'; + + // Set initial button state + if (isIndividualsFilterActive) { + individualsFilterButton.classList.remove('btn-outline-secondary'); + individualsFilterButton.classList.add('btn-secondary'); + } + + individualsFilterButton.addEventListener('click', () => { + const isCurrentlyActive = individualsFilterButton.classList.contains('btn-secondary'); + const newState = !isCurrentlyActive; + + // Update button appearance + if (newState) { + individualsFilterButton.classList.remove('btn-outline-secondary'); + individualsFilterButton.classList.add('btn-secondary'); + } else { + individualsFilterButton.classList.remove('btn-secondary'); + individualsFilterButton.classList.add('btn-outline-secondary'); + } + + // Update URL parameter and reload + const currentUrl = new URL(window.location); + + if (newState) { + currentUrl.searchParams.set('individuals', 'only'); + } else { + currentUrl.searchParams.delete('individuals'); + } + + // Reset to page 1 when filtering in server-side pagination mode + if (isPaginationMode()) { + currentUrl.searchParams.set('page', '1'); + } + + // Navigate to updated URL + window.location.href = currentUrl.toString(); + }); } \ No newline at end of file diff --git a/static/scripts/parts.js b/static/scripts/parts.js index 95d5cb9..4ea5033 100644 --- a/static/scripts/parts.js +++ b/static/scripts/parts.js @@ -1,10 +1,63 @@ // Parts page functionality - now uses shared functions +// Check if we're in pagination mode (server-side) or original mode (client-side) +function isPaginationMode() { + const tableElement = document.querySelector('#parts'); + return tableElement && tableElement.getAttribute('data-table') === 'false'; +} + // Keep filters expanded after selection function applyFiltersAndKeepOpen() { window.applyFiltersAndKeepState('parts', 'parts-filter-state'); } +// Initialize individuals filter functionality +function initializeIndividualsFilter() { + const individualsFilterButton = document.getElementById('individuals-filter-toggle'); + if (!individualsFilterButton) return; + + // Check if the filter should be active from URL parameters + const urlParams = new URLSearchParams(window.location.search); + const isIndividualsFilterActive = urlParams.get('individuals') === 'only'; + + // Set initial button state + if (isIndividualsFilterActive) { + individualsFilterButton.classList.remove('btn-outline-secondary'); + individualsFilterButton.classList.add('btn-secondary'); + } + + individualsFilterButton.addEventListener('click', () => { + const isCurrentlyActive = individualsFilterButton.classList.contains('btn-secondary'); + const newState = !isCurrentlyActive; + + // Update button appearance + if (newState) { + individualsFilterButton.classList.remove('btn-outline-secondary'); + individualsFilterButton.classList.add('btn-secondary'); + } else { + individualsFilterButton.classList.remove('btn-secondary'); + individualsFilterButton.classList.add('btn-outline-secondary'); + } + + // Update URL parameter and reload + const currentUrl = new URL(window.location); + + if (newState) { + currentUrl.searchParams.set('individuals', 'only'); + } else { + currentUrl.searchParams.delete('individuals'); + } + + // Reset to page 1 when filtering in server-side pagination mode + if (isPaginationMode()) { + currentUrl.searchParams.set('page', '1'); + } + + // Navigate to updated URL + window.location.href = currentUrl.toString(); + }); +} + // Initialize parts page document.addEventListener("DOMContentLoaded", () => { // Use shared table page initialization @@ -24,11 +77,14 @@ document.addEventListener("DOMContentLoaded", () => { hasColorDropdown: true }); + // Initialize individuals filter + initializeIndividualsFilter(); + // Initialize clear filters button const clearButton = document.getElementById('table-filter-clear'); if (clearButton) { clearButton.addEventListener('click', () => { - window.clearPageFilters('parts', ['owner', 'color', 'theme', 'year']); + window.clearPageFilters('parts', ['owner', 'color', 'theme', 'year', 'individuals']); }); } }); diff --git a/static/scripts/quick-add-individual-part.js b/static/scripts/quick-add-individual-part.js new file mode 100644 index 0000000..4bac71d --- /dev/null +++ b/static/scripts/quick-add-individual-part.js @@ -0,0 +1,127 @@ +// Quick Add Individual Part from Set Parts Table +// Handles the modal popup and form submission for adding individual parts + +document.addEventListener('DOMContentLoaded', function() { + // Get modal and form elements + const modal = document.getElementById('quickAddIndividualPartModal'); + if (!modal) return; // Modal only exists on set details page + + const bsModal = new bootstrap.Modal(modal); + const form = document.getElementById('quickAddForm'); + const submitBtn = document.getElementById('quickAddSubmit'); + + // Handle click on quick-add buttons/links + document.addEventListener('click', function(e) { + const trigger = e.target.closest('.quick-add-individual-part'); + if (!trigger) return; + + // Prevent default link behavior if it's a dropdown item + e.preventDefault(); + + // Get part data from trigger attributes + const partNumber = trigger.dataset.part; + const colorId = trigger.dataset.color; + const partName = trigger.dataset.name; + const colorName = trigger.dataset.colorName; + const imageUrl = trigger.dataset.image; + + // Populate modal with part info + document.getElementById('quickAddPartImage').src = imageUrl; + document.getElementById('quickAddPartName').textContent = partName; + document.getElementById('quickAddPartNumber').textContent = `Part: ${partNumber}`; + document.getElementById('quickAddPartColor').textContent = `Color: ${colorName}`; + document.getElementById('quickAddPart').value = partNumber; + document.getElementById('quickAddColor').value = colorId; + + // Reset form fields + document.getElementById('quickAddQuantity').value = 1; + document.getElementById('quickAddStorage').value = ''; + document.getElementById('quickAddPurchaseLocation').value = ''; + document.getElementById('quickAddDescription').value = ''; + + // Show modal + bsModal.show(); + }); + + // Handle form submission + submitBtn.addEventListener('click', async function() { + if (!form.checkValidity()) { + form.reportValidity(); + return; + } + + // Disable submit button to prevent double-submit + submitBtn.disabled = true; + submitBtn.innerHTML = 'Adding...'; + + // Collect form data + const formData = { + part: document.getElementById('quickAddPart').value, + color: document.getElementById('quickAddColor').value, + quantity: parseInt(document.getElementById('quickAddQuantity').value), + storage: document.getElementById('quickAddStorage').value || null, + purchase_location: document.getElementById('quickAddPurchaseLocation').value || null, + description: document.getElementById('quickAddDescription').value || null + }; + + try { + // Submit to backend + const response = await fetch('/individual-parts/quick-add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(formData) + }); + + const result = await response.json(); + + if (response.ok) { + // Success - show success message and close modal + bsModal.hide(); + + // Show success notification + showNotification('success', `Added ${formData.quantity}x ${document.getElementById('quickAddPartName').textContent} to individual parts inventory`); + + // Optionally reload the page or update UI + // window.location.reload(); + } else { + // Error from server + showNotification('error', result.error || 'Failed to add part to inventory'); + submitBtn.disabled = false; + submitBtn.innerHTML = 'Add to Inventory'; + } + } catch (error) { + // Network or other error + console.error('Error adding individual part:', error); + showNotification('error', 'Network error. Please try again.'); + submitBtn.disabled = false; + submitBtn.innerHTML = 'Add to Inventory'; + } + }); + + // Reset submit button when modal is hidden + modal.addEventListener('hidden.bs.modal', function() { + submitBtn.disabled = false; + submitBtn.innerHTML = 'Add to Inventory'; + }); +}); + +// Helper function to show notifications +function showNotification(type, message) { + // Create toast/alert element + const toast = document.createElement('div'); + toast.className = `alert alert-${type === 'success' ? 'success' : 'danger'} alert-dismissible fade show position-fixed`; + toast.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;'; + toast.innerHTML = ` + ${message} + + `; + + document.body.appendChild(toast); + + // Auto-remove after 5 seconds + setTimeout(() => { + toast.remove(); + }, 5000); +} diff --git a/templates/add.html b/templates/add.html index 368424e..0c35113 100644 --- a/templates/add.html +++ b/templates/add.html @@ -6,10 +6,24 @@ {% block main %}
- {% if not bulk and not config['HIDE_ADD_BULK_SET'] %} - +{% if bulk %} {% with id='add', bulk=bulk %} {% include 'set/socket.html' %} {% endwith %} +{% endif %} {% endblock %} diff --git a/templates/add_parts.html b/templates/add_parts.html new file mode 100644 index 0000000..e4b0302 --- /dev/null +++ b/templates/add_parts.html @@ -0,0 +1,206 @@ +{% import 'macro/accordion.html' as accordion %} + +{% extends 'base.html' %} + +{% block title %} - Add individual parts{% endblock %} + +{% block main %} +
+
+
+
+
+
Add individual parts
+
+
+ + + +
+ + +
Enter the Rebrickable part number (e.g., 3001, 3622, etc.)
+
+ +
+ +
+ + +
Each part is added to your inventory as soon as you select it
+
+
+ + +
Parts are added to a cart and saved together as individual parts (no lot created)
+
+
+ + +
Parts are added to a cart and saved together as a lot
+
+
+ + +
+ +
+ + +
+
Upload a CSV file with columns: Part, Color, Quantity (automatically enables Lot Mode)
+
+ + +
+
+ Cart + 0 +
+
+ +
+
+ + +
+
+ +
+
+

+ Progress + + + Loading... + +

+
+
+
+

+
+ + +
+
Select Color
+
+ +
+
+ + +
+
Metadata
+
+ {% if not (brickset_owners | length) and not (brickset_purchase_locations | length) and not (brickset_storages | length) and not (brickset_tags | length) %} + + {% else %} + {% if brickset_owners | length %} + {{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }} +
+ {% for owner in brickset_owners %} + {% with id=owner.as_dataset() %} +
+ + +
+ {% endwith %} + {% endfor %} +
+ {{ accordion.footer() }} + {% endif %} + + {% if brickset_purchase_locations | length %} + {{ accordion.header('Purchase location', 'purchase-location', 'metadata', icon='building-line') }} + +
+ +
+ {{ accordion.footer() }} + {% endif %} + + {% if brickset_storages | length %} + {{ accordion.header('Storage', 'storage', 'metadata', icon='archive-line') }} + +
+ +
+ {{ accordion.footer() }} + {% endif %} + + {% if brickset_tags | length %} + {{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-3-line') }} +
+ {% for tag in brickset_tags %} + {% with id=tag.as_dataset() %} +
+ + +
+ {% endwith %} + {% endfor %} +
+ {{ accordion.footer() }} + {% endif %} + + {% if brickset_purchase_locations | length %} + {{ accordion.header('Purchase details', 'purchase', 'metadata', icon='money-dollar-circle-line') }} +
+ + +
+
+ + +
+ {{ accordion.footer() }} + {% endif %} + {% endif %} +
+
+
+ +
+
+
+
+{% endblock %} diff --git a/templates/admin/configuration.html b/templates/admin/configuration.html index 496d8f4..16e9a2a 100644 --- a/templates/admin/configuration.html +++ b/templates/admin/configuration.html @@ -164,6 +164,14 @@
+
+ + +
+
+ +
+ + +
@@ -252,6 +268,14 @@
Hide the "Checked" column in parts tables
+ +
+ + +
@@ -826,6 +850,24 @@
+
+
+ + +
+
+
+
+ + +
+
diff --git a/templates/part/card.html b/templates/part/card.html index a83a2f7..5a52f32 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -3,7 +3,26 @@ {% import 'macro/card.html' as card %}
- {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.part, icon='shapes-line') }} +
+
+ {{ badge.identifier(item.fields.part, icon='shapes-line', solo=solo, header=true) }} + {% if solo %} + {{ badge.color(item, header=true) }} + {{ badge.print(item, header=true) }} + {% endif %} + {{ item.fields.name }} +
+ {% if solo and g.login.is_authenticated() and not config.get('HIDE_INDIVIDUAL_PARTS', False) and not config.get('DISABLE_QUICK_ADD_INDIVIDUAL_PARTS', False) %} + + {% endif %} +
{{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.image_id, medium=true) }}
{% if not solo %} @@ -29,9 +48,82 @@ {{ accordion.cards(minifigures_using, 'Minifigures using this part', 'minifigures-using-inventory', 'part-details', 'minifigure/card.html', icon='group-line') }} {{ accordion.cards(minifigures_missing, 'Minifigures missing this part', 'minifigures-missing-inventory', 'part-details', 'minifigure/card.html', icon='question-line') }} {{ accordion.cards(minifigures_damaged, 'Minifigures with this part damaged', 'minifigures-damaged-inventory', 'part-details', 'minifigure/card.html', icon='error-warning-line') }} + {{ accordion.cards(individual_parts, 'Individual part instances', 'individual-parts-inventory', 'part-details', 'individual_part/card.html', icon='box-3-line') }} + {{ accordion.cards(individual_lots, 'Lot instances', 'individual-lots-inventory', 'part-details', 'individual_part/lot_card.html', icon='shopping-cart-line') }} {{ accordion.cards(different_color, 'Same part with a different color', 'different-color', 'part-details', 'part/card.html', icon='palette-line') }} {{ accordion.cards(similar_prints, 'Prints using the same base', 'similar-prints', 'part-details', 'part/card.html', icon='paint-brush-line') }}
{% endif %}
+ +{% if solo and g.login.is_authenticated() and not config.get('HIDE_INDIVIDUAL_PARTS', False) and not config.get('DISABLE_QUICK_ADD_INDIVIDUAL_PARTS', False) %} + + +{% endif %} diff --git a/templates/part/lot_table.html b/templates/part/lot_table.html new file mode 100644 index 0000000..5b756b9 --- /dev/null +++ b/templates/part/lot_table.html @@ -0,0 +1,45 @@ +{% import 'macro/form.html' as form %} +{% import 'macro/table.html' as table %} + +
+ + {{ table.header(color=true, quantity=true, sets=false, minifigures=false, checked=not read_only, hamburger_menu=not read_only, accordion_id=accordion_id|default('')) }} + + {% for item in table_collection %} + + {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.part, accordion=solo) }} + + + + {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + + {% endif %} + {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + + {% endif %} + {% if not config['HIDE_TABLE_CHECKED_PARTS'] and not read_only %} + + {% endif %} + {% if g.login.is_authenticated() and not read_only %} + {% set show_missing_menu = not config['HIDE_TABLE_MISSING_PARTS'] %} + {% set show_checked_menu = not config['HIDE_TABLE_CHECKED_PARTS'] %} + {% if show_missing_menu or show_checked_menu %} + + {% endif %} + {% endif %} + + {% endfor %} + +
+ {{ item.fields.name }} + + {% if item.fields.color_rgb %}{% endif %} + {{ item.fields.color_name }} + {{ item.fields.quantity }} + {{ form.input('Missing', item.fields.id, item.html_id('missing'), item.url_for_problem('missing'), item.fields.missing, all=false, read_only=read_only) }} + + {{ form.input('Damaged', item.fields.id, item.html_id('damaged'), item.url_for_problem('damaged'), item.fields.damaged, all=false, read_only=read_only) }} + +
{{ form.checkbox('', item.fields.id, item.html_id('checked'), item.url_for_checked(), item.fields.checked | default(false), parent='part', delete=read_only) }}
+
+
diff --git a/templates/part/table.html b/templates/part/table.html index 36146a7..c37670e 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -49,8 +49,29 @@ {% if g.login.is_authenticated() and not read_only %} {% set show_missing_menu = not config['HIDE_TABLE_MISSING_PARTS'] %} {% set show_checked_menu = not config['HIDE_TABLE_CHECKED_PARTS'] %} - {% if show_missing_menu or show_checked_menu %} - + {% set show_quick_add = not config['DISABLE_QUICK_ADD_INDIVIDUAL_PARTS'] and not config['HIDE_INDIVIDUAL_PARTS'] %} + {% if show_missing_menu or show_checked_menu or show_quick_add %} + + {% if show_quick_add %} + + {% endif %} + {% endif %} {% endif %} {% endif %} diff --git a/templates/parts.html b/templates/parts.html index c9d2d1a..c2645eb 100644 --- a/templates/parts.html +++ b/templates/parts.html @@ -24,6 +24,11 @@ + {% if not config.get('HIDE_INDIVIDUAL_PARTS', False) %} + + {% endif %} diff --git a/templates/purchase_location.html b/templates/purchase_location.html new file mode 100644 index 0000000..f8e4dc9 --- /dev/null +++ b/templates/purchase_location.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block title %} - Purchase Location {{ item.fields.name}}{% endblock %} + +{% block main %} +
+
+
+ {% with solo=true %} + {% include 'purchase_location/card.html' %} + {% endwith %} +
+
+
+{% endblock %} diff --git a/templates/purchase_location/card.html b/templates/purchase_location/card.html new file mode 100644 index 0000000..b9024f7 --- /dev/null +++ b/templates/purchase_location/card.html @@ -0,0 +1,34 @@ +{% import 'macro/accordion.html' as accordion with context %} +{% import 'macro/badge.html' as badge %} +{% import 'macro/card.html' as card %} + +
+ {{ card.header(item, item.fields.name, solo=solo, icon='map-pin-line') }} +
+ {{ badge.total_sets(sets | length, solo=solo, last=last) }} + {% if individual_minifigures | length %} + {{ badge.badge(check=individual_minifigures | length, solo=solo, last=last, color='info', icon='user-line', collapsible='Individual Minifigures:', text=individual_minifigures | length) }} + {% endif %} + {% if individual_parts | length %} + {{ badge.badge(check=individual_parts | length, solo=solo, last=last, color='success', icon='shapes-line', collapsible='Individual Parts:', text=individual_parts | length) }} + {% endif %} + {% if individual_part_lots | length %} + {{ badge.badge(check=individual_part_lots | length, solo=solo, last=last, color='warning', icon='hammer-line', collapsible='Part Lots:', text=individual_part_lots | length) }} + {% endif %} +
+ {% if solo %} +
+ {{ accordion.cards(sets, 'Sets', 'sets-purchased', 'purchase-location-details', 'set/card.html', expanded=true, icon='hashtag') }} + {% if individual_minifigures | length %} + {{ accordion.cards(individual_minifigures, 'Individual Minifigures', 'individual-minifigures-purchased', 'purchase-location-details', 'individual_minifigure/card.html', expanded=false, icon='user-line') }} + {% endif %} + {% if individual_parts | length %} + {{ accordion.cards(individual_parts, 'Individual Parts', 'individual-parts-purchased', 'purchase-location-details', 'individual_part/card.html', expanded=false, icon='shapes-line') }} + {% endif %} + {% if individual_part_lots | length %} + {{ accordion.cards(individual_part_lots, 'Individual Part Lots', 'individual-part-lots-purchased', 'purchase-location-details', 'individual_part/lot_card.html', expanded=false, icon='hammer-line') }} + {% endif %} +
+ + {% endif %} +
diff --git a/templates/purchase_location/table.html b/templates/purchase_location/table.html new file mode 100644 index 0000000..7256bcf --- /dev/null +++ b/templates/purchase_location/table.html @@ -0,0 +1,16 @@ +{% import 'macro/form.html' as form %} +{% import 'macro/table.html' as table %} + +
+ + {{ table.header(image=false, missing=false, damaged=false, sets=true) }} + + {% for item in table_collection %} + + + + + {% endfor %} + +
{{ item.fields.name }}{{ item.fields.total_sets }}
+
diff --git a/templates/purchase_locations.html b/templates/purchase_locations.html new file mode 100644 index 0000000..83fdeb8 --- /dev/null +++ b/templates/purchase_locations.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %} - All purchase locations{% endblock %} + +{% block main %} +
+ {% with all=true %} + {% include 'purchase_location/table.html' %} + {% endwith %} +
+{% endblock %} diff --git a/templates/set/card.html b/templates/set/card.html index d1af194..d651410 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -154,3 +154,74 @@ {% endif %} + +{% if solo and g.login.is_authenticated() %} + + +{% endif %} diff --git a/templates/set/socket.html b/templates/set/socket.html index c4000a8..97bcd33 100644 --- a/templates/set/socket.html +++ b/templates/set/socket.html @@ -11,6 +11,7 @@ LOAD_SET: '{{ messages['LOAD_SET'] }}', PROGRESS: '{{ messages['PROGRESS'] }}', SET_LOADED: '{{ messages['SET_LOADED'] }}', + MINIFIGURE_LOADED: '{{ messages['MINIFIGURE_LOADED'] }}', }, {% if bulk %}true{% else %}false{% endif %}, {% if refresh %}true{% else %}false{% endif %} diff --git a/templates/statistics.html b/templates/statistics.html index 0051231..4726d7d 100644 --- a/templates/statistics.html +++ b/templates/statistics.html @@ -273,7 +273,7 @@
- Sets by Storage + Storage
@@ -286,8 +286,8 @@ Storage Location Sets - Parts - Minifigures + Parts (lots, individual parts) + Minifigures (individual minifigures) Value @@ -295,7 +295,7 @@ {% for storage in storage_statistics %} - + {{ storage.storage_name or 'Unknown' }} @@ -303,10 +303,10 @@ {{ storage.set_count }} - {{ "{:,}".format(storage.total_parts) }} + {{ "{:,}".format(storage.total_parts) }}{% if storage.lot_count or storage.individual_part_count %} ({% if storage.lot_count %}{{ storage.lot_count }} lot{{ 's' if storage.lot_count != 1 else '' }}{% endif %}{% if storage.lot_count and storage.individual_part_count %}, {% endif %}{% if storage.individual_part_count %}{{ "{:,}".format(storage.individual_part_count) }} individual{% endif %}){% endif %} - {{ storage.total_minifigures }} + {{ storage.total_minifigures }}{% if storage.individual_minifig_count %} ({{ storage.individual_minifig_count }} individual){% endif %} {% if storage.total_value %} @@ -335,7 +335,7 @@
- Sets by Purchase Location + Purchase Location
@@ -359,7 +359,7 @@ {% for location in purchase_location_statistics %} - + {{ location.location_name or 'Unknown' }} diff --git a/templates/storage/card.html b/templates/storage/card.html index cf29de3..1545f2e 100644 --- a/templates/storage/card.html +++ b/templates/storage/card.html @@ -6,10 +6,28 @@ {{ card.header(item, item.fields.name, solo=solo, icon='archive-2-line') }}
{{ badge.total_sets(sets | length, solo=solo, last=last) }} + {% if individual_minifigures | length %} + {{ badge.badge(check=individual_minifigures | length, solo=solo, last=last, color='info', icon='user-line', collapsible='Individual Minifigures:', text=individual_minifigures | length) }} + {% endif %} + {% if individual_parts | length %} + {{ badge.badge(check=individual_parts | length, solo=solo, last=last, color='success', icon='shapes-line', collapsible='Individual Parts:', text=individual_parts | length) }} + {% endif %} + {% if individual_part_lots | length %} + {{ badge.badge(check=individual_part_lots | length, solo=solo, last=last, color='warning', icon='hammer-line', collapsible='Part Lots:', text=individual_part_lots | length) }} + {% endif %}
{% if solo %}
{{ accordion.cards(sets, 'Sets', 'sets-stored', 'storage-details', 'set/card.html', expanded=true, icon='hashtag') }} + {% if individual_minifigures | length %} + {{ accordion.cards(individual_minifigures, 'Individual Minifigures', 'individual-minifigures-stored', 'storage-details', 'individual_minifigure/card.html', expanded=false, icon='user-line') }} + {% endif %} + {% if individual_parts | length %} + {{ accordion.cards(individual_parts, 'Individual Parts', 'individual-parts-stored', 'storage-details', 'individual_part/card.html', expanded=false, icon='shapes-line') }} + {% endif %} + {% if individual_part_lots | length %} + {{ accordion.cards(individual_part_lots, 'Individual Part Lots', 'individual-part-lots-stored', 'storage-details', 'individual_part/lot_card.html', expanded=false, icon='hammer-line') }} + {% endif %}
{% endif %}