';
+ 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'] %}
-
-
Too many to add?
-
You can import multiple sets at once with Bulk add.
+ {% if not bulk and (not config['HIDE_ADD_BULK_SET'] or not config['HIDE_INDIVIDUAL_PARTS']) %}
+
+ {% if not config['HIDE_ADD_BULK_SET'] %}
+
+
+
Too many to add?
+
You can import multiple sets at once with Bulk add.
+
+
+ {% endif %}
+ {% if not config['HIDE_INDIVIDUAL_PARTS'] %}
+
+
+
Adding individual parts?
+
You can add standalone parts (not from a set) with Add parts.
+
+
+ {% endif %}
{% endif %}
@@ -26,8 +40,20 @@
{% endif %}
-
-
+
+
+
Sets: use format like 107-1{% if not config['DISABLE_INDIVIDUAL_MINIFIGURES'] %}. Minifigures: use format like fig-001234{% endif %}
@@ -141,7 +167,9 @@
+{% 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) %}
+
+ You have no metadata configured.
+ You can add entries in the Set metadata management section of the Admin panel.
+