forked from FrederikBaerentsen/BrickTracker
Compare commits
28 Commits
release/1.4
...
v1.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cb3e96e21 | |||
| e94b8d2de5 | |||
| 4074aa7c4c | |||
| f262411dc4 | |||
| 711833e5de | |||
| 12dead4ded | |||
| 0e3ba26010 | |||
| 6177187103 | |||
| 5c0daed160 | |||
| 66bbed3597 | |||
| d3a014765b | |||
| 665441c5ac | |||
| d751a3d0af | |||
| 1b077e86b1 | |||
| ef6bdc823d | |||
| 0567d9817f | |||
| fa9e0c3765 | |||
| 69318e7b0b | |||
| 9caeebd82e | |||
| 16f11a3465 | |||
| 57e01f9fb4 | |||
| 754b57f6f4 | |||
| 82cd083294 | |||
| 79bc5243eb | |||
| 126fb1e5cb | |||
| 2536cbe170 | |||
| c7d582c908 | |||
| 79e450ed28 |
+2
-5
@@ -32,11 +32,8 @@ LICENSE
|
||||
**/__pycache__
|
||||
*.pyc
|
||||
|
||||
# Git
|
||||
.git
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
# Hidden directories
|
||||
.?*
|
||||
|
||||
# Dev
|
||||
test-server.sh
|
||||
|
||||
@@ -36,3 +36,6 @@ vitepress/
|
||||
# Local data
|
||||
offline/
|
||||
data/
|
||||
|
||||
# Hidden folders
|
||||
.?*
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## 1.4.1
|
||||
|
||||
### Enhancements
|
||||
|
||||
- **Added per-column header filters to the Parts table on the set details page**: The main Parts table now has an always-visible filter row under the column headers
|
||||
- Name: case-insensitive substring search
|
||||
- Color: dropdown populated from the colors present in the current results
|
||||
- Missing / Damaged: All / With / Without dropdowns
|
||||
- Checked: All / Checked / Unchecked
|
||||
- Filters combine (AND) and re-evaluate live as missing/damaged values or checkboxes change. Bulk actions (mark all missing, check all, etc.) only affect the rows visible under the active filter. Scoped to the main Parts table only; per-minifigure tables stay sort-only
|
||||
- **"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
|
||||
@@ -36,6 +72,19 @@
|
||||
- Notes display as an info alert box below badges when enabled
|
||||
- Both settings can be toggled in Admin -> Live Settings panel without container restart
|
||||
- Fixed consolidated SQL query to include description field for proper notes display in server-side pagination
|
||||
- **Fixed permission denied when running as non-root user** (Issue #138): Resolved container startup failure when using `user:` directive in docker-compose
|
||||
- Added `chmod -R a+rX /app` to Dockerfile to ensure all files are readable regardless of build environment
|
||||
- Added commented `user:` example in `compose.yaml` to document non-root support
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- **Parts default order column names changed**: The `BK_PARTS_DEFAULT_ORDER` environment variable now uses `"combined"` instead of `"bricktracker_parts"` for column references
|
||||
- If you have a custom `BK_PARTS_DEFAULT_ORDER` setting, update column references:
|
||||
- `"bricktracker_parts"."spare"` → `"combined"."spare"`
|
||||
- `"bricktracker_parts"."part"` → `"combined"."part"`
|
||||
- `"bricktracker_parts"."quantity"` → `"combined"."quantity"`
|
||||
- Or remove the custom setting to use the new defaults
|
||||
- See `.env.sample` for the full list of available column names
|
||||
|
||||
### New Features
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
# Bricktracker
|
||||
COPY . .
|
||||
|
||||
# Ensure all files are readable by non-root users (supports user: directive in compose)
|
||||
RUN chmod -R a+rX /app
|
||||
|
||||
# Set executable permissions for entrypoint script
|
||||
RUN chmod +x entrypoint.sh
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ A web application for organizing and tracking LEGO sets, parts, and minifigures.
|
||||
|
||||
<a href="https://www.buymeacoffee.com/frederikb" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="41" width="174"></a>
|
||||
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=48JEEKLCGB8DJ"><img src="./docs/images/blue.svg" height="40"></a>
|
||||
|
||||
## Features
|
||||
|
||||
- Track multiple LEGO sets with their parts and minifigures
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -94,6 +94,36 @@ class BrickSet(RebrickableSet):
|
||||
)
|
||||
self.fields.purchase_location = purchase_location.fields.id
|
||||
|
||||
# Save the purchase date
|
||||
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
|
||||
|
||||
# Save the purchase price
|
||||
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 the description/notes
|
||||
description = data.get('description', None)
|
||||
if description == '':
|
||||
description = None
|
||||
self.fields.description = description
|
||||
|
||||
# Insert into database (deferred - will execute at final commit)
|
||||
# All operations are atomic - if anything fails, nothing is committed
|
||||
self.insert(commit=False)
|
||||
@@ -105,6 +135,13 @@ class BrickSet(RebrickableSet):
|
||||
owner = BrickSetOwnerList.get(id)
|
||||
owner.update_set_state(self, state=True, commit=False)
|
||||
|
||||
# Save the statuses (deferred - will execute at final commit)
|
||||
statuses: list[str] = list(data.get('statuses', []))
|
||||
|
||||
for id in statuses:
|
||||
status = BrickSetStatusList.get(id)
|
||||
status.update_set_state(self, state=True, commit=False)
|
||||
|
||||
# Save the tags (deferred - will execute at final commit)
|
||||
tags: list[str] = list(data.get('tags', []))
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2,10 +2,16 @@ INSERT OR IGNORE INTO "bricktracker_sets" (
|
||||
"id",
|
||||
"set",
|
||||
"storage",
|
||||
"purchase_location"
|
||||
"purchase_location",
|
||||
"purchase_date",
|
||||
"purchase_price",
|
||||
"description"
|
||||
) VALUES (
|
||||
:id,
|
||||
:set,
|
||||
:storage,
|
||||
:purchase_location
|
||||
:purchase_location,
|
||||
:purchase_date,
|
||||
:purchase_price,
|
||||
:description
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,6 +4,7 @@ from flask_login import login_required
|
||||
from ..configuration_list import BrickConfigurationList
|
||||
from .exceptions import exception_handler
|
||||
from ..set_list import set_metadata_lists
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
from ..socket import MESSAGES
|
||||
|
||||
add_page = Blueprint('add', __name__, url_prefix='/add')
|
||||
@@ -21,6 +22,7 @@ def add() -> str:
|
||||
path=current_app.config['SOCKET_PATH'],
|
||||
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||
messages=MESSAGES,
|
||||
brickset_statuses=BrickSetStatusList.list(all=True),
|
||||
**set_metadata_lists()
|
||||
)
|
||||
|
||||
@@ -38,6 +40,7 @@ def bulk() -> str:
|
||||
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||
messages=MESSAGES,
|
||||
bulk=True,
|
||||
brickset_statuses=BrickSetStatusList.list(all=True),
|
||||
**set_metadata_lists()
|
||||
)
|
||||
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -3,6 +3,8 @@ services:
|
||||
container_name: BrickTracker
|
||||
restart: unless-stopped
|
||||
image: gitea.baerentsen.space/frederikbaerentsen/bricktracker:latest
|
||||
# Optional: Run as non-root user (uncomment to enable)
|
||||
# user: "1000:1000"
|
||||
ports:
|
||||
- "3333:3333"
|
||||
volumes:
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1050 200" style="enable-background:new 0 0 1050 200;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#009CDE;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{filter:url(#Adobe_OpacityMaskFilter);}
|
||||
.st3{opacity:0.678;mask:url(#e_1_);fill:#FFFFFF;enable-background:new ;}
|
||||
.st4{filter:url(#Adobe_OpacityMaskFilter_1_);}
|
||||
.st5{opacity:0.696;mask:url(#g_1_);fill:#FFFFFF;enable-background:new ;}
|
||||
.st6{filter:url(#Adobe_OpacityMaskFilter_2_);}
|
||||
.st7{mask:url(#i_1_);fill:#FFFFFF;}
|
||||
</style>
|
||||
<g id="button">
|
||||
<path id="blue" class="st0" d="M1018,200H32c-17.7,0-32-14.3-32-32L0,32C0,14.3,14.3,0,32,0l986,0c17.7,0,32,14.3,32,32v136
|
||||
C1050,185.7,1035.7,200,1018,200z"/>
|
||||
</g>
|
||||
<g id="donate">
|
||||
<g>
|
||||
<path class="st1" d="M73.3,110.6c0-13.1,6.8-21.4,17.2-21.4c5.9,0,10.6,3.1,12.8,7.7h0.2V79.3c0-3.3,2-5.1,4.8-5.1
|
||||
s4.9,1.8,4.9,5.1v47.9c0,3.1-2,4.9-4.8,4.9s-4.9-1.7-4.9-4.9V124h-0.2c-2,4.8-6.4,8-12.9,8C80,132,73.3,123.8,73.3,110.6z
|
||||
M103.5,110.6c0-8.1-4-13.4-10.2-13.4c-6.3,0-10.2,5.2-10.2,13.4c0,8.2,3.9,13.4,10.2,13.4C99.5,124,103.5,118.8,103.5,110.6z"/>
|
||||
<path class="st1" d="M205.5,131.7c-3.3,0-5.2-2-5.2-5.7v-19.7c0-5.4-2.7-8.5-7.5-8.5c-5.4,0-8.7,3.9-8.7,10.4v17.8
|
||||
c0,3.6-1.9,5.7-5.2,5.7s-5.2-2-5.2-5.7V95c0-3.4,2-5.5,5.1-5.5c3.1,0,4.9,1.8,5,5.2v2.5h0.5c1.6-4.9,6.3-8,12.3-8
|
||||
c8.8,0,13.8,5.2,13.8,14.3v22.5C210.6,129.7,208.8,131.7,205.5,131.7z"/>
|
||||
<path class="st1" d="M232,131.9c-8.1,0-13.6-5.1-13.6-12.6c0-7.7,5.7-12.4,15.1-12.4h9.8v-3.4c0-4.2-2.6-6.4-7.3-6.4
|
||||
c-3.1,0-5.4,1.2-7.7,3.2c-1.2,0.9-2.3,1.4-3.9,1.4c-2.3,0-3.7-1.5-3.7-3.6c0-2.4,1.7-4.9,5.3-6.7c2.6-1.4,6.1-2.1,10.6-2.1
|
||||
c10.9,0,17,5.2,17,14.3v22.7c0,3.5-1.8,5.5-5,5.5c-2.7,0-4.5-1.5-4.8-4.2v-1.4h-0.5C241.1,129.8,237,131.9,232,131.9z
|
||||
M235.4,124.5c4.5,0,7.9-3,7.9-7v-4.3h-8c-4.2,0-6.7,2-6.7,5.4C228.6,122.1,231.4,124.5,235.4,124.5z"/>
|
||||
<path class="st1" d="M266.3,98.3h-2.1c-2.8,0-4.4-1.5-4.4-4s1.6-4,4.4-4h2.3v-5.4c0-3.6,1.8-5.5,5-5.5c3.2,0,5,2,5,5.5v5.4h4.1
|
||||
c2.8,0,4.4,1.5,4.4,4s-1.6,4-4.4,4h-4v20.9c0,3.8,1,5,4.1,5c1.1,0,1.8-0.2,2.7-0.2c1.8,0,3,1.2,3,3.1c0,1.5-0.7,2.8-2.1,3.7
|
||||
c-1.4,0.9-3.6,1.4-6.4,1.4c-8.2,0-11.7-3.4-11.7-11.9V98.3z"/>
|
||||
<path class="st1" d="M291.3,108c0-11.3,7.5-19,18.6-19c10.5,0,18,7.5,18,17.7c0,4.6-1.3,6.2-5.2,6.2h-21v1.8
|
||||
c0,5.8,3.7,9.5,9.8,9.5c3.2,0,5.5-0.8,7.3-2.3c2-1.5,2.7-2.1,4.3-2.1c2.1,0,3.6,1.5,3.6,3.8c0,1.8-1.1,3.6-3.1,5
|
||||
c-2.7,2.3-7.5,3.7-12.8,3.7c-12.1,0-19.6-7.1-19.6-18.9V108z M318.1,106.4L318.1,106.4c0-5.7-3.1-9.4-8-9.4
|
||||
c-5.1,0-8.3,3.8-8.3,9.3v0.1H318.1z"/>
|
||||
<path class="st1" d="M359.5,97.1c-0.3-1.2-0.4-1.9-0.4-2.5c0-2.9,2.1-5,5.2-5c2.7,0,4.5,1.8,5,5.1l5.2,25.9h0.5l6.2-24.5
|
||||
c1.2-4.8,2.9-6.6,6.3-6.6c3.4,0,5.2,1.8,6.4,6.6l6.3,24.5h0.5l5.4-25.9c0.6-3.2,2.4-5.1,5-5.1c3.1,0,5,2,5,4.7
|
||||
c0,0.6-0.2,1.5-0.5,2.7l-7.2,26.5c-1.7,6.2-3.7,8.2-7.3,8.2c-3.9,0-5.9-2-7.5-8.2l-6-21.8h-0.4l-5.8,21.8
|
||||
c-1.6,6.3-3.4,8.2-7.3,8.2c-3.8,0-5.9-2.1-7.5-8.2L359.5,97.1z"/>
|
||||
<path class="st1" d="M426.9,74c3.4,0,5.9,2.3,5.9,5.4s-2.5,5.4-5.9,5.4c-3.3,0-5.8-2.3-5.8-5.4S423.6,74,426.9,74z M426.9,131.7
|
||||
c-3.2,0-5.1-2-5.1-5.7V95.2c0-3.8,2-5.7,5.2-5.7c3.2,0,5.1,2,5.1,5.7v30.9C432.1,129.8,430.1,131.7,426.9,131.7z"/>
|
||||
<path class="st1" d="M444,98.3h-2.1c-2.8,0-4.4-1.5-4.4-4s1.6-4,4.4-4h2.3v-5.4c0-3.6,1.8-5.5,5-5.5c3.2,0,5,2,5,5.5v5.4h4.1
|
||||
c2.8,0,4.4,1.5,4.4,4s-1.6,4-4.4,4h-4v20.9c0,3.8,1,5,4.1,5c1.1,0,1.8-0.2,2.7-0.2c1.8,0,3,1.2,3,3.1c0,1.5-0.7,2.8-2.1,3.7
|
||||
c-1.4,0.9-3.6,1.4-6.4,1.4c-8.2,0-11.7-3.4-11.7-11.9V98.3z"/>
|
||||
<path class="st1" d="M501.1,131.7c-3.3,0-5.2-2-5.2-5.7v-19.6c0-5.4-2.8-8.5-7.6-8.5c-5.3,0-8.8,4-8.8,10.4v17.8
|
||||
c0,3.6-1.9,5.7-5.2,5.7s-5.1-2-5.1-5.7V80.6c0-3.6,1.8-5.7,5.2-5.7c3.3,0,5.1,2.1,5.1,5.7v16.6h0.5c1.4-4.7,6.5-8,12.4-8
|
||||
c8.6,0,13.8,5.4,13.8,14.4v22.4C506.3,129.7,504.4,131.7,501.1,131.7z"/>
|
||||
</g>
|
||||
<path id="heart_2_" class="st1" d="M154.3,89.2c-4.3,0-8.5,2.1-10.8,6.7c-2.3-4.6-6.5-6.7-10.8-6.7c-6.7,0-12.7,4.7-12.7,13.1
|
||||
c0,14.2,19.4,21.4,23.5,29.9c4.1-8.5,23.5-15.7,23.5-29.9C167,93.3,160.3,89.2,154.3,89.2z M150.2,99.2c3,0,5.7,2,5.7,6
|
||||
c0,6.3-8.3,10.6-12.8,15.2c-4.4-4.6-12.8-8.9-12.8-15.2c0-3.9,2.6-6,5.7-6c4.1,0,6.2,4.6,7,6.8C144,103.8,146.2,99.2,150.2,99.2z"
|
||||
/>
|
||||
</g>
|
||||
<g id="PayPal">
|
||||
<path class="st1" d="M888.7,89.7c-1.2,8-7.3,8-13.2,8h-3.4l2.4-14.9c0.1-0.9,0.9-1.6,1.8-1.6h1.5c4,0,7.8,0,9.7,2.3
|
||||
C888.8,84.9,889.1,86.9,888.7,89.7 M886.1,68.9h-22.2c-1.5,0-2.8,1.1-3,2.6l-9,56.9c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h11.4
|
||||
c1.1,0,2-0.8,2.1-1.8l2.5-16.1c0.2-1.5,1.5-2.6,3.1-2.6h7c14.6,0,23.1-7.1,25.3-21.1c1-6.1,0-11-2.8-14.3
|
||||
C899.2,70.9,893.6,68.9,886.1,68.9 M730.2,89.7c-1.2,8-7.3,8-13.2,8h-3.4l2.4-14.9c0.1-0.9,0.9-1.6,1.8-1.6h1.5c4,0,7.8,0,9.7,2.3
|
||||
C730.3,84.9,730.7,86.9,730.2,89.7 M727.7,68.9h-22.2c-1.5,0-2.8,1.1-3.1,2.6l-9,56.9c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h10.6
|
||||
c1.5,0,2.8-1.1,3-2.6l2.4-15.4c0.2-1.5,1.5-2.6,3-2.6h7c14.6,0,23.1-7.1,25.3-21.1c1-6.1,0-11-2.8-14.3
|
||||
C740.7,70.9,735.1,68.9,727.7,68.9 M779.3,110.2c-1,6.1-5.9,10.2-12,10.2c-3.1,0-5.6-1-7.1-2.9c-1.6-1.9-2.2-4.5-1.7-7.5
|
||||
c1-6,5.9-10.2,11.9-10.2c3,0,5.5,1,7.1,2.9C779.1,104.5,779.7,107.2,779.3,110.2 M794.1,89.5h-10.6c-0.9,0-1.7,0.7-1.8,1.6l-0.5,3
|
||||
l-0.7-1.1c-2.3-3.3-7.4-4.5-12.6-4.5c-11.8,0-21.8,8.9-23.7,21.4c-1,6.2,0.4,12.2,4,16.3c3.2,3.8,7.9,5.4,13.4,5.4
|
||||
c9.5,0,14.7-6.1,14.7-6.1l-0.5,3c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h9.6c1.5,0,2.8-1.1,3-2.6l5.8-36.4c0.2-1-0.5-2-1.5-2.1
|
||||
C794.3,89.5,794.2,89.5,794.1,89.5 M937.7,110.2c-1,6.1-5.9,10.2-12,10.2c-3.1,0-5.6-1-7.1-2.9c-1.6-1.9-2.2-4.5-1.7-7.5
|
||||
c1-6,5.9-10.2,11.9-10.2c3,0,5.5,1,7.1,2.9C937.5,104.5,938.2,107.2,937.7,110.2 M952.5,89.5h-10.6c-0.9,0-1.7,0.7-1.8,1.6l-0.5,3
|
||||
l-0.7-1.1c-2.3-3.3-7.4-4.5-12.6-4.5c-11.8,0-21.8,8.9-23.7,21.4c-1,6.2,0.4,12.2,4,16.3c3.2,3.8,7.9,5.4,13.4,5.4
|
||||
c9.5,0,14.7-6.1,14.7-6.1l-0.5,3c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h9.6c1.5,0,2.8-1.1,3-2.6l5.8-36.4c0.2-1-0.5-2-1.5-2.1
|
||||
C952.7,89.5,952.6,89.5,952.5,89.5 M850.7,89.5H840c-1,0-2,0.5-2.6,1.4l-14.7,21.7l-6.3-20.9c-0.4-1.3-1.6-2.2-3-2.2H803
|
||||
c-1,0-1.9,0.8-1.9,1.9c0,0.2,0,0.4,0.1,0.6l11.8,34.5L802,142.1c-0.9,1.2,0,2.9,1.5,2.9h10.7c1,0,2-0.5,2.5-1.3l35.6-51.3
|
||||
C853.1,91.1,852.2,89.5,850.7,89.5 M965.1,70.5l-9.1,58c-0.2,1,0.5,2,1.5,2.1c0.1,0,0.2,0,0.3,0h9.2c1.5,0,2.8-1.1,3-2.6l9-56.9
|
||||
c0.2-1-0.5-2-1.5-2.1c-0.1,0-0.2,0-0.3,0h-10.3C966,68.9,965.2,69.6,965.1,70.5"/>
|
||||
<g>
|
||||
<defs>
|
||||
<filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="575" y="47" width="83.1" height="98">
|
||||
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<mask maskUnits="userSpaceOnUse" x="575" y="47" width="83.1" height="98" id="e_1_">
|
||||
<g class="st2">
|
||||
<path id="d_1_" class="st1" d="M575,47h83.1v98H575V47z"/>
|
||||
</g>
|
||||
</mask>
|
||||
<path class="st3" d="M649.9,71.9c1.2-7.6,0-12.8-4.1-17.5c-4.5-5.2-12.7-7.4-23.2-7.4H592c-2.1,0-4,1.6-4.3,3.7L575,131.1
|
||||
c-0.2,1.4,0.7,2.8,2.2,3c0.1,0,0.3,0,0.4,0h18.8l-1.3,8.2c-0.2,1.2,0.7,2.4,1.9,2.6c0.1,0,0.2,0,0.4,0h15.9c1.9,0,3.5-1.4,3.8-3.2
|
||||
l0.2-0.8l3-18.9l0.2-1c0.3-1.9,1.9-3.2,3.8-3.2h2.4c15.4,0,27.4-6.2,30.9-24.3c1.5-7.5,0.7-13.8-3.2-18.3
|
||||
C653,73.9,651.6,72.8,649.9,71.9"/>
|
||||
<defs>
|
||||
<filter id="Adobe_OpacityMaskFilter_1_" filterUnits="userSpaceOnUse" x="575" y="47" width="75.3" height="87.1">
|
||||
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<mask maskUnits="userSpaceOnUse" x="575" y="47" width="75.3" height="87.1" id="g_1_">
|
||||
<g class="st4">
|
||||
<path id="f_1_" class="st1" d="M575,47h75.3v87.1H575V47z"/>
|
||||
</g>
|
||||
</mask>
|
||||
<path class="st5" d="M649.9,71.9c1.2-7.6,0-12.8-4.1-17.5c-4.5-5.2-12.7-7.4-23.2-7.4H592c-2.1,0-4,1.6-4.3,3.7L575,131.1
|
||||
c-0.2,1.4,0.7,2.8,2.2,3c0.1,0,0.3,0,0.4,0h18.8l4.7-30l-0.1,0.9c0.3-2.1,2.1-3.7,4.3-3.7h8.9c17.6,0,31.3-7.1,35.3-27.8
|
||||
C649.7,73.1,649.8,72.5,649.9,71.9"/>
|
||||
<defs>
|
||||
<filter id="Adobe_OpacityMaskFilter_2_" filterUnits="userSpaceOnUse" x="575" y="47" width="75.3" height="87.1">
|
||||
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<mask maskUnits="userSpaceOnUse" x="575" y="47" width="75.3" height="87.1" id="i_1_">
|
||||
<g class="st6">
|
||||
<path id="h_1_" class="st1" d="M575,47h75.3v87.1H575V47z"/>
|
||||
</g>
|
||||
</mask>
|
||||
<path class="st7" d="M606.2,72c0.3-1.9,1.9-3.2,3.8-3.2h23.9c2.8,0,5.5,0.2,7.9,0.6c0.7,0.1,1.4,0.2,2,0.4
|
||||
c0.9,0.2,1.9,0.5,2.8,0.8c1.1,0.4,2.2,0.8,3.3,1.4c1.2-7.6,0-12.8-4.1-17.5c-4.5-5.2-12.7-7.4-23.2-7.4H592c-2.1,0-4,1.6-4.3,3.7
|
||||
L575,131.1c-0.2,1.4,0.7,2.8,2.2,3c0.1,0,0.3,0,0.4,0h18.8l4.7-30L606.2,72L606.2,72z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.7 KiB |
+13
-63
@@ -1,80 +1,30 @@
|
||||
// Add page - handles both sets and individual minifigures
|
||||
// Server auto-routes fig- numbers to IndividualMinifigure via LOAD_SET / IMPORT_SET
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Get template data from data attributes
|
||||
document.querySelectorAll('[data-add-date="true"]').forEach(el => {
|
||||
new Datepicker(el, {
|
||||
buttonClass: 'btn',
|
||||
format: 'yyyy/mm/dd',
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
@@ -55,6 +55,10 @@ class BrickChanger {
|
||||
this.html_clear.addEventListener("click", ((changer) => (e) => {
|
||||
changer.html_element.value = "";
|
||||
changer.change();
|
||||
// change() only POSTs to the server; dispatch an input event so
|
||||
// client-side listeners (e.g. the parts table filter) react to
|
||||
// the programmatic clear the same way they do to typing.
|
||||
changer.html_element.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
})(this));
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,9 @@ class PartsBulkOperations {
|
||||
// Find all rows in this accordion
|
||||
const rows = accordionElement.querySelectorAll('tbody tr');
|
||||
rows.forEach(row => {
|
||||
// Skip rows hidden by an active header filter
|
||||
if (row.classList.contains('parts-filtered-out')) return;
|
||||
|
||||
// Find the quantity cell (usually 4th column)
|
||||
const quantityCell = row.cells[3]; // Index 3 for quantity column
|
||||
const missingInput = row.querySelector('input[id*="-missing-"]');
|
||||
@@ -134,6 +137,7 @@ class PartsBulkOperations {
|
||||
|
||||
const missingInputs = accordionElement.querySelectorAll('input[id*="-missing-"]');
|
||||
missingInputs.forEach(input => {
|
||||
if (input.closest('tr')?.classList.contains('parts-filtered-out')) return;
|
||||
if (input.value !== '') {
|
||||
input.value = '';
|
||||
// Trigger change event to activate BrickChanger
|
||||
@@ -148,6 +152,7 @@ class PartsBulkOperations {
|
||||
|
||||
const checkboxes = accordionElement.querySelectorAll('input[id*="-checked-"][type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.closest('tr')?.classList.contains('parts-filtered-out')) return;
|
||||
if (!checkbox.checked) {
|
||||
checkbox.checked = true;
|
||||
// Trigger change event to activate BrickChanger
|
||||
@@ -162,6 +167,7 @@ class PartsBulkOperations {
|
||||
|
||||
const checkboxes = accordionElement.querySelectorAll('input[id*="-checked-"][type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (checkbox.closest('tr')?.classList.contains('parts-filtered-out')) return;
|
||||
if (checkbox.checked) {
|
||||
checkbox.checked = false;
|
||||
// Trigger change event to activate BrickChanger
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// Per-column header filters for the accordion parts tables on the set details page.
|
||||
class PartsTableFilter {
|
||||
constructor(table) {
|
||||
this.table = table;
|
||||
this.body = table.querySelector('tbody');
|
||||
this.rows = Array.from(this.body ? this.body.querySelectorAll('tr') : []);
|
||||
|
||||
this.nameInput = table.querySelector('[data-parts-filter="name"]');
|
||||
this.colorSelect = table.querySelector('[data-parts-filter="color"]');
|
||||
this.missingSelect = table.querySelector('[data-parts-filter="missing"]');
|
||||
this.damagedSelect = table.querySelector('[data-parts-filter="damaged"]');
|
||||
this.checkedSelect = table.querySelector('[data-parts-filter="checked"]');
|
||||
this.clearButton = table.querySelector('[data-parts-filter-clear]');
|
||||
|
||||
this.setupListeners();
|
||||
this.apply();
|
||||
}
|
||||
|
||||
setupListeners() {
|
||||
const debouncedApply = this.debounce(() => this.apply(), 150);
|
||||
|
||||
// Name search is debounced so typing stays smooth on big tables.
|
||||
if (this.nameInput) {
|
||||
this.nameInput.addEventListener('input', debouncedApply);
|
||||
}
|
||||
|
||||
// Dropdowns re-filter immediately.
|
||||
[this.colorSelect, this.missingSelect, this.damagedSelect, this.checkedSelect].forEach(select => {
|
||||
if (select) {
|
||||
select.addEventListener('change', () => this.apply());
|
||||
}
|
||||
});
|
||||
|
||||
if (this.clearButton) {
|
||||
this.clearButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.clear();
|
||||
});
|
||||
}
|
||||
|
||||
// Live re-evaluation: editing a missing/damaged value or toggling a
|
||||
// checkbox in the body should re-run the active filter.
|
||||
if (this.body) {
|
||||
this.body.addEventListener('change', () => this.apply());
|
||||
this.body.addEventListener('input', debouncedApply);
|
||||
}
|
||||
}
|
||||
|
||||
cellText(row, col) {
|
||||
const cell = row.querySelector(`[data-col="${col}"]`);
|
||||
if (!cell) {
|
||||
return '';
|
||||
}
|
||||
const sort = cell.getAttribute('data-sort');
|
||||
return (sort !== null ? sort : cell.textContent).trim().toLowerCase();
|
||||
}
|
||||
|
||||
// Numeric value for missing/damaged, reading the live input when present.
|
||||
cellNumber(row, col) {
|
||||
const cell = row.querySelector(`[data-col="${col}"]`);
|
||||
if (!cell) {
|
||||
return 0;
|
||||
}
|
||||
const input = cell.querySelector('input');
|
||||
const raw = input ? input.value : (cell.getAttribute('data-sort') || cell.textContent);
|
||||
const value = parseInt(raw, 10);
|
||||
return Number.isNaN(value) ? 0 : value;
|
||||
}
|
||||
|
||||
rowChecked(row) {
|
||||
const cell = row.querySelector('[data-col="checked"]');
|
||||
if (!cell) {
|
||||
return false;
|
||||
}
|
||||
const checkbox = cell.querySelector('input[type="checkbox"]');
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
}
|
||||
return cell.getAttribute('data-sort') === '1';
|
||||
}
|
||||
|
||||
matchesNumberFilter(value, mode) {
|
||||
if (mode === 'with') {
|
||||
return value > 0;
|
||||
}
|
||||
if (mode === 'without') {
|
||||
return value === 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
apply() {
|
||||
const nameTerm = (this.nameInput ? this.nameInput.value : '').trim().toLowerCase();
|
||||
const colorValue = (this.colorSelect ? this.colorSelect.value : '').trim().toLowerCase();
|
||||
const missingMode = this.missingSelect ? this.missingSelect.value : '';
|
||||
const damagedMode = this.damagedSelect ? this.damagedSelect.value : '';
|
||||
const checkedValue = this.checkedSelect ? this.checkedSelect.value : '';
|
||||
|
||||
// Colors available among rows that pass every filter except color, so
|
||||
// the picker only offers colors still present in the current results.
|
||||
const availableColors = new Set();
|
||||
let visibleCount = 0;
|
||||
|
||||
this.rows.forEach(row => {
|
||||
const passNonColor =
|
||||
(!nameTerm || this.cellText(row, 'name').includes(nameTerm)) &&
|
||||
this.matchesNumberFilter(this.cellNumber(row, 'missing'), missingMode) &&
|
||||
this.matchesNumberFilter(this.cellNumber(row, 'damaged'), damagedMode) &&
|
||||
(checkedValue === '' ||
|
||||
(checkedValue === '1' ? this.rowChecked(row) : !this.rowChecked(row)));
|
||||
|
||||
if (passNonColor) {
|
||||
const colorCell = row.querySelector('[data-col="color"]');
|
||||
if (colorCell) {
|
||||
const name = colorCell.textContent.trim();
|
||||
if (name) {
|
||||
availableColors.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const passColor = !colorValue || this.cellText(row, 'color') === colorValue;
|
||||
const visible = passNonColor && passColor;
|
||||
row.classList.toggle('parts-filtered-out', !visible);
|
||||
if (visible) {
|
||||
visibleCount += 1;
|
||||
}
|
||||
});
|
||||
|
||||
this.updateColorOptions(availableColors);
|
||||
this.updateEmptyState(visibleCount);
|
||||
}
|
||||
|
||||
// Rebuild the color dropdown from the colors currently in the results,
|
||||
// keeping the user's current selection if it is still available.
|
||||
updateColorOptions(colors) {
|
||||
if (!this.colorSelect) {
|
||||
return;
|
||||
}
|
||||
const current = this.colorSelect.value;
|
||||
const sorted = Array.from(colors).sort((a, b) => a.localeCompare(b));
|
||||
const options = ['<option value="">All colors</option>']
|
||||
.concat(sorted.map(name => `<option value="${name.toLowerCase()}">${name}</option>`));
|
||||
this.colorSelect.innerHTML = options.join('');
|
||||
// Restore selection (the <option> values are lowercased to match).
|
||||
this.colorSelect.value = sorted.some(n => n.toLowerCase() === current) ? current : '';
|
||||
}
|
||||
|
||||
updateEmptyState(visibleCount) {
|
||||
if (!this.body) {
|
||||
return;
|
||||
}
|
||||
if (!this.emptyRow) {
|
||||
const columns = this.table.querySelectorAll('thead tr:first-child th').length || 1;
|
||||
this.emptyRow = document.createElement('tr');
|
||||
this.emptyRow.className = 'parts-filter-empty no-sort';
|
||||
this.emptyRow.innerHTML = `<td colspan="${columns}" class="text-center text-body-secondary py-3">No matching parts</td>`;
|
||||
this.body.appendChild(this.emptyRow);
|
||||
}
|
||||
this.emptyRow.style.display = visibleCount === 0 ? '' : 'none';
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.nameInput) { this.nameInput.value = ''; }
|
||||
[this.colorSelect, this.missingSelect, this.damagedSelect, this.checkedSelect].forEach(select => {
|
||||
if (select) {
|
||||
select.value = '';
|
||||
}
|
||||
});
|
||||
this.apply();
|
||||
}
|
||||
|
||||
debounce(fn, wait) {
|
||||
let timer = null;
|
||||
return (...args) => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => fn.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('table[data-parts-filterable="true"]').forEach(table => {
|
||||
new PartsTableFilter(table);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -16,9 +16,13 @@ class BrickSetSocket 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_location = document.getElementById(`${id}-purchase-location`);
|
||||
this.html_purchase_price = document.getElementById(`${id}-purchase-price`);
|
||||
this.html_statuses = document.getElementById(`${id}-statuses`);
|
||||
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`);
|
||||
@@ -181,12 +185,44 @@ class BrickSetSocket extends BrickSocket {
|
||||
this.html_progress_bar.scrollIntoView();
|
||||
}
|
||||
|
||||
// Grab the purchase date
|
||||
let purchase_date = '';
|
||||
if (this.html_purchase_date) {
|
||||
purchase_date = this.html_purchase_date.value;
|
||||
}
|
||||
|
||||
// Grab the purchase price
|
||||
let purchase_price = '';
|
||||
if (this.html_purchase_price) {
|
||||
purchase_price = this.html_purchase_price.value;
|
||||
}
|
||||
|
||||
// Grab the statuses
|
||||
const statuses = [];
|
||||
if (this.html_statuses) {
|
||||
this.html_statuses.querySelectorAll('input').forEach(input => {
|
||||
if (input.checked) {
|
||||
statuses.push(input.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Grab the description/notes
|
||||
let description = '';
|
||||
if (this.html_description) {
|
||||
description = this.html_description.value;
|
||||
}
|
||||
|
||||
this.socket.emit(this.messages.IMPORT_SET, {
|
||||
set: (set !== undefined) ? set : this.html_input.value,
|
||||
owners: owners,
|
||||
purchase_date: purchase_date,
|
||||
purchase_location: purchase_location,
|
||||
purchase_price: purchase_price,
|
||||
statuses: statuses,
|
||||
storage: storage,
|
||||
tags: tags,
|
||||
description: description,
|
||||
refresh: this.refresh
|
||||
});
|
||||
} else {
|
||||
@@ -361,10 +397,22 @@ class BrickSetSocket 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_location) {
|
||||
this.html_purchase_location.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_purchase_price) {
|
||||
this.html_purchase_price.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_statuses) {
|
||||
this.html_statuses.querySelectorAll('input').forEach(input => input.disabled = !enabled);
|
||||
}
|
||||
|
||||
if (this.html_storage) {
|
||||
this.html_storage.disabled = !enabled;
|
||||
}
|
||||
@@ -373,6 +421,10 @@ class BrickSetSocket extends BrickSocket {
|
||||
this.html_tags.querySelectorAll('input').forEach(input => input.disabled = !enabled);
|
||||
}
|
||||
|
||||
if (this.html_description) {
|
||||
this.html_description.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_card_confirm) {
|
||||
this.html_card_confirm.disabled = !enabled;
|
||||
}
|
||||
|
||||
@@ -331,3 +331,18 @@
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
}
|
||||
|
||||
/* Accordion parts table header filters */
|
||||
.parts-filter-row > th {
|
||||
padding: 0.35rem 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.parts-filter-row .parts-filter-input,
|
||||
.parts-filter-row .parts-filter-select {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
tr.parts-filtered-out {
|
||||
display: none;
|
||||
}
|
||||
|
||||
+81
-50
@@ -63,30 +63,43 @@
|
||||
</div>
|
||||
<h6 class="border-bottom mt-2">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.
|
||||
{% if brickset_owners | length %}
|
||||
{{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }}
|
||||
<div id="add-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="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if brickset_owners | length %}
|
||||
{{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }}
|
||||
<div id="add-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="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ 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-purchase-location">{{ name }}</label>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{{ accordion.header('Purchase', 'purchase', 'metadata', 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 configure how it is displayed in the set 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">
|
||||
<label for="add-purchase-date" class="form-label visually-hidden">Date</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-1"><i class="ri-calendar-line me-1"></i><span class="ms-1 d-none d-md-inline"> Date</span></span>
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="add-purchase-date" data-add-date="true" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="add-purchase-price" class="form-label visually-hidden">Price</label>
|
||||
<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" id="add-purchase-price" autocomplete="off">
|
||||
{% if config['PURCHASE_CURRENCY'] %}<span class="input-group-text d-none d-md-inline px-1">{{ config['PURCHASE_CURRENCY'] }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if brickset_purchase_locations | length %}
|
||||
<div class="col-12 flex-grow-1">
|
||||
<label for="add-purchase-location" class="form-label visually-hidden">Location</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-1"><i class="ri-building-line me-1"></i><span class="ms-1 d-none d-md-inline"> Location</span></span>
|
||||
<select id="add-purchase-location" class="form-select" autocomplete="off">
|
||||
<option value="" selected><i>None</i></option>
|
||||
{% for purchase_location in brickset_purchase_locations %}
|
||||
@@ -94,35 +107,53 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{% if brickset_storages | length %}
|
||||
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
|
||||
<label class="visually-hidden" for="add-storage">{{ name }}</label>
|
||||
<div class="input-group">
|
||||
<select id="add-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-2-line') }}
|
||||
<div id="add-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="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Notes', 'notes', 'metadata', icon='sticky-note-line') }}
|
||||
<textarea class="form-control" id="add-description" rows="3" placeholder="Optional notes..."></textarea>
|
||||
{{ accordion.footer() }}
|
||||
{% if brickset_storages | length %}
|
||||
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
|
||||
<label class="visually-hidden" for="add-storage">Storage</label>
|
||||
<div class="input-group">
|
||||
<select id="add-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_statuses | length %}
|
||||
{{ accordion.header('Statuses', 'statuses', 'metadata', icon='checkbox-circle-line') }}
|
||||
<div id="add-statuses">
|
||||
{% for status in brickset_statuses %}
|
||||
{% with id=status.as_dataset() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ status.fields.id }}" id="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ status.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{% if brickset_tags | length %}
|
||||
{{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
|
||||
<div id="add-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="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -191,6 +191,7 @@
|
||||
<script src="{{ url_for('static', filename='scripts/sets.js') }}"></script>
|
||||
{% endif %}
|
||||
{% if request.endpoint == 'set.details' %}
|
||||
<script src="{{ url_for('static', filename='scripts/parts-table-filter.js') }}"></script>
|
||||
<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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table(table_collection, title, id, parent, target, quantity=none, icon=none, image=none, alt=none, details=none, read_only=none, hamburger_menu=none) %}
|
||||
{% macro table(table_collection, title, id, parent, target, quantity=none, icon=none, image=none, alt=none, details=none, read_only=none, hamburger_menu=none, filters=false) %}
|
||||
{% set size=table_collection | length %}
|
||||
{% if size %}
|
||||
{{ header(title, id, parent, quantity=quantity, icon=icon, class='p-0', image=image, alt=alt, hamburger_menu=hamburger_menu) }}
|
||||
@@ -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, read_only=read_only, hamburger_menu=hamburger_menu %}
|
||||
{% with solo=true, all=false, accordion_id=id, read_only=read_only, hamburger_menu=hamburger_menu, filters=filters %}
|
||||
{% include target %}
|
||||
{% endwith %}
|
||||
{{ footer() }}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% macro header(image=true, color=false, parts=false, quantity=false, missing=true, missing_parts=false, damaged=true, damaged_parts=false, sets=false, minifigures=false, checked=false, hamburger_menu=false, accordion_id='') %}
|
||||
{% macro header(image=true, color=false, parts=false, quantity=false, missing=true, missing_parts=false, damaged=true, damaged_parts=false, sets=false, minifigures=false, checked=false, hamburger_menu=false, accordion_id='', filters=false) %}
|
||||
<thead>
|
||||
<tr>
|
||||
{% if image %}
|
||||
@@ -59,6 +59,72 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if filters %}
|
||||
<tr class="parts-filter-row">
|
||||
{% if image %}
|
||||
<th class="no-sort"></th>
|
||||
{% endif %}
|
||||
<th class="no-sort">
|
||||
<input type="search" class="form-control form-control-sm parts-filter-input" data-parts-filter="name" placeholder="Search…" aria-label="Filter by name" autocomplete="off">
|
||||
</th>
|
||||
{% if color %}
|
||||
<th class="no-sort">
|
||||
<select class="form-select form-select-sm parts-filter-select" data-parts-filter="color" aria-label="Filter by color">
|
||||
<option value="">All colors</option>
|
||||
</select>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% if parts %}
|
||||
<th class="no-sort"></th>
|
||||
{% endif %}
|
||||
{% if quantity %}
|
||||
<th class="no-sort"></th>
|
||||
{% endif %}
|
||||
{% if missing and not config['HIDE_TABLE_MISSING_PARTS'] %}
|
||||
<th class="no-sort">
|
||||
<select class="form-select form-select-sm parts-filter-select" data-parts-filter="missing" aria-label="Filter by missing">
|
||||
<option value="">All</option>
|
||||
<option value="with">With missing</option>
|
||||
<option value="without">Without missing</option>
|
||||
</select>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% if damaged and not config['HIDE_TABLE_DAMAGED_PARTS'] %}
|
||||
<th class="no-sort">
|
||||
<select class="form-select form-select-sm parts-filter-select" data-parts-filter="damaged" aria-label="Filter by damaged">
|
||||
<option value="">All</option>
|
||||
<option value="with">With damaged</option>
|
||||
<option value="without">Without damaged</option>
|
||||
</select>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% if sets %}
|
||||
<th class="no-sort"></th>
|
||||
{% endif %}
|
||||
{% if minifigures %}
|
||||
<th class="no-sort"></th>
|
||||
{% endif %}
|
||||
{% if checked and not config['HIDE_TABLE_CHECKED_PARTS'] %}
|
||||
<th class="no-sort">
|
||||
<select class="form-select form-select-sm parts-filter-select" data-parts-filter="checked" aria-label="Filter by checked">
|
||||
<option value="">All</option>
|
||||
<option value="1">Checked</option>
|
||||
<option value="0">Unchecked</option>
|
||||
</select>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% 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'] %}
|
||||
{% 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 class="no-sort text-end">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary parts-filter-clear" data-parts-filter-clear title="Clear filters"><i class="ri-filter-off-line"></i></button>
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
{% import 'macro/table.html' as table %}
|
||||
|
||||
<div class="table-responsive-sm">
|
||||
<table data-table="{% if all %}true{% endif %}" class="table table-striped align-middle {% if not all %}sortable mb-0{% endif %}" {% if all %}id="parts"{% endif %}>
|
||||
{{ table.header(color=true, quantity=not no_quantity, sets=all, minifigures=all, checked=not all and not read_only, hamburger_menu=not all and not read_only, accordion_id=accordion_id|default('')) }}
|
||||
<table data-table="{% if all %}true{% endif %}" class="table table-striped align-middle {% if not all %}sortable mb-0{% endif %}" {% if all %}id="parts"{% endif %} {% if filters %}data-parts-filterable="true"{% endif %}>
|
||||
{{ table.header(color=true, quantity=not no_quantity, sets=all, minifigures=all, checked=not all and not read_only, hamburger_menu=not all and not read_only, accordion_id=accordion_id|default(''), filters=filters|default(false)) }}
|
||||
<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 }}">
|
||||
<td data-sort="{{ item.fields.name }}" data-col="name">
|
||||
<a class="text-reset" href="{{ item.url() }}">{{ item.fields.name }}</a>
|
||||
{% if item.fields.spare %}<span class="badge rounded-pill text-bg-warning fw-normal"><i class="ri-loop-left-line"></i> Spare</span>{% endif %}
|
||||
{% if all %}
|
||||
@@ -16,7 +16,7 @@
|
||||
{{ table.bricklink(item) }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td data-sort="{{ item.fields.color_name }}">
|
||||
<td data-sort="{{ item.fields.color_name }}" data-col="color">
|
||||
{% 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>
|
||||
@@ -28,12 +28,12 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if not config['HIDE_TABLE_MISSING_PARTS'] %}
|
||||
<td data-sort="{{ item.fields.total_missing }}" class="table-td-input">
|
||||
<td data-sort="{{ item.fields.total_missing }}" data-col="missing" class="table-td-input">
|
||||
{{ form.input('Missing', item.fields.id, item.html_id('missing'), item.url_for_problem('missing'), item.fields.total_missing, all=all, read_only=read_only) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
|
||||
<td data-sort="{{ item.fields.total_damaged }}" class="table-td-input">
|
||||
<td data-sort="{{ item.fields.total_damaged }}" data-col="damaged" class="table-td-input">
|
||||
{{ form.input('Damaged', item.fields.id, item.html_id('damaged'), item.url_for_problem('damaged'), item.fields.total_damaged, all=all, read_only=read_only) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
@@ -42,7 +42,7 @@
|
||||
<td>{{ item.fields.total_minifigures }}</td>
|
||||
{% else %}
|
||||
{% if not config['HIDE_TABLE_CHECKED_PARTS'] and not read_only %}
|
||||
<td data-sort="{{ item.fields.checked | default(0) | int }}" class="table-td-input">
|
||||
<td data-sort="{{ item.fields.checked | default(0) | int }}" data-col="checked" 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 %}
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line', hamburger_menu=g.login.is_authenticated())}}
|
||||
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line', hamburger_menu=g.login.is_authenticated(), filters=true)}}
|
||||
{% for minifigure in item.minifigures() %}
|
||||
{{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.figure, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.figure, details=minifigure.url(), hamburger_menu=g.login.is_authenticated())}}
|
||||
{% endfor %}
|
||||
|
||||
+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