Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f262411dc4 | |||
| 711833e5de | |||
| 12dead4ded | |||
| 0e3ba26010 | |||
| 6177187103 | |||
| 5c0daed160 | |||
| 66bbed3597 | |||
| d3a014765b | |||
| 665441c5ac | |||
| d751a3d0af | |||
| 1b077e86b1 | |||
| ef6bdc823d | |||
| 0567d9817f | |||
| fa9e0c3765 | |||
| 69318e7b0b | |||
| 9caeebd82e |
@@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## 1.4.1
|
||||
|
||||
### Enhancements
|
||||
|
||||
- **"Reset to Defaults" confirmation now uses a Bootstrap modal instead of a browser dialog**: Replaced the native browser `confirm()` popup with a consistent Bootstrap modal matching the style of BrickTracker
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Fixed prices on the Statistics page being rounded to whole numbers** (Issue #146): All price values now display with two decimal places (`%.2f`) instead of being rounded to whole numbers (`%.0f`)
|
||||
|
||||
- **Fixed "Reset to Defaults" blanking all settings instead of restoring them** (Issue #149a, branch `bugfix/issue-149a`): "Reset to Defaults" was clearing all fields to empty/false instead of populating them with their actual default values
|
||||
- `resetToDefaults()` now reads from `window.DEFAULT_CONFIG` and restores each field to its proper default, matching the same logic used on initial page load
|
||||
- **Fixed `BK_INSTRUCTIONS_ALLOWED_EXTENSIONS` being treated as a string instead of a list** (Issue #149b, branch `bugfix/issue-149b`): When this setting was saved via the admin panel, it was stored and cast as a plain string rather than a list, causing it to be iterated character by character (e.g. `['.', 'p', 'd', 'f']` instead of `['.pdf']`)
|
||||
- Added `allowed_extensions` to the list-type keyword detection in `_cast_value()`, matching the existing pattern used for `badge_order` settings
|
||||
- **Fixed crash when importing sets containing minifigures or parts with no image on Rebrickable** (Issue #149c, branch `bugfix/issue-149c`): Adding or refreshing a set would fail entirely if any minifigure or part had no image URL, with error `Invalid URL '': No scheme supplied`
|
||||
- Rebrickable returns an empty string (not `None`) for missing images; normalize empty strings to `None` at the point of ingestion in `rebrickable_minifigure.py` and `individual_minifigure.py`, matching the existing pattern in `rebrickable_set.py`
|
||||
- Updated `rebrickable_image.py` to treat empty strings the same as `None` throughout, falling back to the configured nil placeholder image
|
||||
- Note: the originally reported sets could no longer reproduce the crash (images may have since been added on Rebrickable), so this fix is based on assumptions only
|
||||
- **Fixed previously added set being re-added when adding an individual minifigure** (Issue #150, branch `bugfix/issue-150`): After adding a set, entering a `fig-` ID and confirming would add the previous set again instead of the minifigure, if user did not reload inbetween.
|
||||
- `add.js` was creating a second `BrickMinifigureSocket` with its own listeners on the same button and input as `BrickSetSocket`, causing duplicate socket events and cross-socket state confusion
|
||||
- **Fixed purchase date, price, and notes not being saved when adding an individual minifigure** (Issue #151, branch `bugfix/issue-151`): Filling in purchase date, price, or notes before clicking Add had no effect, only purchase location was saved
|
||||
- `BrickMinifigureSocket` was missing references to `#add-purchase-date`, `#add-purchase-price`, and `#add-description`, so those fields were never read or included in the socket emit
|
||||
- The backend already supported all three fields. This was just a frontend error
|
||||
- **Fixed purchase date and price not being converted when adding an individual minifigure** (Issue #151 follow-up): Purchase date was stored as a raw `YYYY/MM/DD` string and price as a raw string instead of a Unix epoch float and float respectively, causing them to be silently dropped from statistics aggregations
|
||||
- `IndividualMinifigure.download()` now mirrors the conversion logic already present in `BrickSet.download()`: date parsed via `datetime.strptime` to timestamp, price cast to `float`
|
||||
- **Fixed a price of 0 being treated as no price** (Issue #153): Setting a purchase price of `0` on sets, individual minifigures, or parts was indistinguishable from having no price set at all
|
||||
- Replaced truthiness checks (`if price`, `price or ''`) with explicit `None` checks throughout badge display, management input fields, and inline price update endpoints
|
||||
- **Fixed deleting a wish with an owner assigned** (Issue #152, branch `bugfix/issue-152`): Resolved foreign key constraint error when removing a set from the wishlist that had an owner assigned
|
||||
- Wish owners are now deleted before the wish itself, respecting the FK constraint
|
||||
|
||||
## 1.4
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -192,7 +192,7 @@ class ConfigManager:
|
||||
def _cast_value(self, var_name: str, value: Any) -> Any:
|
||||
"""Cast value to appropriate type based on variable name"""
|
||||
# List variables (admin sections, badge order) - Check this FIRST before boolean check
|
||||
if any(keyword in var_name.lower() for keyword in ['sections', 'badge_order']):
|
||||
if any(keyword in var_name.lower() for keyword in ['sections', 'badge_order', 'allowed_extensions']):
|
||||
if isinstance(value, str):
|
||||
return [section.strip() for section in value.split(',') if section.strip()]
|
||||
elif isinstance(value, list):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Any, Self, TYPE_CHECKING
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -67,8 +68,25 @@ class IndividualMinifigure(RebrickableMinifigure):
|
||||
self.fields.purchase_location = purchase_location.fields.id if purchase_location else None
|
||||
|
||||
# Save purchase date and price
|
||||
self.fields.purchase_date = data.get('purchase_date', None)
|
||||
self.fields.purchase_price = data.get('purchase_price', None)
|
||||
purchase_date = data.get('purchase_date', None)
|
||||
if purchase_date == '':
|
||||
purchase_date = None
|
||||
if purchase_date is not None:
|
||||
try:
|
||||
purchase_date = datetime.strptime(purchase_date, '%Y/%m/%d').timestamp()
|
||||
except Exception:
|
||||
purchase_date = None
|
||||
self.fields.purchase_date = purchase_date
|
||||
|
||||
purchase_price = data.get('purchase_price', None)
|
||||
if purchase_price == '':
|
||||
purchase_price = None
|
||||
if purchase_price is not None:
|
||||
try:
|
||||
purchase_price = float(purchase_price)
|
||||
except Exception:
|
||||
purchase_price = None
|
||||
self.fields.purchase_price = purchase_price
|
||||
|
||||
# Save quantity and description
|
||||
self.fields.quantity = int(data.get('quantity', 1))
|
||||
@@ -543,6 +561,6 @@ class IndividualMinifigure(RebrickableMinifigure):
|
||||
'figure': str(data['set_num']),
|
||||
'number': int(number),
|
||||
'name': str(data['set_name']),
|
||||
'image': data.get('set_img_url'),
|
||||
'image': str(data['set_img_url']) if data.get('set_img_url') else None,
|
||||
'number_of_parts': int(data.get('num_parts', 0)),
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class RebrickableImage(object):
|
||||
|
||||
# Get the URL (this handles nil images via url() method)
|
||||
url = self.url()
|
||||
if url is None:
|
||||
if not url:
|
||||
return
|
||||
|
||||
# Grab the image
|
||||
@@ -88,7 +88,7 @@ class RebrickableImage(object):
|
||||
return self.part.fields.image_id
|
||||
|
||||
if self.minifigure is not None:
|
||||
if self.minifigure.fields.image is None:
|
||||
if not self.minifigure.fields.image:
|
||||
return RebrickableImage.nil_minifigure_name()
|
||||
else:
|
||||
return self.minifigure.fields.figure
|
||||
@@ -113,13 +113,13 @@ class RebrickableImage(object):
|
||||
# Return the url depending on the objects provided
|
||||
def url(self, /) -> str:
|
||||
if self.part is not None:
|
||||
if self.part.fields.image is None:
|
||||
if not self.part.fields.image:
|
||||
return current_app.config['REBRICKABLE_IMAGE_NIL']
|
||||
else:
|
||||
return self.part.fields.image
|
||||
|
||||
if self.minifigure is not None:
|
||||
if self.minifigure.fields.image is None:
|
||||
if not self.minifigure.fields.image:
|
||||
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
|
||||
else:
|
||||
return self.minifigure.fields.image
|
||||
|
||||
@@ -110,5 +110,5 @@ class RebrickableMinifigure(BrickRecord):
|
||||
'number': int(number),
|
||||
'name': str(data['set_name']),
|
||||
'quantity': int(data['quantity']),
|
||||
'image': data['set_img_url'],
|
||||
'image': str(data['set_img_url']) if data['set_img_url'] else None,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ SELECT
|
||||
"bricktracker_individual_minifigures"."description",
|
||||
"bricktracker_individual_minifigures"."storage",
|
||||
"bricktracker_individual_minifigures"."purchase_location",
|
||||
"bricktracker_individual_minifigures"."purchase_date",
|
||||
"bricktracker_individual_minifigures"."purchase_price",
|
||||
"rebrickable_minifigures"."number",
|
||||
"rebrickable_minifigures"."name",
|
||||
"rebrickable_minifigures"."image",
|
||||
|
||||
@@ -6,6 +6,8 @@ SELECT
|
||||
"bricktracker_individual_minifigures"."description",
|
||||
"bricktracker_individual_minifigures"."storage",
|
||||
"bricktracker_individual_minifigures"."purchase_location",
|
||||
"bricktracker_individual_minifigures"."purchase_date",
|
||||
"bricktracker_individual_minifigures"."purchase_price",
|
||||
"rebrickable_minifigures"."number",
|
||||
"rebrickable_minifigures"."name",
|
||||
"rebrickable_minifigures"."image",
|
||||
|
||||
@@ -6,6 +6,8 @@ SELECT
|
||||
"bricktracker_individual_minifigures"."description",
|
||||
"bricktracker_individual_minifigures"."storage",
|
||||
"bricktracker_individual_minifigures"."purchase_location",
|
||||
"bricktracker_individual_minifigures"."purchase_date",
|
||||
"bricktracker_individual_minifigures"."purchase_price",
|
||||
"rebrickable_minifigures"."number",
|
||||
"rebrickable_minifigures"."name",
|
||||
"rebrickable_minifigures"."image",
|
||||
|
||||
@@ -6,6 +6,8 @@ SELECT
|
||||
"bricktracker_individual_minifigures"."description",
|
||||
"bricktracker_individual_minifigures"."storage",
|
||||
"bricktracker_individual_minifigures"."purchase_location",
|
||||
"bricktracker_individual_minifigures"."purchase_date",
|
||||
"bricktracker_individual_minifigures"."purchase_price",
|
||||
"rebrickable_minifigures"."number",
|
||||
"rebrickable_minifigures"."name",
|
||||
"rebrickable_minifigures"."image",
|
||||
|
||||
@@ -6,6 +6,8 @@ SELECT
|
||||
"bricktracker_individual_minifigures"."description",
|
||||
"bricktracker_individual_minifigures"."storage",
|
||||
"bricktracker_individual_minifigures"."purchase_location",
|
||||
"bricktracker_individual_minifigures"."purchase_date",
|
||||
"bricktracker_individual_minifigures"."purchase_price",
|
||||
"rebrickable_minifigures"."number",
|
||||
"rebrickable_minifigures"."name",
|
||||
"rebrickable_minifigures"."image",
|
||||
|
||||
@@ -100,6 +100,23 @@ part_lot_stats AS (
|
||||
FROM "bricktracker_individual_part_lots"
|
||||
),
|
||||
|
||||
-- Combined min/max price across all item types (separate CTE to avoid scalar subquery issues in SQLite)
|
||||
all_prices AS (
|
||||
SELECT "purchase_price" AS price FROM "bricktracker_sets" WHERE "purchase_price" IS NOT NULL AND "purchase_price" != ''
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_parts" WHERE "purchase_price" IS NOT NULL AND "purchase_price" != '' AND "lot_id" IS NULL
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_minifigures" WHERE "purchase_price" IS NOT NULL AND "purchase_price" != ''
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_part_lots" WHERE "purchase_price" IS NOT NULL AND "purchase_price" != ''
|
||||
),
|
||||
price_range AS (
|
||||
SELECT
|
||||
MIN(price) AS combined_minimum_cost,
|
||||
MAX(price) AS combined_maximum_cost
|
||||
FROM all_prices
|
||||
),
|
||||
|
||||
-- Rebrickable sets count (for sets we actually own)
|
||||
rebrickable_stats AS (
|
||||
SELECT COUNT(*) AS unique_rebrickable_sets
|
||||
@@ -140,26 +157,9 @@ financial_stats AS (
|
||||
END AS combined_average_cost,
|
||||
|
||||
-- Min/Max price across all item types
|
||||
(SELECT MIN(price) FROM (
|
||||
SELECT "purchase_price" AS price FROM "bricktracker_sets" WHERE "purchase_price" IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_parts" WHERE "purchase_price" IS NOT NULL AND "lot_id" IS NULL
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_minifigures" WHERE "purchase_price" IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_part_lots" WHERE "purchase_price" IS NOT NULL
|
||||
)) AS combined_minimum_cost,
|
||||
|
||||
(SELECT MAX(price) FROM (
|
||||
SELECT "purchase_price" AS price FROM "bricktracker_sets" WHERE "purchase_price" IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_parts" WHERE "purchase_price" IS NOT NULL AND "lot_id" IS NULL
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_minifigures" WHERE "purchase_price" IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT "purchase_price" FROM "bricktracker_individual_part_lots" WHERE "purchase_price" IS NOT NULL
|
||||
)) AS combined_maximum_cost
|
||||
FROM set_stats, individual_part_stats, individual_minifig_stats, part_lot_stats
|
||||
price_range.combined_minimum_cost,
|
||||
price_range.combined_maximum_cost
|
||||
FROM set_stats, individual_part_stats, individual_minifig_stats, part_lot_stats, price_range
|
||||
)
|
||||
|
||||
-- Final select combining all statistics
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DELETE FROM "bricktracker_wishes"
|
||||
WHERE "bricktracker_wishes"."set" IS NOT DISTINCT FROM '{{ set }}';
|
||||
|
||||
DELETE FROM "bricktracker_wish_owners"
|
||||
WHERE "bricktracker_wish_owners"."set" IS NOT DISTINCT FROM '{{ set }}';
|
||||
|
||||
DELETE FROM "bricktracker_wishes"
|
||||
WHERE "bricktracker_wishes"."set" IS NOT DISTINCT FROM '{{ set }}';
|
||||
|
||||
COMMIT;
|
||||
@@ -58,8 +58,8 @@ class BrickStatistics:
|
||||
return {
|
||||
'total_cost': overview.get('combined_total_cost') or 0,
|
||||
'average_cost': overview.get('combined_average_cost') or 0,
|
||||
'minimum_cost': overview.get('combined_minimum_cost') or 0,
|
||||
'maximum_cost': overview.get('combined_maximum_cost') or 0,
|
||||
'minimum_cost': float(overview['combined_minimum_cost']) if overview.get('combined_minimum_cost') not in (None, '') else None,
|
||||
'maximum_cost': float(overview['combined_maximum_cost']) if overview.get('combined_maximum_cost') not in (None, '') else None,
|
||||
'items_with_price': overview.get('total_items_with_price') or 0,
|
||||
'sets_with_price': overview.get('sets_with_price') or 0,
|
||||
'total_sets': overview.get('total_sets') or 0,
|
||||
|
||||
@@ -304,6 +304,13 @@ def update_purchase_date(*, id: str):
|
||||
def update_purchase_price(*, id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
purchase_price = request.json.get('value')
|
||||
if purchase_price is not None and str(purchase_price).strip() != '':
|
||||
try:
|
||||
purchase_price = float(purchase_price)
|
||||
except (ValueError, TypeError):
|
||||
purchase_price = None
|
||||
else:
|
||||
purchase_price = None
|
||||
|
||||
BrickSQL().execute_and_commit(
|
||||
'individual_minifigure/update',
|
||||
@@ -314,7 +321,7 @@ def update_purchase_price(*, id: str):
|
||||
'storage': item.fields.storage,
|
||||
'purchase_location': item.fields.purchase_location if hasattr(item.fields, 'purchase_location') else None,
|
||||
'purchase_date': item.fields.purchase_date if hasattr(item.fields, 'purchase_date') else None,
|
||||
'purchase_price': purchase_price if purchase_price else None,
|
||||
'purchase_price': purchase_price,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ def update_purchase_price(*, id: str):
|
||||
purchase_price = request.json.get('value', '')
|
||||
|
||||
# Convert to float if provided, otherwise None
|
||||
if purchase_price and str(purchase_price).strip():
|
||||
if purchase_price is not None and str(purchase_price).strip() != '':
|
||||
try:
|
||||
price = float(purchase_price)
|
||||
item.update_field('purchase_price', price)
|
||||
@@ -662,7 +662,14 @@ def update_lot_purchase_price(*, lot_id: str):
|
||||
|
||||
from ..sql import BrickSQL
|
||||
sql = BrickSQL()
|
||||
sql.execute_and_commit('individual_part_lot/update/purchase_price', parameters={'purchase_price': purchase_price if purchase_price else None, 'id': lot_id})
|
||||
if purchase_price is not None and str(purchase_price).strip() != '':
|
||||
try:
|
||||
purchase_price = float(purchase_price)
|
||||
except (ValueError, TypeError):
|
||||
purchase_price = None
|
||||
else:
|
||||
purchase_price = None
|
||||
sql.execute_and_commit('individual_part_lot/update/purchase_price', parameters={'purchase_price': purchase_price, 'id': lot_id})
|
||||
|
||||
logger.info('Updated lot {lot_id} purchase_price to: {price}'.format(lot_id=lot_id, price=purchase_price))
|
||||
return jsonify({'success': True})
|
||||
|
||||
+6
-64
@@ -1,6 +1,6 @@
|
||||
// Add page - handles both sets and individual minifigures
|
||||
// Server auto-routes fig- numbers to IndividualMinifigure via LOAD_SET / IMPORT_SET
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Initialize date pickers
|
||||
document.querySelectorAll('[data-add-date="true"]').forEach(el => {
|
||||
new Datepicker(el, {
|
||||
buttonClass: 'btn',
|
||||
@@ -8,81 +8,23 @@ 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: {
|
||||
new BrickSetSocket(
|
||||
'add',
|
||||
addContainer.dataset.path,
|
||||
addContainer.dataset.namespace,
|
||||
{
|
||||
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();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -204,14 +204,20 @@ function setupButtonHandlers() {
|
||||
});
|
||||
}
|
||||
|
||||
// Reset button
|
||||
// Reset button. Opens Bootstrap modal instead of browser confirm()
|
||||
const resetBtn = document.getElementById('config-reset');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
console.log('Reset clicked');
|
||||
if (confirm('Are you sure you want to reset all settings to default values? This action cannot be undone.')) {
|
||||
resetToDefaults();
|
||||
}
|
||||
const modal = new bootstrap.Modal(document.getElementById('resetDefaultsModal'));
|
||||
modal.show();
|
||||
});
|
||||
}
|
||||
|
||||
// Confirm reset inside the modal
|
||||
const confirmResetBtn = document.getElementById('confirm-reset-defaults');
|
||||
if (confirmResetBtn) {
|
||||
confirmResetBtn.addEventListener('click', () => {
|
||||
resetToDefaults();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -285,18 +291,21 @@ function saveLiveConfiguration() {
|
||||
function resetToDefaults() {
|
||||
console.log('Resetting to defaults');
|
||||
|
||||
// Reset all form inputs
|
||||
document.querySelectorAll('.config-toggle, .config-number, .config-text').forEach(input => {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = false;
|
||||
} else {
|
||||
input.value = '';
|
||||
}
|
||||
});
|
||||
Object.keys(window.DEFAULT_CONFIG).forEach(varName => {
|
||||
const defaultValue = window.DEFAULT_CONFIG[varName];
|
||||
|
||||
// Update badges
|
||||
Object.keys(window.CURRENT_CONFIG).forEach(varName => {
|
||||
updateConfigBadge(varName, null);
|
||||
const toggle = document.getElementById(varName);
|
||||
if (toggle && toggle.type === 'checkbox') {
|
||||
toggle.checked = defaultValue === true;
|
||||
}
|
||||
|
||||
document.querySelectorAll(`input[data-var="${varName}"]:not(.config-static)`).forEach(input => {
|
||||
if (input.type !== 'checkbox') {
|
||||
input.value = defaultValue !== null && defaultValue !== undefined ? defaultValue : '';
|
||||
}
|
||||
});
|
||||
|
||||
updateConfigBadge(varName, defaultValue);
|
||||
});
|
||||
|
||||
// Show status message
|
||||
|
||||
@@ -13,9 +13,12 @@ class BrickMinifigureSocket extends BrickSocket {
|
||||
this.html_input = document.getElementById(`${id}-set`);
|
||||
this.html_no_confim = document.getElementById(`${id}-no-confirm`);
|
||||
this.html_owners = document.getElementById(`${id}-owners`);
|
||||
this.html_purchase_date = document.getElementById(`${id}-purchase-date`);
|
||||
this.html_purchase_price = document.getElementById(`${id}-purchase-price`);
|
||||
this.html_purchase_location = document.getElementById(`${id}-purchase-location`);
|
||||
this.html_storage = document.getElementById(`${id}-storage`);
|
||||
this.html_tags = document.getElementById(`${id}-tags`);
|
||||
this.html_description = document.getElementById(`${id}-description`);
|
||||
|
||||
// Card elements
|
||||
this.html_card = document.getElementById(`${id}-card`);
|
||||
@@ -98,12 +101,28 @@ class BrickMinifigureSocket extends BrickSocket {
|
||||
});
|
||||
}
|
||||
|
||||
// Grab the purchase location
|
||||
// Grab the purchase info
|
||||
let purchase_date = null;
|
||||
if (this.html_purchase_date) {
|
||||
purchase_date = this.html_purchase_date.value || null;
|
||||
}
|
||||
|
||||
let purchase_price = null;
|
||||
if (this.html_purchase_price) {
|
||||
purchase_price = this.html_purchase_price.value || null;
|
||||
}
|
||||
|
||||
let purchase_location = null;
|
||||
if (this.html_purchase_location) {
|
||||
purchase_location = this.html_purchase_location.value;
|
||||
}
|
||||
|
||||
// Grab the description (notes)
|
||||
let description = '';
|
||||
if (this.html_description) {
|
||||
description = this.html_description.value || '';
|
||||
}
|
||||
|
||||
// Grab the storage
|
||||
let storage = null;
|
||||
if (this.html_storage) {
|
||||
@@ -129,9 +148,12 @@ class BrickMinifigureSocket extends BrickSocket {
|
||||
this.socket.emit(this.messages.IMPORT_MINIFIGURE, {
|
||||
figure: (figure !== undefined) ? figure : this.html_input.value,
|
||||
owners: owners,
|
||||
purchase_date: purchase_date,
|
||||
purchase_price: purchase_price,
|
||||
purchase_location: purchase_location,
|
||||
storage: storage,
|
||||
tags: tags,
|
||||
description: description,
|
||||
quantity: 1
|
||||
});
|
||||
} else {
|
||||
@@ -235,10 +257,22 @@ class BrickMinifigureSocket extends BrickSocket {
|
||||
this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
|
||||
}
|
||||
|
||||
if (this.html_purchase_date) {
|
||||
this.html_purchase_date.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_purchase_price) {
|
||||
this.html_purchase_price.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_purchase_location) {
|
||||
this.html_purchase_location.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_description) {
|
||||
this.html_description.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_storage) {
|
||||
this.html_storage.disabled = !enabled;
|
||||
}
|
||||
|
||||
@@ -1019,6 +1019,32 @@
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
|
||||
<!-- Reset to Defaults Confirmation Modal -->
|
||||
<div class="modal fade" id="resetDefaultsModal" tabindex="-1" aria-labelledby="resetDefaultsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-warning">
|
||||
<h5 class="modal-title" id="resetDefaultsModalLabel">
|
||||
<i class="ri-restart-line"></i> Reset to Defaults
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>This will reset all live settings to their default values.</p>
|
||||
<p class="text-muted small"><i class="ri-information-line"></i> Changes are not saved until you click <strong>Save All Changes</strong>.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="ri-close-line"></i> Cancel
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning" id="confirm-reset-defaults" data-bs-dismiss="modal">
|
||||
<i class="ri-restart-line"></i> Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Initialize Configuration Data -->
|
||||
<script type="text/javascript">
|
||||
window.CURRENT_CONFIG = {
|
||||
|
||||
@@ -35,12 +35,12 @@
|
||||
{% if management_read_only %}
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-1"><i class="ri-wallet-3-line me-1"></i><span class="ms-1 d-none d-md-inline"> Price</span></span>
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.purchase_price or '' }}" disabled autocomplete="off">
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ (item.fields.purchase_price if item.fields.purchase_price is not none else '') }}" disabled autocomplete="off">
|
||||
<span class="input-group-text d-none d-md-inline px-1">{{ config['PURCHASE_CURRENCY'] }}</span>
|
||||
<span class="input-group-text ri-prohibited-line px-1"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ 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') }}
|
||||
{{ form.input('Price', item.fields.id, 'purchase_price', url_for('individual_minifigure.update_purchase_price', id=item.fields.id), (item.fields.purchase_price if item.fields.purchase_price is not none else ''), suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
|
||||
@@ -53,12 +53,12 @@
|
||||
{% if management_read_only %}
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-1"><i class="ri-wallet-3-line me-1"></i><span class="ms-1 d-none d-md-inline"> Price</span></span>
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.purchase_price or '' }}" disabled autocomplete="off">
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ (item.fields.purchase_price if item.fields.purchase_price is not none else '') }}" disabled autocomplete="off">
|
||||
<span class="input-group-text d-none d-md-inline px-1">{{ config['PURCHASE_CURRENCY'] }}</span>
|
||||
<span class="input-group-text ri-prohibited-line px-1"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ 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') }}
|
||||
{{ 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 if item.fields.purchase_price is not none else ''), suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
@@ -112,12 +112,12 @@
|
||||
{% if management_read_only %}
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-1"><i class="ri-wallet-3-line me-1"></i><span class="ms-1 d-none d-md-inline"> Price</span></span>
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.purchase_price or '' }}" disabled autocomplete="off">
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ (item.fields.purchase_price if item.fields.purchase_price is not none else '') }}" disabled autocomplete="off">
|
||||
<span class="input-group-text d-none d-md-inline px-1">{{ config['PURCHASE_CURRENCY'] }}</span>
|
||||
<span class="input-group-text ri-prohibited-line px-1"></span>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ 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') }}
|
||||
{{ form.input('Price', item.fields.id, 'purchase_price', item.url_for_purchase_price(), (item.fields.purchase_price if item.fields.purchase_price is not none else ''), suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-12 flex-grow-1">
|
||||
|
||||
@@ -118,12 +118,14 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro purchase_price(price, solo=false, last=false) %}
|
||||
{% if last %}
|
||||
{% set tooltip=price %}
|
||||
{% else %}
|
||||
{% set text=price %}
|
||||
{% if price is not none %}
|
||||
{% if last %}
|
||||
{% set tooltip = price | string %}
|
||||
{% else %}
|
||||
{% set text = price | string %}
|
||||
{% endif %}
|
||||
{{ badge(check=true, solo=solo, last=last, color='light border', icon='wallet-3-line', text=text, tooltip=tooltip, collapsible='Price:') }}
|
||||
{% endif %}
|
||||
{{ badge(check=price, solo=solo, last=last, color='light border', icon='wallet-3-line', text=text, tooltip=tooltip, collapsible='Price:') }}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro set(set, solo=false, last=false, url=None, id=None) %}
|
||||
@@ -294,7 +296,7 @@
|
||||
{% if not 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 %}
|
||||
{% elif item.fields.purchase_price is defined and item.fields.purchase_price is not none %}
|
||||
{{ purchase_price(item.fields.purchase_price, solo=solo, last=last) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
|
||||
<div class="input-group">
|
||||
{% if icon %}<span class="input-group-text px-1"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value is not none and value != '' %}{{ value }}{% endif %}"
|
||||
{% if g.login.is_authenticated() %}
|
||||
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
|
||||
{% if date %}data-changer-date="true"{% endif %}
|
||||
|
||||
+16
-12
@@ -99,7 +99,11 @@
|
||||
<div class="col-6">
|
||||
<div class="text-center">
|
||||
<div class="text-dark small">Range</div>
|
||||
{% if financial_summary.minimum_cost is not none and financial_summary.maximum_cost is not none %}
|
||||
<div class="fw-bold">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.minimum_cost) }} - {{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(financial_summary.maximum_cost) }}</div>
|
||||
{% else %}
|
||||
<div class="fw-bold">-</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,7 +249,7 @@
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if theme.total_spent %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(theme.total_spent) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(theme.total_spent) }}</small>
|
||||
{% else %}
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
@@ -309,8 +313,8 @@
|
||||
<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 %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(storage.total_value) }}</small>
|
||||
{% if storage.total_value is not none %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(storage.total_value) }}</small>
|
||||
{% else %}
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
@@ -380,15 +384,15 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if location.avg_price %}
|
||||
{% if location.avg_price is not none %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.avg_price) }}</small>
|
||||
{% else %}
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if location.min_price and location.max_price %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(location.min_price) }}-{{ "%.0f"|format(location.max_price) }}</small>
|
||||
{% if location.min_price is not none and location.max_price is not none %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(location.min_price) }}-{{ "%.2f"|format(location.max_price) }}</small>
|
||||
{% else %}
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
@@ -460,14 +464,14 @@
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if year.total_spent %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.total_spent) }}</small>
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(year.total_spent) }}</small>
|
||||
{% else %}
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if year.avg_price_per_set %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
|
||||
{% if year.avg_price_per_set is not none %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(year.avg_price_per_set) }}</small>
|
||||
{% else %}
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
@@ -542,8 +546,8 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if year.avg_price_per_set %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year.avg_price_per_set) }}</small>
|
||||
{% if year.avg_price_per_set is not none %}
|
||||
<small class="text-dark">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(year.avg_price_per_set) }}</small>
|
||||
{% else %}
|
||||
<small class="text-dark">-</small>
|
||||
{% endif %}
|
||||
@@ -560,7 +564,7 @@
|
||||
<small class="text-dark">Peak Year</small><br>
|
||||
{% if year_summary.peak_spending_year %}
|
||||
<strong>{{ year_summary.peak_spending_year }}</strong>
|
||||
<small class="text-success d-block">{{ config['PURCHASE_CURRENCY'] }}{{ "%.0f"|format(year_summary.max_spending) }}</small>
|
||||
<small class="text-success d-block">{{ config['PURCHASE_CURRENCY'] }}{{ "%.2f"|format(year_summary.max_spending) }}</small>
|
||||
{% else %}
|
||||
<small class="text-dark">N/A</small>
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user