feat(ui): add templates and scripts for individual items and bulk part addition
This commit is contained in:
@@ -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();
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -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 = '<i class="ri-add-line"></i> 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 = '<p class="text-muted text-center">Cart is empty</p>';
|
||||
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 = `
|
||||
<strong>${item.part}</strong> - ${item.part_name}<br>
|
||||
<small class="text-muted">Color: ${item.color_name}, Qty: ${item.quantity}</small>
|
||||
`;
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'btn btn-sm btn-outline-danger';
|
||||
removeBtn.innerHTML = '<i class="ri-delete-bin-line"></i>';
|
||||
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 = `<strong>Success:</strong> ${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 = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
+57
-1
@@ -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']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 = '<span class="spinner-border spinner-border-sm me-2"></span>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}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 5000);
|
||||
}
|
||||
+34
-6
@@ -6,10 +6,24 @@
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
{% if not bulk and not config['HIDE_ADD_BULK_SET'] %}
|
||||
<div class="alert alert-primary" role="alert">
|
||||
<h4 class="alert-heading">Too many to add?</h4>
|
||||
<p class="mb-0">You can import multiple sets at once with <a href="{{ url_for('add.bulk') }}" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</a>.</p>
|
||||
{% if not bulk and (not config['HIDE_ADD_BULK_SET'] or not config['HIDE_INDIVIDUAL_PARTS']) %}
|
||||
<div class="row g-3 mb-3">
|
||||
{% if not config['HIDE_ADD_BULK_SET'] %}
|
||||
<div class="col-12 {% if not config['HIDE_INDIVIDUAL_PARTS'] %}col-md-6{% endif %}">
|
||||
<div class="alert alert-primary mb-0 h-100" role="alert">
|
||||
<h4 class="alert-heading">Too many to add?</h4>
|
||||
<p class="mb-0">You can import multiple sets at once with <a href="{{ url_for('add.bulk') }}" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not config['HIDE_INDIVIDUAL_PARTS'] %}
|
||||
<div class="col-12 {% if not config['HIDE_ADD_BULK_SET'] %}col-md-6{% endif %}">
|
||||
<div class="alert alert-info mb-0 h-100" role="alert">
|
||||
<h4 class="alert-heading">Adding individual parts?</h4>
|
||||
<p class="mb-0">You can add standalone parts (not from a set) with <a href="{{ url_for('add.parts') }}" class="btn btn-info"><i class="ri-hammer-line"></i> Add parts</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
@@ -26,8 +40,20 @@
|
||||
<div id="add-complete"></div>
|
||||
{% endif %}
|
||||
<div class="mb-3">
|
||||
<label for="add-set" class="form-label">{% if not bulk %}Set number (only one){% else %}List of sets (separated by a comma){% endif %}</label>
|
||||
<input type="text" class="form-control" id="add-set" placeholder="{% if not bulk %}107-1 or 1642-1 or ...{% else %}107-1, 1642-1, ...{% endif %}">
|
||||
<label for="add-set" class="form-label">{% if not bulk %}{% if not config['DISABLE_INDIVIDUAL_MINIFIGURES'] %}Set or Minifigure number (only one){% else %}Set number (only one){% endif %}{% else %}List of sets (separated by a comma){% endif %}</label>
|
||||
<input type="text" class="form-control" id="add-set" placeholder="{% if not bulk %}{% if not config['DISABLE_INDIVIDUAL_MINIFIGURES'] %}107-1 or fig-001234 or ...{% else %}107-1 or ...{% endif %}{% else %}107-1, 1642-1, ...{% endif %}"
|
||||
data-path="{{ path }}"
|
||||
data-namespace="{{ namespace }}"
|
||||
data-msg-complete="{{ messages['COMPLETE'] }}"
|
||||
data-msg-fail="{{ messages['FAIL'] }}"
|
||||
data-msg-import-set="{{ messages['IMPORT_SET'] }}"
|
||||
data-msg-load-set="{{ messages['LOAD_SET'] }}"
|
||||
data-msg-progress="{{ messages['PROGRESS'] }}"
|
||||
data-msg-set-loaded="{{ messages['SET_LOADED'] }}"
|
||||
data-msg-import-minifigure="{{ messages['IMPORT_MINIFIGURE'] }}"
|
||||
data-msg-load-minifigure="{{ messages['LOAD_MINIFIGURE'] }}"
|
||||
data-msg-minifigure-loaded="{{ messages['MINIFIGURE_LOADED'] }}">
|
||||
<div class="form-text">Sets: use format like 107-1{% if not config['DISABLE_INDIVIDUAL_MINIFIGURES'] %}. Minifigures: use format like fig-001234{% endif %}</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="add-no-confirm" {% if bulk %}checked disabled{% endif %}>
|
||||
@@ -141,7 +167,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if bulk %}
|
||||
{% with id='add', bulk=bulk %}
|
||||
{% include 'set/socket.html' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
{% import 'macro/accordion.html' as accordion %}
|
||||
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %} - Add individual parts{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="ri-hammer-line"></i> Add individual parts</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="add-part-fail" class="alert alert-danger d-none" role="alert"></div>
|
||||
<div id="add-part-complete" class="alert alert-success d-none" role="alert"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="add-part-input" class="form-label">Part number</label>
|
||||
<input type="text" class="form-control" id="add-part-input" placeholder="3001" required
|
||||
data-path="{{ path }}"
|
||||
data-namespace="{{ namespace }}"
|
||||
data-msg-complete="{{ messages['COMPLETE'] }}"
|
||||
data-msg-create-lot="{{ messages['CREATE_LOT'] }}"
|
||||
data-msg-fail="{{ messages['FAIL'] }}"
|
||||
data-msg-load-part="{{ messages['LOAD_PART'] }}"
|
||||
data-msg-load-part-colors="{{ messages['LOAD_PART_COLORS'] }}"
|
||||
data-msg-part-colors-loaded="{{ messages['PART_COLORS_LOADED'] }}"
|
||||
data-msg-progress="{{ messages['PROGRESS'] }}"
|
||||
data-msg-part-loaded="{{ messages['PART_LOADED'] }}">
|
||||
<div class="form-text">Enter the Rebrickable part number (e.g., 3001, 3622, etc.)</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><strong>Add Mode</strong></label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="add-mode" id="add-part-single-mode" value="single" checked>
|
||||
<label class="form-check-label" for="add-part-single-mode">
|
||||
<strong>Single Mode</strong> - Add parts immediately one at a time
|
||||
</label>
|
||||
<div class="form-text">Each part is added to your inventory as soon as you select it</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="add-mode" id="add-part-bulk-mode" value="bulk">
|
||||
<label class="form-check-label" for="add-part-bulk-mode">
|
||||
<strong>Bulk Mode</strong> - Add multiple individual parts
|
||||
</label>
|
||||
<div class="form-text">Parts are added to a cart and saved together as individual parts (no lot created)</div>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="add-mode" id="add-part-lot-mode" value="lot">
|
||||
<label class="form-check-label" for="add-part-lot-mode">
|
||||
<strong>Lot Mode</strong> - Create a part lot
|
||||
</label>
|
||||
<div class="form-text">Parts are added to a cart and saved together as a lot</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CSV Import section -->
|
||||
<div class="mb-3">
|
||||
<label for="add-part-csv-file" class="form-label">Or import from Rebrickable CSV</label>
|
||||
<div class="input-group">
|
||||
<input type="file" class="form-control" id="add-part-csv-file" accept=".csv">
|
||||
<button id="add-part-import-csv" type="button" class="btn btn-secondary" disabled>
|
||||
<i class="ri-upload-line"></i> Import CSV
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">Upload a CSV file with columns: Part, Color, Quantity (automatically enables Lot Mode)</div>
|
||||
</div>
|
||||
|
||||
<!-- Cart section (only visible in lot mode) -->
|
||||
<div id="add-part-cart-section" class="d-none mb-3">
|
||||
<h6 class="border-bottom">
|
||||
<i class="ri-shopping-cart-line"></i> Cart
|
||||
<span class="badge bg-primary" id="add-part-cart-count">0</span>
|
||||
</h6>
|
||||
<div id="add-part-cart-items" class="mb-2">
|
||||
<!-- Cart items will be inserted here -->
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button id="add-part-complete-lot" type="button" class="btn btn-success" disabled>
|
||||
<i class="ri-check-line"></i> <span id="add-part-complete-button-text">Complete Lot & Add All Parts</span>
|
||||
</button>
|
||||
<button id="add-part-clear-cart" type="button" class="btn btn-outline-danger btn-sm">
|
||||
<i class="ri-delete-bin-line"></i> Clear Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<p>
|
||||
Progress <span id="add-part-count"></span>
|
||||
<span id="add-part-spinner" class="d-none">
|
||||
<span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
|
||||
<span class="visually-hidden" role="status">Loading...</span>
|
||||
</span>
|
||||
</p>
|
||||
<div id="add-part-progress" class="progress" role="progressbar" aria-label="Add part progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||
<div id="add-part-progress-bar" class="progress-bar" style="width: 0%"></div>
|
||||
</div>
|
||||
<p id="add-part-progress-message" class="text-center d-none"></p>
|
||||
</div>
|
||||
|
||||
<!-- Color selection section (hidden until part is loaded) -->
|
||||
<div id="add-part-colors-section" class="d-none">
|
||||
<h6 class="border-bottom mt-3 mb-3">Select Color</h6>
|
||||
<div id="add-part-colors-grid" class="row row-cols-2 row-cols-md-3 row-cols-lg-4 g-3">
|
||||
<!-- Color cards will be inserted here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata section (initially hidden, shown after selecting part) -->
|
||||
<div id="add-part-metadata-section" class="d-none">
|
||||
<h6 class="border-bottom mt-3">Metadata</h6>
|
||||
<div class="accordion accordion" id="metadata">
|
||||
{% if not (brickset_owners | length) and not (brickset_purchase_locations | length) and not (brickset_storages | length) and not (brickset_tags | length) %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
You have no metadata configured.
|
||||
You can add entries in the <a href="{{ url_for('admin.admin', open_metadata=true) }}" class="btn btn-warning" role="button"><i class="ri-profile-line"></i> Set metadata management</a> section of the Admin panel.
|
||||
</div>
|
||||
{% else %}
|
||||
{% if brickset_owners | length %}
|
||||
{{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }}
|
||||
<div id="add-part-owners">
|
||||
{% for owner in brickset_owners %}
|
||||
{% with id=owner.as_dataset() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="part-{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="part-{{ id }}">{{ owner.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
|
||||
{% if brickset_purchase_locations | length %}
|
||||
{{ accordion.header('Purchase location', 'purchase-location', 'metadata', icon='building-line') }}
|
||||
<label class="visually-hidden" for="add-part-purchase-location">Purchase location</label>
|
||||
<div class="input-group">
|
||||
<select id="add-part-purchase-location" class="form-select" autocomplete="off">
|
||||
<option value="" selected><i>None</i></option>
|
||||
{% for purchase_location in brickset_purchase_locations %}
|
||||
<option value="{{ purchase_location.fields.id }}">{{ purchase_location.fields.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
|
||||
{% if brickset_storages | length %}
|
||||
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-line') }}
|
||||
<label class="visually-hidden" for="add-part-storage">Storage</label>
|
||||
<div class="input-group">
|
||||
<select id="add-part-storage" class="form-select" autocomplete="off">
|
||||
<option value="" selected><i>None</i></option>
|
||||
{% for storage in brickset_storages %}
|
||||
<option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
|
||||
{% if brickset_tags | length %}
|
||||
{{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-3-line') }}
|
||||
<div id="add-part-tags">
|
||||
{% for tag in brickset_tags %}
|
||||
{% with id=tag.as_dataset() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ tag.fields.id }}" id="part-{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="part-{{ id }}">{{ tag.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
|
||||
{% if brickset_purchase_locations | length %}
|
||||
{{ accordion.header('Purchase details', 'purchase', 'metadata', icon='money-dollar-circle-line') }}
|
||||
<div class="mb-3">
|
||||
<label for="add-part-purchase-date" class="form-label">Purchase date</label>
|
||||
<input type="date" class="form-control" id="add-part-purchase-date">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="add-part-purchase-price" class="form-label">Purchase price</label>
|
||||
<input type="number" class="form-control" id="add-part-purchase-price" placeholder="0.00" step="0.01" min="0">
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-end">
|
||||
<span id="add-part-status-icon" class="me-1"></span><span id="add-part-status" class="me-1"></span>
|
||||
<button id="add-part-lookup" type="button" class="btn btn-primary"><i class="ri-search-line"></i> Look up part</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -164,6 +164,14 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_INDIVIDUAL_MINIFIGURES" data-var="BK_HIDE_INDIVIDUAL_MINIFIGURES" {{ is_locked('BK_HIDE_INDIVIDUAL_MINIFIGURES') }}>
|
||||
<label class="form-check-label" for="BK_HIDE_INDIVIDUAL_MINIFIGURES">
|
||||
BK_HIDE_INDIVIDUAL_MINIFIGURES {{ config_badges('BK_HIDE_INDIVIDUAL_MINIFIGURES') }}
|
||||
<div class="text-muted small">Hide individual minifigures page access</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_ALL_PARTS" data-var="BK_HIDE_ALL_PARTS" {{ is_locked('BK_HIDE_ALL_PARTS') }}>
|
||||
<label class="form-check-label" for="BK_HIDE_ALL_PARTS">
|
||||
@@ -171,6 +179,14 @@
|
||||
<div class="text-muted small">Hide the "Parts" menu entry</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input config-toggle" type="checkbox" id="BK_HIDE_INDIVIDUAL_PARTS" data-var="BK_HIDE_INDIVIDUAL_PARTS" {{ is_locked('BK_HIDE_INDIVIDUAL_PARTS') }}>
|
||||
<label class="form-check-label" for="BK_HIDE_INDIVIDUAL_PARTS">
|
||||
BK_HIDE_INDIVIDUAL_PARTS {{ config_badges('BK_HIDE_INDIVIDUAL_PARTS') }}
|
||||
<div class="text-muted small">Hide individual parts and part lots dropdown from Parts menu</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
@@ -252,6 +268,14 @@
|
||||
<div class="text-muted small">Hide the "Checked" column in parts tables</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input config-toggle" type="checkbox" id="BK_DISABLE_QUICK_ADD_INDIVIDUAL_PARTS" data-var="BK_DISABLE_QUICK_ADD_INDIVIDUAL_PARTS" {{ is_locked('BK_DISABLE_QUICK_ADD_INDIVIDUAL_PARTS') }}>
|
||||
<label class="form-check-label" for="BK_DISABLE_QUICK_ADD_INDIVIDUAL_PARTS">
|
||||
BK_DISABLE_QUICK_ADD_INDIVIDUAL_PARTS {{ config_badges('BK_DISABLE_QUICK_ADD_INDIVIDUAL_PARTS') }}
|
||||
<div class="text-muted small">Disable quick-add individual parts from set parts tables</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
@@ -826,6 +850,24 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input config-static-toggle" type="checkbox" id="static-BK_DISABLE_INDIVIDUAL_PARTS" data-var="BK_DISABLE_INDIVIDUAL_PARTS" {{ is_locked('BK_DISABLE_INDIVIDUAL_PARTS') }}>
|
||||
<label class="form-check-label" for="static-BK_DISABLE_INDIVIDUAL_PARTS">
|
||||
BK_DISABLE_INDIVIDUAL_PARTS {{ config_badges('BK_DISABLE_INDIVIDUAL_PARTS') }}
|
||||
<div class="text-muted small">Disable individual parts feature (blocks adding/editing, allows viewing existing)</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input config-static-toggle" type="checkbox" id="static-BK_DISABLE_INDIVIDUAL_MINIFIGURES" data-var="BK_DISABLE_INDIVIDUAL_MINIFIGURES" {{ is_locked('BK_DISABLE_INDIVIDUAL_MINIFIGURES') }}>
|
||||
<label class="form-check-label" for="static-BK_DISABLE_INDIVIDUAL_MINIFIGURES">
|
||||
BK_DISABLE_INDIVIDUAL_MINIFIGURES {{ config_badges('BK_DISABLE_INDIVIDUAL_MINIFIGURES') }}
|
||||
<div class="text-muted small">Disable individual minifigures feature (blocks adding/editing, allows viewing existing)</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="static-BK_DOMAIN_NAME" class="form-label">
|
||||
BK_DOMAIN_NAME {{ config_badges('BK_DOMAIN_NAME') }}
|
||||
|
||||
+102
-10
@@ -1,6 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-bs-theme="{{ 'dark' if config.get('DARK_MODE') in [True, 'True', 'true'] else 'light' }}">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BrickTracker{% block title %}{% endblock %}</title>
|
||||
@@ -28,14 +28,90 @@
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
{% for item in config['_NAVBAR'] %}
|
||||
{% if item.flag and not config[item.flag] %}
|
||||
<li class="nav-item px-1">
|
||||
<a {% if request.url_rule.endpoint == item.endpoint %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %} href="{{ url_for(item.endpoint) }}">
|
||||
{% if item.icon %}
|
||||
<i class="ri-{{ item.icon }}"></i>
|
||||
{% endif %}
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% if item.endpoint == 'add.add' %}
|
||||
{# Add menu item with optional dropdown for Bulk/Parts #}
|
||||
{% set has_bulk = not config['HIDE_ADD_BULK_SET'] %}
|
||||
{% set has_parts = not config['HIDE_INDIVIDUAL_PARTS'] %}
|
||||
{% if has_bulk or has_parts %}
|
||||
<li class="nav-item dropdown px-1">
|
||||
<a {% if request.url_rule.endpoint in ['add.add', 'add.bulk', 'add.parts'] %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %}
|
||||
href="{{ url_for(item.endpoint) }}">
|
||||
{% if item.icon %}
|
||||
<i class="ri-{{ item.icon }}"></i>
|
||||
{% endif %}
|
||||
{{ item.title }}
|
||||
</a>
|
||||
<a class="dropdown-toggle dropdown-toggle-split nav-link {% if request.url_rule.endpoint in ['add.add', 'add.bulk', 'add.parts'] %}active{% endif %}"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle dropdown</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
{% if has_bulk %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('add.bulk') }}"><i class="ri-function-add-line"></i> Bulk add</a></li>
|
||||
{% endif %}
|
||||
{% if has_parts %}
|
||||
<li><a class="dropdown-item" href="{{ url_for('add.parts') }}"><i class="ri-hammer-line"></i> Add parts</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
{# Just Add link, no dropdown #}
|
||||
<li class="nav-item px-1">
|
||||
<a {% if request.url_rule.endpoint == item.endpoint %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %} href="{{ url_for(item.endpoint) }}">
|
||||
{% if item.icon %}
|
||||
<i class="ri-{{ item.icon }}"></i>
|
||||
{% endif %}
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% elif item.endpoint == 'part.list' %}
|
||||
{# Parts menu item with optional dropdown for Individual Parts/Lots #}
|
||||
{% set has_individual_parts = not config.get('HIDE_INDIVIDUAL_PARTS', False) %}
|
||||
{% if has_individual_parts %}
|
||||
<li class="nav-item dropdown px-1">
|
||||
<a {% if request.url_rule.endpoint in ['part.list', 'individual_part.list', 'individual_part.list_lots'] %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %}
|
||||
href="{{ url_for(item.endpoint) }}">
|
||||
{% if item.icon %}
|
||||
<i class="ri-{{ item.icon }}"></i>
|
||||
{% endif %}
|
||||
{{ item.title }}
|
||||
</a>
|
||||
<a class="dropdown-toggle dropdown-toggle-split nav-link {% if request.url_rule.endpoint in ['part.list', 'individual_part.list', 'individual_part.list_lots'] %}active{% endif %}"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span class="visually-hidden">Toggle dropdown</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{{ url_for('individual_part.list') }}"><i class="ri-hammer-line"></i> Individual parts</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('individual_part.list_lots') }}"><i class="ri-stack-line"></i> Part lots</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% else %}
|
||||
{# Just Parts link, no dropdown #}
|
||||
<li class="nav-item px-1">
|
||||
<a {% if request.url_rule.endpoint == item.endpoint %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %} href="{{ url_for(item.endpoint) }}">
|
||||
{% if item.icon %}
|
||||
<i class="ri-{{ item.icon }}"></i>
|
||||
{% endif %}
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# Regular menu item #}
|
||||
<li class="nav-item px-1">
|
||||
<a {% if request.url_rule.endpoint == item.endpoint %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %} href="{{ url_for(item.endpoint) }}">
|
||||
{% if item.icon %}
|
||||
<i class="ri-{{ item.icon }}"></i>
|
||||
{% endif %}
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@@ -84,7 +160,6 @@
|
||||
<!-- BrickTracker scripts -->
|
||||
<script src="{{ url_for('static', filename='scripts/collapsible-state.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/changer.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/grid/filter_toggle.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/grid/filter.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/grid/grid.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/grid/sort.js') }}"></script>
|
||||
@@ -93,7 +168,14 @@
|
||||
<script src="{{ url_for('static', filename='scripts/socket/socket.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/socket/instructions.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/socket/set.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/socket/minifigure.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/table.js') }}"></script>
|
||||
{% if request.endpoint == 'add.add' %}
|
||||
<script src="{{ url_for('static', filename='scripts/add.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'add.parts' %}
|
||||
<script src="{{ url_for('static', filename='scripts/add_parts.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'minifigure.list' %}
|
||||
<script src="{{ url_for('static', filename='scripts/minifigures.js') }}"></script>
|
||||
{% endif %}
|
||||
@@ -110,6 +192,16 @@
|
||||
{% if request.endpoint == 'set.details' %}
|
||||
<script src="{{ url_for('static', filename='scripts/parts-bulk-operations.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/set-details.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='scripts/quick-add-individual-part.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'part.details' %}
|
||||
<script src="{{ url_for('static', filename='scripts/quick-add-individual-part.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'individual_part.lot_details' %}
|
||||
<script src="{{ url_for('static', filename='scripts/parts-bulk-operations.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'individual_minifigure.details' %}
|
||||
<script src="{{ url_for('static', filename='scripts/parts-bulk-operations.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'statistics.overview' %}
|
||||
<script src="{{ url_for('static', filename='scripts/statistics.js') }}"></script>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{% import 'macro/badge.html' as badge %}
|
||||
{% import 'macro/card.html' as card %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
<div class="card mb-3 flex-fill">
|
||||
{{ card.header(item, item.fields.name, solo=false, identifier=item.fields.figure, icon='user-line') }}
|
||||
{{ card.image(item, solo=false, last=false, caption=item.fields.name, alt=item.fields.figure, medium=false) }}
|
||||
<div class="card-body border-bottom p-1">
|
||||
{# Always show quantity first #}
|
||||
{{ badge.quantity(item.fields.quantity, solo=false, last=false) }}
|
||||
{# Render all other badges in configured order #}
|
||||
{{ badge.render_ordered_badges(item, brickset_tags, brickset_owners, brickset_storages, brickset_purchase_locations, solo=false, last=false, context='grid') }}
|
||||
{# Description badge at end if exists #}
|
||||
{% if item.fields.description %}
|
||||
<span class="badge text-bg-light text-dark text-wrap" data-bs-toggle="tooltip" title="{{ item.fields.description }}">
|
||||
<i class="ri-file-text-line"></i> {{ item.fields.description[:config.get('DESCRIPTION_BADGE_MAX_LENGTH', 50)] }}{% if item.fields.description | length > config.get('DESCRIPTION_BADGE_MAX_LENGTH', 50) %}...{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if g.login.is_authenticated() %}
|
||||
<div class="card-footer p-1 border-bottom">
|
||||
{{ form.input('Qty', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if brickset_statuses | length %}
|
||||
<ul class="list-group list-group-flush card-check border-bottom-0">
|
||||
{% for status in brickset_statuses %}
|
||||
<li class="d-flex list-group-item p-1 text-nowrap">
|
||||
{{ form.checkbox(status.fields.name, item.fields.id, status.as_dataset(), status.url_for_individual_minifigure_state(item.fields.id), item.fields[status.as_column()]) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends 'base.html' %}
|
||||
{% import 'macro/accordion.html' as accordion %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
{% block title %} - Individual Minifigure {{ item.fields.name }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="ri-user-line"></i> {{ item.fields.name }}
|
||||
<span class="badge text-bg-secondary fw-normal"><i class="ri-hashtag"></i> {{ item.fields.figure }}</span>
|
||||
</h5>
|
||||
<div>
|
||||
<a href="{{ url_for('minifigure.details', figure=item.fields.figure) }}" class="btn btn-sm btn-secondary">
|
||||
<i class="ri-arrow-left-line"></i> Back to {{ item.fields.figure }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-img border-bottom" style="background-image: url({{ item.url_for_image() }})">
|
||||
<a data-lightbox data-caption="{{ item.fields.name }}" href="{{ item.url_for_image() }}" target="_blank">
|
||||
<img class="card-medium-img" src="{{ item.url_for_image() }}" alt="{{ item.fields.figure }}" loading="lazy">
|
||||
</a>
|
||||
</div>
|
||||
<div class="accordion accordion-flush border-top" id="individual-minifigure-details-{{ item.fields.id }}">
|
||||
{{ accordion.header('Quantity', 'accordion-quantity-' ~ item.fields.id, 'individual-minifigure-details-' ~ item.fields.id, icon='functions') }}
|
||||
{{ form.input('Quantity', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.table(item.generic_parts(), 'Parts', 'accordion-parts-' ~ item.fields.id, 'individual-minifigure-details-' ~ item.fields.id, 'part/table.html', icon='shapes-line', alt=item.fields.figure, hamburger_menu=g.login.is_authenticated()) }}
|
||||
{% include 'individual_minifigure/management.html' %}
|
||||
{% if g.login.is_authenticated() %}
|
||||
{{ accordion.header('Danger zone', 'accordion-danger-zone-' ~ item.fields.id, 'individual-minifigure-details-' ~ item.fields.id, danger=true, class='text-end') }}
|
||||
<a href="{{ url_for('individual_minifigure.delete', id=item.fields.id) }}" class="btn btn-danger" role="button" data-bs-toggle="modal" data-bs-target="#deleteModal"><i class="ri-close-line"></i> Delete this individual minifigure instance</a>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete this individual minifigure instance? This action cannot be undone.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form method="POST" action="{{ url_for('individual_minifigure.delete', id=item.fields.id) }}">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,67 @@
|
||||
{% import 'macro/accordion.html' as accordion %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
{% if g.login.is_authenticated() %}
|
||||
{{ accordion.header('Management', 'accordion-management-' ~ item.fields.id, 'individual-minifigure-details-' ~ item.fields.id, icon='settings-4-line', class='p-0') }}
|
||||
{{ accordion.header('Owners', 'accordion-owners-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='group-line', class='p-0') }}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% if brickset_owners | length %}
|
||||
{% for owner in brickset_owners %}
|
||||
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.id, owner.as_dataset(), owner.url_for_individual_minifigure_state(item.fields.id), item.fields[owner.as_column()]) }}</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush border-top">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the minifigure owners</a>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Purchase', 'accordion-purchase-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='wallet-3-line') }}
|
||||
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configured how it is displayed in the minifigure card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
|
||||
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
|
||||
<div class="col-12">
|
||||
{{ form.input('Date', item.fields.id, 'purchase_date', url_for('individual_minifigure.update_purchase_date', id=item.fields.id), item.fields.purchase_date or '', date=true, icon='calendar-line') }}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
{{ form.input('Price', item.fields.id, 'purchase_price', url_for('individual_minifigure.update_purchase_price', id=item.fields.id), item.fields.purchase_price or '', suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
{% if brickset_purchase_locations | length %}
|
||||
{{ form.select('Location', item.fields.id, brickset_purchase_locations.as_prefix(), brickset_purchase_locations.url_for_individual_minifigure_value(item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line') }}
|
||||
{% else %}
|
||||
<i class="ri-error-warning-line"></i> No purchase location found.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<a href="{{ url_for('admin.admin', open_purchase_location=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the minifigure purchase locations</a>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Notes', 'accordion-notes-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='sticky-note-line') }}
|
||||
{{ form.textarea('Notes', item.fields.id, 'description', url_for('individual_minifigure.update_description', id=item.fields.id), item.fields.description, rows=4) }}
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Storage', 'accordion-storage-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='archive-2-line') }}
|
||||
{% if brickset_storages | length %}
|
||||
{{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), brickset_storages.url_for_individual_minifigure_value(item.fields.id), item.fields.storage, brickset_storages, icon='building-line') }}
|
||||
{% else %}
|
||||
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the minifigure storages</a>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Tags', 'accordion-tags-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='price-tag-2-line', class='p-0') }}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% if brickset_tags | length %}
|
||||
{% for tag in brickset_tags %}
|
||||
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(tag.fields.name, item.fields.id, tag.as_dataset(), tag.url_for_individual_minifigure_state(item.fields.id), item.fields[tag.as_column()]) }}</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush border-top">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the minifigure tags</a>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends 'base.html' %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
{% block title %} - Individual Minifigures{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2><i class="ri-user-line"></i> Individual Minifigures</h2>
|
||||
<a href="{{ url_for('add.add') }}" class="btn btn-primary">
|
||||
<i class="ri-add-line"></i> Add Minifigures
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if minifigures | length %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
|
||||
{% for item in minifigures %}
|
||||
<div class="col">
|
||||
{% include 'individual_minifigure/card.html' with context %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="ri-information-line"></i> No individual minifigures found.
|
||||
<a href="{{ url_for('add.add') }}" class="alert-link">Add some minifigures</a> to get started!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% import 'macro/badge.html' as badge %}
|
||||
{% import 'macro/card.html' as card %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
<div class="card mb-3 flex-fill">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<a href="{{ item.url() }}" class="text-decoration-none">
|
||||
<i class="ri-hammer-line"></i>
|
||||
{{ item.fields.name }} ({{ item.fields.color_name }})
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
{{ card.image(item, solo=false, last=false, caption=item.fields.name, alt=item.fields.part, medium=false) }}
|
||||
<div class="card-body border-bottom p-1">
|
||||
{# Always show quantity first #}
|
||||
{{ badge.quantity(item.fields.quantity, solo=false, last=false) }}
|
||||
{# Render all other badges in configured order #}
|
||||
{{ badge.render_ordered_badges(item, brickset_tags, brickset_owners, brickset_storages, brickset_purchase_locations, solo=false, last=false, context='grid') }}
|
||||
{# Description badge at end if exists #}
|
||||
{% if item.fields.description %}
|
||||
<span class="badge text-bg-light text-dark text-wrap" data-bs-toggle="tooltip" title="{{ item.fields.description }}">
|
||||
<i class="ri-file-text-line"></i> {{ item.fields.description[:config.get('DESCRIPTION_BADGE_MAX_LENGTH', 50)] }}{% if item.fields.description | length > config.get('DESCRIPTION_BADGE_MAX_LENGTH', 50) %}...{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if g.login.is_authenticated() %}
|
||||
<div class="card-footer p-1">
|
||||
{{ form.input('Qty', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,98 @@
|
||||
{% extends 'base.html' %}
|
||||
{% import 'macro/accordion.html' as accordion %}
|
||||
{% import 'macro/badge.html' as badge %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
{% block title %} - Individual Part {{ item.fields.name }}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="ri-hammer-line"></i>
|
||||
{{ badge.identifier(item.fields.part, icon='shapes-line', solo=true, header=true) }}
|
||||
{{ badge.color(item, header=true) }}
|
||||
{{ item.fields.name }}
|
||||
</h5>
|
||||
<div>
|
||||
<a href="{{ url_for('part.details', part=item.fields.part, color=item.fields.color) }}" class="btn btn-sm btn-secondary">
|
||||
<i class="ri-arrow-left-line"></i> Back to {{ item.fields.part }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if item.fields.image %}
|
||||
<div class="card-img border-bottom" style="background-image: url({{ item.url_for_image() }})">
|
||||
<a data-lightbox data-caption="{{ item.fields.name }}" href="{{ item.url_for_image() }}" target="_blank">
|
||||
<img class="card-medium-img" src="{{ item.url_for_image() }}" alt="{{ item.fields.part }}" loading="lazy">
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card-body border-bottom">
|
||||
<div class="row g-2">
|
||||
<div class="col-12 col-lg-4">
|
||||
{{ form.input('Quantity', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
{{ form.input('Missing', item.fields.id, 'missing', item.url_for_problem('missing'), item.fields.missing, icon='question-line') }}
|
||||
</div>
|
||||
<div class="col-12 col-lg-4">
|
||||
{{ form.input('Damaged', item.fields.id, 'damaged', item.url_for_problem('damaged'), item.fields.damaged, icon='error-warning-line') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="accordion accordion-flush border-top" id="individual-part-details-{{ item.fields.id }}">
|
||||
{% if lot %}
|
||||
{{ accordion.header('Lot', 'accordion-lot-' ~ item.fields.id, 'individual-part-details-' ~ item.fields.id, icon='shopping-cart-line') }}
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="ri-information-line"></i> This part belongs to a lot
|
||||
{% if lot.fields.name %}<strong>"{{ lot.fields.name }}"</strong>{% endif %}
|
||||
with {{ lot.parts()|length }} total parts.
|
||||
</div>
|
||||
<p class="mb-3">
|
||||
All metadata (storage, purchase information, owners, tags) is managed at the lot level.
|
||||
View the full lot to see all metadata and other parts.
|
||||
</p>
|
||||
<a href="{{ lot.url() }}" class="btn btn-primary">
|
||||
<i class="ri-eye-line"></i> View Full Lot
|
||||
</a>
|
||||
{{ accordion.footer() }}
|
||||
{% else %}
|
||||
{# Only show management accordion if NOT part of a lot #}
|
||||
{% include 'individual_part/management.html' %}
|
||||
{% endif %}
|
||||
{% if g.login.is_authenticated() %}
|
||||
{{ accordion.header('Danger zone', 'accordion-danger-zone-' ~ item.fields.id, 'individual-part-details-' ~ item.fields.id, danger=true, class='text-end') }}
|
||||
<a href="{{ url_for('individual_part.delete_part', id=item.fields.id) }}" class="btn btn-danger" role="button" data-bs-toggle="modal" data-bs-target="#deleteModal"><i class="ri-close-line"></i> Delete this individual part instance</a>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete this individual part instance? This action cannot be undone.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form method="POST" action="{{ url_for('individual_part.delete_part', id=item.fields.id) }}">
|
||||
<button type="submit" class="btn btn-danger">Delete</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,90 @@
|
||||
{% import 'macro/accordion.html' as accordion %}
|
||||
{% import 'macro/badge.html' as badge %}
|
||||
{% import 'macro/card.html' as card %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="ri-shopping-cart-line"></i>
|
||||
{% if item.fields.name %}
|
||||
{{ item.fields.name }}
|
||||
{% else %}
|
||||
Individual Part Lot
|
||||
{% endif %}
|
||||
</h5>
|
||||
{% if not solo %}
|
||||
<a href="{{ item.url() }}" class="btn btn-sm btn-primary">
|
||||
<i class="ri-eye-line"></i> View
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('individual_part.list_lots') }}" class="btn btn-sm btn-secondary">
|
||||
<i class="ri-arrow-left-line"></i> Back
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Part preview grid (2x2 grid of top 4 parts) -->
|
||||
<div class="row g-0 border-bottom">
|
||||
{% set lot_parts = item.parts() %}
|
||||
{% for part in lot_parts[:4] %}
|
||||
<div class="col-6">
|
||||
<div class="{% if solo %}p-3{% else %}p-1{% endif %}" style="{% if solo %}height: 200px{% else %}height: 100px{% endif %}; background-image: url('{{ part.url_for_image() }}'); background-size: contain; background-repeat: no-repeat; background-position: center;">
|
||||
<img src="{{ part.url_for_image() }}" alt="{{ part.fields.name }}" class="d-none" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if lot_parts|length > 4 %}
|
||||
<div class="col-12 text-center p-1 bg-light">
|
||||
<small class="text-muted">+{{ lot_parts|length - 4 }} more parts</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
|
||||
{# Always show unique parts count and total quantity first for part lots #}
|
||||
<span class="badge text-bg-secondary fw-normal mb-1 {% if solo %}fs-6{% endif %}"><i class="ri-shapes-line"></i> {{ item.parts()|length }} unique parts</span>
|
||||
{{ badge.quantity(item.total_quantity(), solo=solo, last=false) }}
|
||||
{# Render remaining badges in configured order #}
|
||||
{{ badge.render_ordered_badges(item, brickset_tags, brickset_owners, brickset_storages, brickset_purchase_locations, solo=solo, last=false, context='detail' if solo else 'grid') }}
|
||||
</div>
|
||||
|
||||
{% if solo %}
|
||||
<div class="accordion accordion-flush border-top" id="lot-details">
|
||||
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'lot-details', 'part/lot_table.html', icon='shapes-line', hamburger_menu=g.login.is_authenticated())}}
|
||||
{% include 'individual_part/management.html' %}
|
||||
{% if g.login.is_authenticated() %}
|
||||
{{ accordion.header('Danger zone', 'danger-zone', 'lot-details', danger=true, class='text-end') }}
|
||||
<a href="{{ url_for('individual_part.delete_lot', lot_id=item.fields.id) }}" class="btn btn-danger" role="button" data-bs-toggle="modal" data-bs-target="#deleteModal">
|
||||
<i class="ri-delete-bin-line"></i> Delete entire lot and all parts
|
||||
</a>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card-footer"></div>
|
||||
</div>
|
||||
|
||||
{% if solo and g.login.is_authenticated() %}
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Confirm Delete</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you want to delete this entire lot and all {{ item.parts()|length }} parts in it? This action cannot be undone.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form method="POST" action="{{ url_for('individual_part.delete_lot', lot_id=item.fields.id) }}">
|
||||
<button type="submit" class="btn btn-danger">Delete Lot</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %} - {% if item.fields.name %}{{ item.fields.name }}{% else %}Individual Part Lot{% endif %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% with solo=true %}
|
||||
{% include 'individual_part/lot_card.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %} - Individual Part Lots{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2><i class="ri-shopping-cart-line"></i> Individual Part Lots</h2>
|
||||
<a href="{{ url_for('add.parts') }}" class="btn btn-primary">
|
||||
<i class="ri-add-line"></i> Add Parts
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if lots | length %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
|
||||
{% for item in lots %}
|
||||
<div class="col">
|
||||
{% include 'individual_part/lot_card.html' with context %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="ri-information-line"></i> No lots found.
|
||||
<a href="{{ url_for('add.parts') }}" class="alert-link">Add some parts</a> to get started!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,132 @@
|
||||
{% import 'macro/accordion.html' as accordion %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
{% if g.login.is_authenticated() %}
|
||||
{{ accordion.header('Management', 'accordion-management-' ~ item.fields.id, 'individual-part-details-' ~ item.fields.id, icon='settings-4-line', class='p-0') }}
|
||||
{% if item.__class__.__name__ == 'IndividualPartLot' %}
|
||||
{{ accordion.header('Title', 'accordion-title-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='file-text-line') }}
|
||||
<div class="alert alert-info" role="alert">Give this part lot a descriptive name to help identify it later. This is shown in the lot card header and list views.</div>
|
||||
{{ form.input('', item.fields.id, 'lot-name-' ~ item.fields.id, url_for('individual_part.update_lot_name', lot_id=item.fields.id), item.fields.name) }}
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{{ accordion.header('Owners', 'accordion-owners-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='group-line', class='p-0') }}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% if brickset_owners | length %}
|
||||
{% for owner in brickset_owners %}
|
||||
{% if item.__class__.__name__ == 'IndividualPartLot' %}
|
||||
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.id, owner.as_dataset(), url_for('individual_part.update_lot_owner', lot_id=item.fields.id, metadata_id=owner.fields.id), item.fields[owner.as_column()] | default(false)) }}</li>
|
||||
{% else %}
|
||||
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.id, owner.as_dataset(), owner.url_for_individual_part_state(item.fields.id), item.fields[owner.as_column()]) }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush border-top">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the part owners</a>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% if item.__class__.__name__ == 'IndividualPartLot' %}
|
||||
{{ accordion.header('Purchase', 'accordion-purchase-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='wallet-3-line') }}
|
||||
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configured how it is displayed in the part lot card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
|
||||
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
|
||||
<div class="col-12">
|
||||
{{ form.input('Date', item.fields.id, 'purchase_date', url_for('individual_part.update_lot_purchase_date', lot_id=item.fields.id), item.fields.purchase_date or '', date=true, icon='calendar-line') }}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
{{ form.input('Price', item.fields.id, 'purchase_price', url_for('individual_part.update_lot_purchase_price', lot_id=item.fields.id), item.fields.purchase_price or '', suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
{% if brickset_purchase_locations | length %}
|
||||
{{ form.select('Location', item.fields.id, brickset_purchase_locations.as_prefix(), url_for('individual_part.update_lot_purchase_location', lot_id=item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line') }}
|
||||
{% else %}
|
||||
<i class="ri-error-warning-line"></i> No purchase location found.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<a href="{{ url_for('admin.admin', open_purchase_location=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the part lot purchase locations</a>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Notes', 'accordion-notes-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='sticky-note-line') }}
|
||||
{{ form.textarea('', item.fields.id, 'lot-description-' ~ item.fields.id, url_for('individual_part.update_lot_description', lot_id=item.fields.id), item.fields.description, rows=4) }}
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Storage', 'accordion-storage-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='archive-2-line') }}
|
||||
{% if brickset_storages | length %}
|
||||
{{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), url_for('individual_part.update_lot_storage', lot_id=item.fields.id), item.fields.storage, brickset_storages, icon='building-line') }}
|
||||
{% else %}
|
||||
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the part lot storages</a>
|
||||
{{ accordion.footer() }}
|
||||
{% else %}
|
||||
{{ accordion.header('Storage', 'accordion-storage-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='archive-2-line') }}
|
||||
{% if brickset_storages | length %}
|
||||
{{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), brickset_storages.url_for_individual_part_value(item.fields.id), item.fields.storage, brickset_storages, icon='building-line') }}
|
||||
{% else %}
|
||||
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
|
||||
{% endif %}
|
||||
<hr>
|
||||
<a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the storages</a>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Purchase', 'accordion-purchase-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='wallet-3-line') }}
|
||||
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configured how it is displayed in the part card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
|
||||
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
|
||||
<div class="col-12">
|
||||
{{ form.input('Date', item.fields.id, 'purchase_date', item.url_for_purchase_date(), item.fields.purchase_date or '', date=true, icon='calendar-line') }}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
{{ form.input('Price', item.fields.id, 'purchase_price', item.url_for_purchase_price(), item.fields.purchase_price or '', suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
{% if brickset_purchase_locations | length %}
|
||||
{{ form.select('Location', item.fields.id, brickset_purchase_locations.as_prefix(), brickset_purchase_locations.url_for_individual_part_value(item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line') }}
|
||||
{% else %}
|
||||
<i class="ri-error-warning-line"></i> No purchase location found.
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<a href="{{ url_for('admin.admin', open_purchase_location=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the purchase locations</a>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Notes', 'accordion-notes-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='sticky-note-line') }}
|
||||
{{ form.textarea('', item.fields.id, 'description', item.url_for_description(), item.fields.description or '', rows=4) }}
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{% if item.__class__.__name__ != 'IndividualPartLot' %}
|
||||
{{ accordion.header('Statuses', 'accordion-statuses-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='checkbox-line', class='p-0') }}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% if brickset_statuses | length %}
|
||||
{% for status in brickset_statuses %}
|
||||
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(status.fields.name, item.fields.id, status.as_dataset(), status.url_for_individual_part_state(item.fields.id), item.fields[status.as_column()]) }}</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No status found.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush border-top">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_status=true) }}"><i class="ri-settings-4-line"></i> Manage the statuses</a>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{{ accordion.header('Tags', 'accordion-tags-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='price-tag-2-line', class='p-0') }}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% if brickset_tags | length %}
|
||||
{% for tag in brickset_tags %}
|
||||
{% if item.__class__.__name__ == 'IndividualPartLot' %}
|
||||
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(tag.fields.name, item.fields.id, tag.as_dataset(), url_for('individual_part.update_lot_tag', lot_id=item.fields.id, metadata_id=tag.fields.id), item.fields[tag.as_column()] | default(false)) }}</li>
|
||||
{% else %}
|
||||
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(tag.fields.name, item.fields.id, tag.as_dataset(), tag.url_for_individual_part_state(item.fields.id), item.fields[tag.as_column()]) }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush border-top">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the tags</a>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends 'base.html' %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
{% block title %} - Individual Parts{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h2><i class="ri-hammer-line"></i> Individual Parts</h2>
|
||||
<a href="{{ url_for('add.parts') }}" class="btn btn-primary">
|
||||
<i class="ri-add-line"></i> Add Parts
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if parts | length %}
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
|
||||
{% for item in parts %}
|
||||
<div class="col">
|
||||
{% include 'individual_part/card.html' with context %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info" role="alert">
|
||||
<i class="ri-information-line"></i> No individual parts found.
|
||||
<a href="{{ url_for('add.parts') }}" class="alert-link">Add some parts</a> to get started!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -57,7 +57,7 @@
|
||||
<a class="btn border bg-secondary-text" href="{{ details }}">{% if icon %}<i class="ri-{{ icon }}"></i>{% endif %} Details</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% with solo=true, all=false, accordion_id=id %}
|
||||
{% with solo=true, all=false, accordion_id=id, read_only=read_only, hamburger_menu=hamburger_menu %}
|
||||
{% include target %}
|
||||
{% endwith %}
|
||||
{{ footer() }}
|
||||
|
||||
+43
-11
@@ -230,27 +230,44 @@
|
||||
{# Render each badge in the configured order #}
|
||||
{% for badge_key in badge_order %}
|
||||
{% if badge_key == 'theme' %}
|
||||
{{ theme(item.theme.name, solo=solo, last=last) }}
|
||||
{# Only sets have themes #}
|
||||
{% if item.theme is defined %}
|
||||
{{ theme(item.theme.name, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'tag' %}
|
||||
{% for tag_item in brickset_tags %}
|
||||
{{ tag(item, tag_item, solo=solo, last=last) }}
|
||||
{% endfor %}
|
||||
{% elif badge_key == 'year' %}
|
||||
{% if not last %}
|
||||
{# Only sets have years #}
|
||||
{% if not last and item.fields.year is defined %}
|
||||
{{ year(item.fields.year, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'parts' %}
|
||||
{{ parts(item.fields.number_of_parts, solo=solo, last=last) }}
|
||||
{# Only sets and minifigures have number_of_parts #}
|
||||
{% if item.fields.number_of_parts is defined %}
|
||||
{{ parts(item.fields.number_of_parts, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'instance_count' %}
|
||||
{# Only consolidated sets have instance_count #}
|
||||
{% if item.fields.instance_count is defined and item.fields.instance_count > 1 %}
|
||||
<span class="badge bg-primary"><i class="ri-stack-line"></i> {{ item.fields.instance_count }} copies</span>
|
||||
{% endif %}
|
||||
{% elif badge_key == 'total_minifigures' %}
|
||||
{{ total_minifigures(item.fields.total_minifigures, solo=solo, last=last) }}
|
||||
{# Only sets have total_minifigures #}
|
||||
{% if item.fields.total_minifigures is defined %}
|
||||
{{ total_minifigures(item.fields.total_minifigures, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'total_missing' %}
|
||||
{{ total_missing(item.fields.total_missing, solo=solo, last=last) }}
|
||||
{# Sets have total_missing, individual items have missing #}
|
||||
{% if item.fields.total_missing is defined %}
|
||||
{{ total_missing(item.fields.total_missing, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'total_damaged' %}
|
||||
{{ total_damaged(item.fields.total_damaged, solo=solo, last=last) }}
|
||||
{# Sets have total_damaged, individual items have damaged #}
|
||||
{% if item.fields.total_damaged is defined %}
|
||||
{{ total_damaged(item.fields.total_damaged, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'owner' %}
|
||||
{% for owner_item in brickset_owners %}
|
||||
{{ owner(item, owner_item, solo=solo, last=last) }}
|
||||
@@ -258,27 +275,42 @@
|
||||
{% elif badge_key == 'storage' %}
|
||||
{{ storage(item, brickset_storages, solo=solo, last=last) }}
|
||||
{% elif badge_key == 'purchase_date' %}
|
||||
{# Sets and part lots have purchase_date methods, individual parts/minifigures have raw fields #}
|
||||
{% if not last %}
|
||||
{{ purchase_date(item.purchase_date(), solo=solo, last=last, date_max_formatted=item.purchase_date_max_formatted()) }}
|
||||
{% if item.purchase_date is defined and item.purchase_date is callable %}
|
||||
{{ purchase_date(item.purchase_date(), solo=solo, last=last, date_max_formatted=item.purchase_date_max_formatted()) }}
|
||||
{% elif item.purchase_date_formatted is defined and item.purchase_date_formatted is callable %}
|
||||
{{ purchase_date(item.purchase_date_formatted(), solo=solo, last=last) }}
|
||||
{% elif item.fields.purchase_date is defined and item.fields.purchase_date %}
|
||||
{{ purchase_date(item.fields.purchase_date, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'purchase_location' %}
|
||||
{% if not last %}
|
||||
{{ purchase_location(item, brickset_purchase_locations, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'purchase_price' %}
|
||||
{# Sets and part lots have purchase_price methods, individual parts/minifigures have raw fields #}
|
||||
{% if not last %}
|
||||
{{ purchase_price(item.purchase_price(), solo=solo, last=last) }}
|
||||
{% if item.purchase_price is defined and item.purchase_price is callable %}
|
||||
{{ purchase_price(item.purchase_price(), solo=solo, last=last) }}
|
||||
{% elif item.fields.purchase_price is defined and item.fields.purchase_price %}
|
||||
{{ purchase_price(item.fields.purchase_price, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'instructions' %}
|
||||
{% if not last and not solo %}
|
||||
{# Only sets have instructions #}
|
||||
{% if not last and not solo and item.instructions is defined %}
|
||||
{{ instructions(item, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'rebrickable' %}
|
||||
{% if not last %}
|
||||
{# Only items with url_for_rebrickable method (sets, parts, minifigures) #}
|
||||
{% if not last and item.url_for_rebrickable is defined %}
|
||||
{{ rebrickable(item, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% elif badge_key == 'bricklink' %}
|
||||
{% if not last %}
|
||||
{# Only items with url_for_bricklink method (sets, parts) #}
|
||||
{% if not last and item.url_for_bricklink is defined %}
|
||||
{{ bricklink(item, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
{% if hamburger_menu and g.login.is_authenticated() %}
|
||||
{% 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 %}
|
||||
<th data-table-no-sort-and-search="true" class="no-sort text-end" scope="col">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="hamburger-{{ accordion_id }}" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
{% if solo %}
|
||||
<div class="accordion accordion-flush" id="minifigure-details">
|
||||
{{ accordion.table(item.generic_parts(), 'Parts', item.fields.figure, 'minifigure-details', 'part/table.html', icon='shapes-line', alt=item.fields.figure, read_only=read_only)}}
|
||||
{% if individual_instances is defined and individual_instances | length %}
|
||||
{{ accordion.cards(individual_instances, 'Individual instances (not in sets)', 'individual-instances', 'minifigure-details', 'individual_minifigure/card.html', icon='user-star-line') }}
|
||||
{% endif %}
|
||||
{{ accordion.cards(using, 'Sets using this minifigure', 'using-inventory', 'minifigure-details', 'set/card.html', icon='hashtag') }}
|
||||
{{ accordion.cards(missing, 'Sets missing parts for this minifigure', 'missing-inventory', 'minifigure-details', 'set/card.html', icon='question-line') }}
|
||||
{{ accordion.cards(damaged, 'Sets with damaged parts for this minifigure', 'damaged-inventory', 'minifigure-details', 'set/card.html', icon='error-warning-line') }}
|
||||
|
||||
@@ -23,6 +23,11 @@
|
||||
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="table-filter">
|
||||
<i class="ri-filter-line"></i> Filters
|
||||
</button>
|
||||
{% if not config.get('HIDE_INDIVIDUAL_MINIFIGURES', False) %}
|
||||
<button class="btn {% if selected_individuals == 'only' %}btn-secondary{% else %}btn-outline-secondary{% endif %}" type="button" id="individuals-filter-toggle" title="Show individual minifigures only">
|
||||
<i class="ri-user-line"></i> Individuals
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,26 @@
|
||||
{% import 'macro/card.html' as card %}
|
||||
|
||||
<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
|
||||
{{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.part, icon='shapes-line') }}
|
||||
<div class="card-header {% if solo %}d-flex justify-content-between align-items-center{% endif %}">
|
||||
<h5 class="mb-0">
|
||||
{{ 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 }}
|
||||
</h5>
|
||||
{% 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) %}
|
||||
<button class="btn btn-sm btn-primary quick-add-individual-part"
|
||||
data-part="{{ item.fields.part }}"
|
||||
data-color="{{ item.fields.color }}"
|
||||
data-name="{{ item.fields.name }}"
|
||||
data-color-name="{{ item.fields.color_name }}"
|
||||
data-image="{{ item.url_for_image() }}">
|
||||
<i class="ri-add-line"></i> Add to Inventory
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.image_id, medium=true) }}
|
||||
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
|
||||
{% 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') }}
|
||||
</div>
|
||||
<div class="card-footer"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% 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) %}
|
||||
<!-- Quick Add Individual Part Modal -->
|
||||
<div class="modal fade" id="quickAddIndividualPartModal" tabindex="-1" aria-labelledby="quickAddModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="quickAddModalLabel">Add to Individual Parts Inventory</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="quickAddForm">
|
||||
<!-- Part info (read-only) -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Part</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<img id="quickAddPartImage" src="" alt="" style="max-width: 100px; max-height: 100px;" class="me-3">
|
||||
<div>
|
||||
<div id="quickAddPartName" class="fw-bold"></div>
|
||||
<div id="quickAddPartNumber" class="text-muted small"></div>
|
||||
<div id="quickAddPartColor" class="text-muted small"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="quickAddPart" name="part">
|
||||
<input type="hidden" id="quickAddColor" name="color">
|
||||
|
||||
<!-- Quantity -->
|
||||
<div class="mb-3">
|
||||
<label for="quickAddQuantity" class="form-label">Quantity <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="quickAddQuantity" name="quantity" value="1" min="1" required>
|
||||
</div>
|
||||
|
||||
<!-- Storage (optional) -->
|
||||
<div class="mb-3">
|
||||
<label for="quickAddStorage" class="form-label">Storage (optional)</label>
|
||||
<select class="form-select" id="quickAddStorage" name="storage">
|
||||
<option value="">-- No storage --</option>
|
||||
{% for storage in brickset_storages %}
|
||||
<option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Purchase Location (optional) -->
|
||||
<div class="mb-3">
|
||||
<label for="quickAddPurchaseLocation" class="form-label">Purchase Location (optional)</label>
|
||||
<select class="form-select" id="quickAddPurchaseLocation" name="purchase_location">
|
||||
<option value="">-- No purchase location --</option>
|
||||
{% for location in brickset_purchase_locations %}
|
||||
<option value="{{ location.fields.id }}">{{ location.fields.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Description/Notes (optional) -->
|
||||
<div class="mb-3">
|
||||
<label for="quickAddDescription" class="form-label">Notes (optional)</label>
|
||||
<textarea class="form-control" id="quickAddDescription" name="description" rows="2"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="quickAddSubmit">Add to Inventory</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{% import 'macro/form.html' as form %}
|
||||
{% import 'macro/table.html' as table %}
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table class="table table-striped align-middle sortable mb-0">
|
||||
{{ table.header(color=true, quantity=true, sets=false, minifigures=false, checked=not read_only, hamburger_menu=not read_only, accordion_id=accordion_id|default('')) }}
|
||||
<tbody>
|
||||
{% for item in table_collection %}
|
||||
<tr>
|
||||
{{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.part, accordion=solo) }}
|
||||
<td data-sort="{{ item.fields.name }}">
|
||||
<a class="text-reset" href="{{ item.url() }}">{{ item.fields.name }}</a>
|
||||
</td>
|
||||
<td data-sort="{{ item.fields.color_name }}">
|
||||
{% if item.fields.color_rgb %}<span class="color-rgb color-rgb-table {% if item.fields.color == 9999 %}color-any{% endif %} align-middle border border-black" {% if item.fields.color != 9999 %}style="background-color: #{{ item.fields.color_rgb }};"{% endif %}></span>{% endif %}
|
||||
<span class="align-middle">{{ item.fields.color_name }}</span>
|
||||
</td>
|
||||
<td>{{ item.fields.quantity }}</td>
|
||||
{% if not config['HIDE_TABLE_MISSING_PARTS'] %}
|
||||
<td data-sort="{{ item.fields.missing }}" class="table-td-input">
|
||||
{{ form.input('Missing', item.fields.id, item.html_id('missing'), item.url_for_problem('missing'), item.fields.missing, all=false, read_only=read_only) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
|
||||
<td data-sort="{{ item.fields.damaged }}" class="table-td-input">
|
||||
{{ form.input('Damaged', item.fields.id, item.html_id('damaged'), item.url_for_problem('damaged'), item.fields.damaged, all=false, read_only=read_only) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if not config['HIDE_TABLE_CHECKED_PARTS'] and not read_only %}
|
||||
<td class="table-td-input">
|
||||
<center>{{ form.checkbox('', item.fields.id, item.html_id('checked'), item.url_for_checked(), item.fields.checked | default(false), parent='part', delete=read_only) }}</center>
|
||||
</td>
|
||||
{% 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 %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -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 %}
|
||||
<td></td>
|
||||
{% 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 %}
|
||||
<td class="text-end">
|
||||
{% if show_quick_add %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="ri-more-2-fill"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a class="dropdown-item quick-add-individual-part" href="#"
|
||||
data-part="{{ item.fields.part }}"
|
||||
data-color="{{ item.fields.color }}"
|
||||
data-name="{{ item.fields.name }}"
|
||||
data-color-name="{{ item.fields.color_name }}"
|
||||
data-image="{{ item.url_for_image() }}">
|
||||
<i class="ri-add-line me-2"></i>Add to individual parts
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="table-filter">
|
||||
<i class="ri-filter-line"></i> Filters
|
||||
</button>
|
||||
{% if not config.get('HIDE_INDIVIDUAL_PARTS', False) %}
|
||||
<button class="btn {% if selected_individuals == 'only' %}btn-secondary{% else %}btn-outline-secondary{% endif %}" type="button" id="individuals-filter-toggle" title="Show individual parts only">
|
||||
<i class="ri-shapes-line"></i> Individuals
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %} - Purchase Location {{ item.fields.name}}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% with solo=true %}
|
||||
{% include 'purchase_location/card.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% import 'macro/accordion.html' as accordion with context %}
|
||||
{% import 'macro/badge.html' as badge %}
|
||||
{% import 'macro/card.html' as card %}
|
||||
|
||||
<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
|
||||
{{ card.header(item, item.fields.name, solo=solo, icon='map-pin-line') }}
|
||||
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
|
||||
{{ 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 %}
|
||||
</div>
|
||||
{% if solo %}
|
||||
<div class="accordion accordion-flush border-top" id="purchase-location-details">
|
||||
{{ 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 %}
|
||||
</div>
|
||||
<div class="card-footer"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
{% import 'macro/form.html' as form %}
|
||||
{% import 'macro/table.html' as table %}
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table data-table="true" class="table table-striped align-middle" id="purchase-locations">
|
||||
{{ table.header(image=false, missing=false, damaged=false, sets=true) }}
|
||||
<tbody>
|
||||
{% for item in table_collection %}
|
||||
<tr>
|
||||
<td data-sort="{{ item.fields.name }}"><a class="text-reset" href="{{ item.url() }}">{{ item.fields.name }}</a></td>
|
||||
<td>{{ item.fields.total_sets }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %} - All purchase locations{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container-fluid px-0">
|
||||
{% with all=true %}
|
||||
{% include 'purchase_location/table.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -154,3 +154,74 @@
|
||||
<div class="card-footer"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if solo and g.login.is_authenticated() %}
|
||||
<!-- Quick Add Individual Part Modal -->
|
||||
<div class="modal fade" id="quickAddIndividualPartModal" tabindex="-1" aria-labelledby="quickAddModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="quickAddModalLabel">Add to Individual Parts Inventory</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="quickAddForm">
|
||||
<!-- Part info (read-only) -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Part</label>
|
||||
<div class="d-flex align-items-center">
|
||||
<img id="quickAddPartImage" src="" alt="" style="max-width: 100px; max-height: 100px;" class="me-3">
|
||||
<div>
|
||||
<div id="quickAddPartName" class="fw-bold"></div>
|
||||
<div id="quickAddPartNumber" class="text-muted small"></div>
|
||||
<div id="quickAddPartColor" class="text-muted small"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="quickAddPart" name="part">
|
||||
<input type="hidden" id="quickAddColor" name="color">
|
||||
|
||||
<!-- Quantity -->
|
||||
<div class="mb-3">
|
||||
<label for="quickAddQuantity" class="form-label">Quantity <span class="text-danger">*</span></label>
|
||||
<input type="number" class="form-control" id="quickAddQuantity" name="quantity" value="1" min="1" required>
|
||||
</div>
|
||||
|
||||
<!-- Storage (optional) -->
|
||||
<div class="mb-3">
|
||||
<label for="quickAddStorage" class="form-label">Storage (optional)</label>
|
||||
<select class="form-select" id="quickAddStorage" name="storage">
|
||||
<option value="">-- No storage --</option>
|
||||
{% for storage in brickset_storages %}
|
||||
<option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Purchase Location (optional) -->
|
||||
<div class="mb-3">
|
||||
<label for="quickAddPurchaseLocation" class="form-label">Purchase Location (optional)</label>
|
||||
<select class="form-select" id="quickAddPurchaseLocation" name="purchase_location">
|
||||
<option value="">-- No purchase location --</option>
|
||||
{% for location in brickset_purchase_locations %}
|
||||
<option value="{{ location.fields.id }}">{{ location.fields.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Description/Notes (optional) -->
|
||||
<div class="mb-3">
|
||||
<label for="quickAddDescription" class="form-label">Notes (optional)</label>
|
||||
<textarea class="form-control" id="quickAddDescription" name="description" rows="2"></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" id="quickAddSubmit">Add to Inventory</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapseStorage" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapseStorage">
|
||||
<i class="ri-archive-2-line"></i> Sets by Storage
|
||||
<i class="ri-archive-2-line"></i> Storage
|
||||
<i class="ri-arrow-down-s-line float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
@@ -286,8 +286,8 @@
|
||||
<tr>
|
||||
<th>Storage Location</th>
|
||||
<th class="text-center">Sets</th>
|
||||
<th class="text-center">Parts</th>
|
||||
<th class="text-center">Minifigures</th>
|
||||
<th class="text-center">Parts (lots, individual parts)</th>
|
||||
<th class="text-center">Minifigures (individual minifigures)</th>
|
||||
<th class="text-center">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -295,7 +295,7 @@
|
||||
{% for storage in storage_statistics %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('set.list', storage=storage.storage_id) }}" class="text-decoration-none">
|
||||
<a href="{{ url_for('storage.details', id=storage.storage_id) }}" class="text-decoration-none">
|
||||
{{ storage.storage_name or 'Unknown' }}
|
||||
</a>
|
||||
</td>
|
||||
@@ -303,10 +303,10 @@
|
||||
<small class="text-dark">{{ storage.set_count }}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-dark">{{ "{:,}".format(storage.total_parts) }}</small>
|
||||
<small class="text-dark">{{ "{:,}".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 %}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<small class="text-dark">{{ storage.total_minifigures }}</small>
|
||||
<small class="text-dark">{{ storage.total_minifigures }}{% if storage.individual_minifig_count %} ({{ storage.individual_minifig_count }} individual){% endif %}</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if storage.total_value %}
|
||||
@@ -335,7 +335,7 @@
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<a class="text-decoration-none text-dark" data-bs-toggle="collapse" href="#collapsePurchase" role="button" aria-expanded="{{ 'true' if config['STATISTICS_DEFAULT_EXPANDED'] else 'false' }}" aria-controls="collapsePurchase">
|
||||
<i class="ri-shopping-cart-line"></i> Sets by Purchase Location
|
||||
<i class="ri-shopping-cart-line"></i> Purchase Location
|
||||
<i class="ri-arrow-down-s-line float-end"></i>
|
||||
</a>
|
||||
</h5>
|
||||
@@ -359,7 +359,7 @@
|
||||
{% for location in purchase_location_statistics %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('set.list', purchase_location=location.location_id) }}" class="text-decoration-none">
|
||||
<a href="{{ url_for('purchase_location.details', id=location.location_id) }}" class="text-decoration-none">
|
||||
{{ location.location_name or 'Unknown' }}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
@@ -6,10 +6,28 @@
|
||||
{{ card.header(item, item.fields.name, solo=solo, icon='archive-2-line') }}
|
||||
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
|
||||
{{ 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 %}
|
||||
</div>
|
||||
{% if solo %}
|
||||
<div class="accordion accordion-flush border-top" id="storage-details">
|
||||
{{ 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 %}
|
||||
</div>
|
||||
<div class="card-footer"></div>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user