From 1cac17a4200b0c29741ad0f888816ecd9e40c958 Mon Sep 17 00:00:00 2001 From: Frederik Baerentsen Date: Mon, 15 Sep 2025 20:28:39 +0200 Subject: [PATCH] Added filter/sort/search to /minifigures and /parts --- bricktracker/minifigure_list.py | 21 ++ bricktracker/part_list.py | 38 ++++ .../sql/minifigure/list/all_by_owner.sql | 71 +++++++ bricktracker/sql/part/colors/list.sql | 16 ++ bricktracker/sql/part/list/all.sql | 6 + bricktracker/sql/part/list/all_by_owner.sql | 78 +++++++ bricktracker/views/minifigure.py | 19 +- bricktracker/views/part.py | 29 ++- static/scripts/minifigures.js | 148 ++++++++++++++ static/scripts/parts.js | 190 ++++++++++++++++++ static/scripts/table.js | 20 +- templates/base.html | 2 + templates/minifigure/filter.html | 15 ++ templates/minifigure/sort.html | 25 +++ templates/minifigure/table.html | 12 +- templates/minifigures.html | 46 ++++- templates/part/filter.html | 30 +++ templates/part/sort.html | 27 +++ templates/parts.html | 44 +++- 19 files changed, 822 insertions(+), 15 deletions(-) create mode 100644 bricktracker/sql/minifigure/list/all_by_owner.sql create mode 100644 bricktracker/sql/part/colors/list.sql create mode 100644 bricktracker/sql/part/list/all_by_owner.sql create mode 100644 static/scripts/minifigures.js create mode 100644 static/scripts/parts.js create mode 100644 templates/minifigure/filter.html create mode 100644 templates/minifigure/sort.html create mode 100644 templates/part/filter.html create mode 100644 templates/part/sort.html diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index fa73562..40c13d0 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -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 diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index a12ef89..96f8653 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -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, diff --git a/bricktracker/sql/minifigure/list/all_by_owner.sql b/bricktracker/sql/minifigure/list/all_by_owner.sql new file mode 100644 index 0000000..6ae14ba --- /dev/null +++ b/bricktracker/sql/minifigure/list/all_by_owner.sql @@ -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 %} \ No newline at end of file diff --git a/bricktracker/sql/part/colors/list.sql b/bricktracker/sql/part/colors/list.sql new file mode 100644 index 0000000..a055ec0 --- /dev/null +++ b/bricktracker/sql/part/colors/list.sql @@ -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 \ No newline at end of file diff --git a/bricktracker/sql/part/list/all.sql b/bricktracker/sql/part/list/all.sql index 8bb8dcb..bcc729b 100644 --- a/bricktracker/sql/part/list/all.sql +++ b/bricktracker/sql/part/list/all.sql @@ -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", diff --git a/bricktracker/sql/part/list/all_by_owner.sql b/bricktracker/sql/part/list/all_by_owner.sql new file mode 100644 index 0000000..ccfc301 --- /dev/null +++ b/bricktracker/sql/part/list/all_by_owner.sql @@ -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 %} \ No newline at end of file diff --git a/bricktracker/views/minifigure.py b/bricktracker/views/minifigure.py index 7123e4a..aeabe7e 100644 --- a/bricktracker/views/minifigure.py +++ b/bricktracker/views/minifigure.py @@ -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, ) diff --git a/bricktracker/views/part.py b/bricktracker/views/part.py index fc800c4..f8e72f2 100644 --- a/bricktracker/views/part.py +++ b/bricktracker/views/part.py @@ -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, ) diff --git a/static/scripts/minifigures.js b/static/scripts/minifigures.js new file mode 100644 index 0000000..8da5e76 --- /dev/null +++ b/static/scripts/minifigures.js @@ -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); + } + }); + } +} \ No newline at end of file diff --git a/static/scripts/parts.js b/static/scripts/parts.js new file mode 100644 index 0000000..5c1393c --- /dev/null +++ b/static/scripts/parts.js @@ -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); + } +}); \ No newline at end of file diff --git a/static/scripts/table.js b/static/scripts/table.js index f96f10b..f9cf06f 100644 --- a/static/scripts/table.js +++ b/static/scripts/table.js @@ -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; + } + } ); diff --git a/templates/base.html b/templates/base.html index 658ef37..b75c678 100644 --- a/templates/base.html +++ b/templates/base.html @@ -91,6 +91,8 @@ + +