Added filter/sort/search to /minifigures and /parts

This commit is contained in:
2025-09-15 20:28:39 +02:00
parent 7bfbbbf298
commit 1cac17a420
19 changed files with 822 additions and 15 deletions

View File

@@ -21,6 +21,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
# Queries
all_query: str = 'minifigure/list/all'
all_by_owner_query: str = 'minifigure/list/all_by_owner'
damaged_part_query: str = 'minifigure/list/damaged_part'
last_query: str = 'minifigure/list/last'
missing_part_query: str = 'minifigure/list/missing_part'
@@ -42,6 +43,16 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
return self
# Load all minifigures by owner
def all_by_owner(self, owner_id: str | None = None, /) -> Self:
# Save the owner_id parameter
self.fields.owner_id = owner_id
# Load the minifigures from the database
self.list(override_query=self.all_by_owner_query)
return self
# Minifigures with a part damaged part
def damaged_part(self, part: str, color: int, /) -> Self:
# Save the parameters to the fields
@@ -83,11 +94,17 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
else:
brickset = None
# Prepare template context for owner filtering
context = {}
if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None:
context['owner_id'] = self.fields.owner_id
# Load the sets from the database
for record in super().select(
override_query=override_query,
order=order,
limit=limit,
**context
):
minifigure = BrickMinifigure(brickset=brickset, record=record)
@@ -132,6 +149,10 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
if self.brickset is not None:
parameters['id'] = self.brickset.fields.id
# Add owner_id parameter for owner filtering
if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None:
parameters['owner_id'] = self.fields.owner_id
return parameters
# Import the minifigures from Rebrickable

View File

@@ -23,6 +23,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
# Queries
all_query: str = 'part/list/all'
all_by_owner_query: str = 'part/list/all_by_owner'
different_color_query = 'part/list/with_different_color'
last_query: str = 'part/list/last'
minifigure_query: str = 'part/list/from_minifigure'
@@ -46,6 +47,35 @@ class BrickPartList(BrickRecordList[BrickPart]):
return self
# Load all parts by owner
def all_by_owner(self, owner_id: str | None = None, /) -> Self:
# Save the owner_id parameter
self.fields.owner_id = owner_id
# Load the parts from the database
self.list(override_query=self.all_by_owner_query)
return self
# Load all parts with filters (owner and/or color)
def all_filtered(self, owner_id: str | None = None, color_id: str | None = None, /) -> Self:
# Save the filter parameters
if owner_id is not None:
self.fields.owner_id = owner_id
if color_id is not None:
self.fields.color_id = color_id
# Choose query based on whether owner filtering is needed
if owner_id and owner_id != 'all':
query = self.all_by_owner_query
else:
query = self.all_query
# Load the parts from the database
self.list(override_query=query)
return self
# Base part list
def list(
self,
@@ -69,11 +99,19 @@ class BrickPartList(BrickRecordList[BrickPart]):
else:
minifigure = None
# Prepare template context for filtering
context_vars = {}
if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None:
context_vars['owner_id'] = self.fields.owner_id
if hasattr(self.fields, 'color_id') and self.fields.color_id is not None:
context_vars['color_id'] = self.fields.color_id
# Load the sets from the database
for record in super().select(
override_query=override_query,
order=order,
limit=limit,
**context_vars
):
part = BrickPart(
brickset=brickset,

View File

@@ -0,0 +1,71 @@
{% extends 'minifigure/base/base.sql' %}
{% block total_missing %}
SUM(IFNULL("problem_join"."total_missing", 0)) AS "total_missing",
{% endblock %}
{% block total_damaged %}
SUM(IFNULL("problem_join"."total_damaged", 0)) AS "total_damaged",
{% endblock %}
{% block total_quantity %}
{% if owner_id and owner_id != 'all' %}
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN IFNULL("bricktracker_minifigures"."quantity", 0) ELSE 0 END) AS "total_quantity",
{% else %}
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity",
{% endif %}
{% endblock %}
{% block total_sets %}
{% if owner_id and owner_id != 'all' %}
COUNT(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_minifigures"."id" ELSE NULL END) AS "total_sets"
{% else %}
COUNT("bricktracker_minifigures"."id") AS "total_sets"
{% endif %}
{% endblock %}
{% block join %}
-- Join with sets to get owner information
INNER JOIN "bricktracker_sets"
ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
-- Left join with set owners (using dynamic columns)
LEFT JOIN "bricktracker_set_owners"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
-- LEFT JOIN + SELECT to avoid messing the total
LEFT JOIN (
SELECT
"bricktracker_parts"."id",
"bricktracker_parts"."figure",
{% if owner_id and owner_id != 'all' %}
SUM(CASE WHEN "owner_parts"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."missing" ELSE 0 END) AS "total_missing",
SUM(CASE WHEN "owner_parts"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."damaged" ELSE 0 END) AS "total_damaged"
{% else %}
SUM("bricktracker_parts"."missing") AS "total_missing",
SUM("bricktracker_parts"."damaged") AS "total_damaged"
{% endif %}
FROM "bricktracker_parts"
INNER JOIN "bricktracker_sets" AS "parts_sets"
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "parts_sets"."id"
LEFT JOIN "bricktracker_set_owners" AS "owner_parts"
ON "parts_sets"."id" IS NOT DISTINCT FROM "owner_parts"."id"
WHERE "bricktracker_parts"."figure" IS NOT NULL
GROUP BY
"bricktracker_parts"."id",
"bricktracker_parts"."figure"
) "problem_join"
ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "problem_join"."id"
AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "problem_join"."figure"
{% endblock %}
{% block where %}
{% if owner_id and owner_id != 'all' %}
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
{% endif %}
{% endblock %}
{% block group %}
GROUP BY
"rebrickable_minifigures"."figure"
{% endblock %}

View File

@@ -0,0 +1,16 @@
SELECT DISTINCT
"rebrickable_parts"."color_id" AS "color_id",
"rebrickable_parts"."color_name" AS "color_name",
"rebrickable_parts"."color_rgb" AS "color_rgb"
FROM "rebrickable_parts"
INNER JOIN "bricktracker_parts"
ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
{% if owner_id and owner_id != 'all' %}
INNER JOIN "bricktracker_sets"
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
INNER JOIN "bricktracker_set_owners"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
{% endif %}
ORDER BY "rebrickable_parts"."color_name" ASC

View File

@@ -26,6 +26,12 @@ ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
{% endblock %}
{% block where %}
{% if color_id and color_id != 'all' %}
WHERE "bricktracker_parts"."color" = {{ color_id }}
{% endif %}
{% endblock %}
{% block group %}
GROUP BY
"bricktracker_parts"."part",

View File

@@ -0,0 +1,78 @@
{% extends 'part/base/base.sql' %}
{% block total_missing %}
{% if owner_id and owner_id != 'all' %}
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."missing" ELSE 0 END) AS "total_missing",
{% else %}
SUM("bricktracker_parts"."missing") AS "total_missing",
{% endif %}
{% endblock %}
{% block total_damaged %}
{% if owner_id and owner_id != 'all' %}
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."damaged" ELSE 0 END) AS "total_damaged",
{% else %}
SUM("bricktracker_parts"."damaged") AS "total_damaged",
{% endif %}
{% endblock %}
{% block total_quantity %}
{% if owner_id and owner_id != 'all' %}
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1) ELSE 0 END) AS "total_quantity",
{% else %}
SUM("bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity",
{% endif %}
{% endblock %}
{% block total_sets %}
{% if owner_id and owner_id != 'all' %}
COUNT(DISTINCT CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "bricktracker_parts"."id" ELSE NULL END) AS "total_sets",
{% else %}
COUNT(DISTINCT "bricktracker_parts"."id") AS "total_sets",
{% endif %}
{% endblock %}
{% block total_minifigures %}
{% if owner_id and owner_id != 'all' %}
SUM(CASE WHEN "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN IFNULL("bricktracker_minifigures"."quantity", 0) ELSE 0 END) AS "total_minifigures"
{% else %}
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures"
{% endif %}
{% endblock %}
{% block join %}
-- Join with sets to get owner information
INNER JOIN "bricktracker_sets"
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
-- Left join with set owners (using dynamic columns)
LEFT JOIN "bricktracker_set_owners"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
-- Left join with minifigures
LEFT JOIN "bricktracker_minifigures"
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
{% endblock %}
{% block where %}
{% set has_where = false %}
{% if owner_id and owner_id != 'all' %}
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
{% set has_where = true %}
{% endif %}
{% if color_id and color_id != 'all' %}
{% if has_where %}
AND "bricktracker_parts"."color" = {{ color_id }}
{% else %}
WHERE "bricktracker_parts"."color" = {{ color_id }}
{% endif %}
{% endif %}
{% endblock %}
{% block group %}
GROUP BY
"bricktracker_parts"."part",
"bricktracker_parts"."color",
"bricktracker_parts"."spare"
{% endblock %}

View File

@@ -1,9 +1,10 @@
from flask import Blueprint, render_template
from flask import Blueprint, render_template, request
from .exceptions import exception_handler
from ..minifigure import BrickMinifigure
from ..minifigure_list import BrickMinifigureList
from ..set_list import BrickSetList, set_metadata_lists
from ..set_owner_list import BrickSetOwnerList
minifigure_page = Blueprint('minifigure', __name__, url_prefix='/minifigures')
@@ -12,9 +13,23 @@ minifigure_page = Blueprint('minifigure', __name__, url_prefix='/minifigures')
@minifigure_page.route('/', methods=['GET'])
@exception_handler(__file__)
def list() -> str:
# Get owner filter from request
owner_id = request.args.get('owner', 'all')
# Get minifigures filtered by owner
if owner_id == 'all' or owner_id is None or owner_id == '':
minifigures = BrickMinifigureList().all()
else:
minifigures = BrickMinifigureList().all_by_owner(owner_id)
# Get list of owners for filter dropdown
owners = BrickSetOwnerList.list()
return render_template(
'minifigures.html',
table_collection=BrickMinifigureList().all(),
table_collection=minifigures,
owners=owners,
selected_owner=owner_id,
)

View File

@@ -1,10 +1,12 @@
from flask import Blueprint, render_template
from flask import Blueprint, render_template, request
from .exceptions import exception_handler
from ..minifigure_list import BrickMinifigureList
from ..part import BrickPart
from ..part_list import BrickPartList
from ..set_list import BrickSetList, set_metadata_lists
from ..set_owner_list import BrickSetOwnerList
from ..sql import BrickSQL
part_page = Blueprint('part', __name__, url_prefix='/parts')
@@ -13,9 +15,32 @@ part_page = Blueprint('part', __name__, url_prefix='/parts')
@part_page.route('/', methods=['GET'])
@exception_handler(__file__)
def list() -> str:
# Get filter parameters from request
owner_id = request.args.get('owner', 'all')
color_id = request.args.get('color', 'all')
# Get parts with filters applied
parts = BrickPartList().all_filtered(owner_id, color_id)
# Get list of owners for filter dropdown
owners = BrickSetOwnerList.list()
# Get list of colors for filter dropdown
# Prepare context for color query (filter by owner if selected)
color_context = {}
if owner_id != 'all' and owner_id:
color_context['owner_id'] = owner_id
colors = BrickSQL().fetchall('part/colors/list', **color_context)
return render_template(
'parts.html',
table_collection=BrickPartList().all()
table_collection=parts,
owners=owners,
selected_owner=owner_id,
colors=colors,
selected_color=color_id,
)

View File

@@ -0,0 +1,148 @@
// Minifigures page functionality
function filterByOwner() {
const select = document.getElementById('filter-owner');
const selectedOwner = select.value;
const currentUrl = new URL(window.location);
if (selectedOwner === 'all') {
currentUrl.searchParams.delete('owner');
} else {
currentUrl.searchParams.set('owner', selectedOwner);
}
window.location.href = currentUrl.toString();
}
// Keep filters expanded after selection
function filterByOwnerAndKeepOpen() {
// Remember if filters were open
const filterSection = document.getElementById('table-filter');
const wasOpen = filterSection && filterSection.classList.contains('show');
filterByOwner();
// Store the state to restore after page reload
if (wasOpen) {
sessionStorage.setItem('keepFiltersOpen', 'true');
}
}
// Setup table search and sort functionality
document.addEventListener("DOMContentLoaded", () => {
const searchInput = document.getElementById('table-search');
const searchClear = document.getElementById('table-search-clear');
// Restore filter state after page load
if (sessionStorage.getItem('keepFiltersOpen') === 'true') {
const filterSection = document.getElementById('table-filter');
const filterButton = document.querySelector('[data-bs-target="#table-filter"]');
if (filterSection && filterButton) {
filterSection.classList.add('show');
filterButton.setAttribute('aria-expanded', 'true');
}
sessionStorage.removeItem('keepFiltersOpen');
}
if (searchInput && searchClear) {
// Wait for table to be initialized by setup_tables
setTimeout(() => {
const tableElement = document.querySelector('table[data-table="true"]');
if (tableElement && window.brickTableInstance) {
// Enable custom search for minifigures table
window.brickTableInstance.table.searchable = true;
// Connect search input to table
searchInput.addEventListener('input', (e) => {
window.brickTableInstance.table.search(e.target.value);
});
// Clear search
searchClear.addEventListener('click', () => {
searchInput.value = '';
window.brickTableInstance.table.search('');
});
// Setup sort buttons
setupSortButtons();
}
}, 100);
}
});
function setupSortButtons() {
// Sort button functionality
const sortButtons = document.querySelectorAll('[data-sort-attribute]');
const clearButton = document.querySelector('[data-sort-clear]');
sortButtons.forEach(button => {
button.addEventListener('click', () => {
const attribute = button.dataset.sortAttribute;
const isDesc = button.dataset.sortDesc === 'true';
// Get column index based on attribute
const columnMap = {
'name': 1,
'parts': 2,
'quantity': 3,
'missing': 4,
'damaged': 5,
'sets': 6
};
const columnIndex = columnMap[attribute];
if (columnIndex !== undefined && window.brickTableInstance) {
// Determine sort direction
const isCurrentlyActive = button.classList.contains('btn-primary');
const currentDirection = button.dataset.currentDirection || (isDesc ? 'desc' : 'asc');
const newDirection = isCurrentlyActive ?
(currentDirection === 'asc' ? 'desc' : 'asc') :
(isDesc ? 'desc' : 'asc');
// Clear other active buttons
sortButtons.forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-primary');
btn.removeAttribute('data-current-direction');
});
// Mark this button as active
button.classList.remove('btn-outline-primary');
button.classList.add('btn-primary');
button.dataset.currentDirection = newDirection;
// Apply sort using Simple DataTables API
window.brickTableInstance.table.columns.sort(columnIndex, newDirection);
}
});
});
if (clearButton) {
clearButton.addEventListener('click', () => {
// Clear all sort buttons
sortButtons.forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-primary');
btn.removeAttribute('data-current-direction');
});
// Reset table sort - remove all sorting
if (window.brickTableInstance) {
// Destroy and recreate to clear sorting
const tableElement = document.querySelector('#minifigures');
const currentPerPage = window.brickTableInstance.table.options.perPage;
window.brickTableInstance.table.destroy();
setTimeout(() => {
// Create new instance using the globally available BrickTable class
const newInstance = new window.BrickTable(tableElement, currentPerPage);
window.brickTableInstance = newInstance;
// Re-enable search functionality
newInstance.table.searchable = true;
}, 50);
}
});
}
}

190
static/scripts/parts.js Normal file
View File

@@ -0,0 +1,190 @@
// Parts page functionality
function applyFilters() {
const ownerSelect = document.getElementById('filter-owner');
const colorSelect = document.getElementById('filter-color');
const currentUrl = new URL(window.location);
// Handle owner filter
if (ownerSelect) {
const selectedOwner = ownerSelect.value;
if (selectedOwner === 'all') {
currentUrl.searchParams.delete('owner');
} else {
currentUrl.searchParams.set('owner', selectedOwner);
}
}
// Handle color filter
if (colorSelect) {
const selectedColor = colorSelect.value;
if (selectedColor === 'all') {
currentUrl.searchParams.delete('color');
} else {
currentUrl.searchParams.set('color', selectedColor);
}
}
window.location.href = currentUrl.toString();
}
function setupColorDropdown() {
const colorSelect = document.getElementById('filter-color');
if (!colorSelect) return;
// Add color squares to option text
const options = colorSelect.querySelectorAll('option[data-color-rgb]');
options.forEach(option => {
const colorRgb = option.dataset.colorRgb;
const colorId = option.dataset.colorId;
const colorName = option.textContent.trim();
if (colorRgb && colorId !== '9999') {
// Create a visual indicator (using Unicode square)
option.textContent = `${colorName}`; //■
//option.style.color = `#${colorRgb}`;
}
});
}
// Keep filters expanded after selection
function applyFiltersAndKeepOpen() {
// Remember if filters were open
const filterSection = document.getElementById('table-filter');
const wasOpen = filterSection && filterSection.classList.contains('show');
applyFilters();
// Store the state to restore after page reload
if (wasOpen) {
sessionStorage.setItem('keepFiltersOpen', 'true');
}
}
function setupSortButtons() {
// Sort button functionality
const sortButtons = document.querySelectorAll('[data-sort-attribute]');
const clearButton = document.querySelector('[data-sort-clear]');
sortButtons.forEach(button => {
button.addEventListener('click', () => {
const attribute = button.dataset.sortAttribute;
const isDesc = button.dataset.sortDesc === 'true';
// Get column index based on attribute
const columnMap = {
'name': 1,
'color': 2,
'quantity': 3,
'missing': 4,
'damaged': 5,
'sets': 6,
'minifigures': 7
};
const columnIndex = columnMap[attribute];
if (columnIndex !== undefined && window.partsTableInstance) {
// Determine sort direction
const isCurrentlyActive = button.classList.contains('btn-primary');
const currentDirection = button.dataset.currentDirection || (isDesc ? 'desc' : 'asc');
const newDirection = isCurrentlyActive ?
(currentDirection === 'asc' ? 'desc' : 'asc') :
(isDesc ? 'desc' : 'asc');
// Clear other active buttons
sortButtons.forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-primary');
btn.removeAttribute('data-current-direction');
});
// Mark this button as active
button.classList.remove('btn-outline-primary');
button.classList.add('btn-primary');
button.dataset.currentDirection = newDirection;
// Apply sort using Simple DataTables API
window.partsTableInstance.table.columns.sort(columnIndex, newDirection);
}
});
});
if (clearButton) {
clearButton.addEventListener('click', () => {
// Clear all sort buttons
sortButtons.forEach(btn => {
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-primary');
btn.removeAttribute('data-current-direction');
});
// Reset table sort - remove all sorting
if (window.partsTableInstance) {
// Destroy and recreate to clear sorting
const tableElement = document.querySelector('#parts');
const currentPerPage = window.partsTableInstance.table.options.perPage;
window.partsTableInstance.table.destroy();
setTimeout(() => {
// Create new instance using the globally available BrickTable class
const newInstance = new window.BrickTable(tableElement, currentPerPage);
window.partsTableInstance = newInstance;
// Re-enable search functionality
newInstance.table.searchable = true;
}, 50);
}
});
}
}
// Setup table search and sort functionality
document.addEventListener("DOMContentLoaded", () => {
const searchInput = document.getElementById('table-search');
const searchClear = document.getElementById('table-search-clear');
// Setup color dropdown with color squares
setupColorDropdown();
// Restore filter state after page load
if (sessionStorage.getItem('keepFiltersOpen') === 'true') {
const filterSection = document.getElementById('table-filter');
const filterButton = document.querySelector('[data-bs-target="#table-filter"]');
if (filterSection && filterButton) {
filterSection.classList.add('show');
filterButton.setAttribute('aria-expanded', 'true');
}
sessionStorage.removeItem('keepFiltersOpen');
}
if (searchInput && searchClear) {
// Wait for table to be initialized by setup_tables
const setupSearch = () => {
const tableElement = document.querySelector('table[data-table="true"]');
if (tableElement && window.partsTableInstance) {
// Enable custom search for parts table
window.partsTableInstance.table.searchable = true;
// Connect search input to table
searchInput.addEventListener('input', (e) => {
window.partsTableInstance.table.search(e.target.value);
});
// Clear search
searchClear.addEventListener('click', () => {
searchInput.value = '';
window.partsTableInstance.table.search('');
});
// Setup sort buttons
setupSortButtons();
} else {
// If table instance not ready, try again
setTimeout(setupSearch, 100);
}
};
setTimeout(setupSearch, 100);
}
});

View File

@@ -1,4 +1,5 @@
class BrickTable {
// Make BrickTable globally accessible
window.BrickTable = class BrickTable {
constructor(table, per_page) {
const columns = [];
const no_sort_and_filter = [];
@@ -32,12 +33,17 @@ class BrickTable {
columns.push({ select: number, type: "number", searchable: false });
}
// Special configuration for tables with custom search/sort
const isMinifiguresTable = table.id === 'minifigures';
const isPartsTable = table.id === 'parts';
const hasCustomInterface = isMinifiguresTable || isPartsTable;
this.table = new simpleDatatables.DataTable(`#${table.id}`, {
columns: columns,
pagerDelta: 1,
perPage: per_page,
perPageSelect: [10, 25, 50, 100, 500, 1000],
searchable: true,
searchable: !hasCustomInterface, // Disable built-in search for tables with custom interface
searchMethod: (table => (terms, cell, row, column, source) => table.search(terms, cell, row, column, source))(this),
searchQuerySeparator: "",
tableRender: () => {
@@ -92,5 +98,13 @@ class BrickTable {
// Helper to setup the tables
const setup_tables = (per_page) => document.querySelectorAll('table[data-table="true"]').forEach(
el => new BrickTable(el, per_page)
el => {
const brickTable = new window.BrickTable(el, per_page);
// Store the instance globally for external access
if (el.id === 'minifigures') {
window.brickTableInstance = brickTable;
} else if (el.id === 'parts') {
window.partsTableInstance = brickTable;
}
}
);

View File

@@ -91,6 +91,8 @@
<script src="{{ url_for('static', filename='scripts/socket/instructions.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/socket/set.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/table.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/minifigures.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/parts.js') }}"></script>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", () => {
setup_grids();

View File

@@ -0,0 +1,15 @@
<div id="table-filter" class="collapse {% if config['SHOW_GRID_FILTERS'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
{% if owners | length %}
<div class="col-12 flex-grow-1">
<div class="input-group">
<span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-md-inline"> Owner</span></span>
<select id="filter-owner" class="form-select" onchange="filterByOwnerAndKeepOpen()" autocomplete="off">
<option value="all" {% if selected_owner == 'all' %}selected{% endif %}>All owners</option>
{% for owner in owners %}
<option value="{{ owner.fields.id }}" {% if selected_owner == owner.fields.id %}selected{% endif %}>{{ owner.fields.name }}</option>
{% endfor %}
</select>
</div>
</div>
{% endif %}
</div>

View File

@@ -0,0 +1,25 @@
<div id="table-sort" class="collapse {% if config['SHOW_GRID_SORT'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
<div class="col-12 flex-grow-1">
<div class="input-group">
<span class="input-group-text mb-2"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-md-inline"> Sort</span></span>
<button id="sort-name" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-md-inline"> Name</span></button>
<button id="sort-parts" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xl-inline"> Parts</span></button>
<button id="sort-quantity" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="quantity" data-sort-desc="true"><i class="ri-functions"></i><span class="d-none d-xl-inline"> Quantity</span></button>
{% if not config['HIDE_TABLE_MISSING_PARTS'] %}
<button id="sort-missing" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="missing" data-sort-desc="true"><i class="ri-question-line"></i><span class="d-none d-xl-inline"> Missing</span></button>
{% endif %}
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
<button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button>
{% endif %}
<button id="sort-sets" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="sets" data-sort-desc="true"><i class="ri-hashtag"></i><span class="d-none d-xl-inline"> Sets</span></button>
<button id="sort-clear" type="button" class="btn btn-outline-dark mb-2"
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button>
</div>
</div>
</div>

View File

@@ -7,17 +7,17 @@
{% for item in table_collection %}
<tr>
{{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.figure) }}
<td >
<td data-sort="name">
<a class="text-reset" href="{{ item.url() }}" style="max-width:auto">{{ item.fields.name }}</a>
{% if all %}
{{ table.rebrickable(item) }}
{% endif %}
</td>
<td>{{ item.fields.number_of_parts }}</td>
<td>{{ item.fields.total_quantity }}</td>
<td>{{ item.fields.total_missing }}</td>
<td>{{ item.fields.total_damaged }}</td>
<td>{{ item.fields.total_sets }}</td>
<td data-sort="parts">{{ item.fields.number_of_parts }}</td>
<td data-sort="quantity">{{ item.fields.total_quantity }}</td>
<td data-sort="missing">{{ item.fields.total_missing }}</td>
<td data-sort="damaged">{{ item.fields.total_damaged }}</td>
<td data-sort="sets">{{ item.fields.total_sets }}</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -3,9 +3,53 @@
{% block title %} - All minifigures{% endblock %}
{% block main %}
<div class="container-fluid px-0">
{% if table_collection | length %}
<div class="container-fluid">
<div class="row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="table-search">Search</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
<input id="table-search" class="form-control form-control-sm px-1" type="text" placeholder="Figure name, parts count, sets" value="">
<button id="table-search-clear" type="button" class="btn btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
</div>
</div>
<div class="col-12">
<div class="input-group">
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-sort" aria-expanded="{% if config['SHOW_GRID_SORT'] %}true{% else %}false{% endif %}" aria-controls="table-sort">
<i class="ri-sort-asc"></i> Sort
</button>
</div>
</div>
{% if owners | length > 1 %}
<div class="col-12">
<div class="input-group">
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="table-filter">
<i class="ri-filter-line"></i> Filters
</button>
</div>
</div>
{% endif %}
</div>
{% include 'minifigure/sort.html' %}
{% include 'minifigure/filter.html' %}
{% with all=true %}
{% include 'minifigure/table.html' %}
{% endwith %}
</div>
{% else %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="text-center">
<i class="ri-group-line" style="font-size: 4rem; color: #6c757d;"></i>
<h3 class="mt-3">No minifigures found</h3>
<p class="text-muted">No minifigures are available for the selected owner.</p>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,30 @@
<div id="table-filter" class="collapse {% if config['SHOW_GRID_FILTERS'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
{% if owners | length %}
<div class="col-12 col-md-6 flex-grow-1">
<div class="input-group">
<span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-md-inline"> Owner</span></span>
<select id="filter-owner" class="form-select" onchange="applyFiltersAndKeepOpen()" autocomplete="off">
<option value="all" {% if selected_owner == 'all' %}selected{% endif %}>All owners</option>
{% for owner in owners %}
<option value="{{ owner.fields.id }}" {% if selected_owner == owner.fields.id %}selected{% endif %}>{{ owner.fields.name }}</option>
{% endfor %}
</select>
</div>
</div>
{% endif %}
{% if colors | length %}
<div class="col-12 col-md-6 flex-grow-1">
<div class="input-group">
<span class="input-group-text"><i class="ri-palette-line"></i><span class="ms-1 d-none d-md-inline"> Color</span></span>
<select id="filter-color" class="form-select" onchange="applyFiltersAndKeepOpen()" autocomplete="off">
<option value="all" {% if selected_color == 'all' %}selected{% endif %}>All colors</option>
{% for color in colors %}
<option value="{{ color.color_id }}" {% if selected_color == color.color_id|string %}selected{% endif %} data-color-rgb="{{ color.color_rgb }}" data-color-id="{{ color.color_id }}">
{{ color.color_name }}
</option>
{% endfor %}
</select>
</div>
</div>
{% endif %}
</div>

27
templates/part/sort.html Normal file
View File

@@ -0,0 +1,27 @@
<div id="table-sort" class="collapse {% if config['SHOW_GRID_SORT'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
<div class="col-12 flex-grow-1">
<div class="input-group">
<span class="input-group-text mb-2"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-md-inline"> Sort</span></span>
<button id="sort-name" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-md-inline"> Name</span></button>
<button id="sort-color" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="color"><i class="ri-palette-line"></i><span class="d-none d-xl-inline"> Color</span></button>
<button id="sort-quantity" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="quantity" data-sort-desc="true"><i class="ri-functions"></i><span class="d-none d-xl-inline"> Quantity</span></button>
{% if not config['HIDE_TABLE_MISSING_PARTS'] %}
<button id="sort-missing" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="missing" data-sort-desc="true"><i class="ri-question-line"></i><span class="d-none d-xl-inline"> Missing</span></button>
{% endif %}
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
<button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button>
{% endif %}
<button id="sort-sets" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="sets" data-sort-desc="true"><i class="ri-hashtag"></i><span class="d-none d-xl-inline"> Sets</span></button>
<button id="sort-minifigures" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xl-inline"> Figures</span></button>
<button id="sort-clear" type="button" class="btn btn-outline-dark mb-2"
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button>
</div>
</div>
</div>

View File

@@ -3,9 +3,51 @@
{% block title %} - All parts{% endblock %}
{% block main %}
<div class="container-fluid px-0">
{% if table_collection | length %}
<div class="container-fluid">
<div class="row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="table-search">Search</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
<input id="table-search" class="form-control form-control-sm px-1" type="text" placeholder="Part name, color, quantity, sets" value="">
<button id="table-search-clear" type="button" class="btn btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
</div>
</div>
<div class="col-12">
<div class="input-group">
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-sort" aria-expanded="{% if config['SHOW_GRID_SORT'] %}true{% else %}false{% endif %}" aria-controls="table-sort">
<i class="ri-sort-asc"></i> Sort
</button>
</div>
</div>
<div class="col-12">
<div class="input-group">
<button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#table-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="table-filter">
<i class="ri-filter-line"></i> Filters
</button>
</div>
</div>
</div>
{% include 'part/sort.html' %}
{% include 'part/filter.html' %}
{% with all=true %}
{% include 'part/table.html' %}
{% endwith %}
</div>
{% else %}
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-6">
<div class="text-center">
<i class="ri-shapes-line" style="font-size: 4rem; color: #6c757d;"></i>
<h3 class="mt-3">No parts found</h3>
<p class="text-muted">No parts are available for the selected owner.</p>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}