Make products filterable by product type

This commit is contained in:
imperosol 2024-12-21 02:13:37 +01:00
parent 6953eaa9d0
commit accf1befce
7 changed files with 151 additions and 61 deletions

View File

@ -107,7 +107,7 @@ form {
} }
} }
label { label, legend {
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;

View File

@ -429,6 +429,16 @@ body {
.row { .row {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
$col-gap: 1rem;
&.gap {
column-gap: var($col-gap);
}
@for $i from 2 through 5 {
&.gap-#{$i}x {
column-gap: $i * $col-gap;
}
}
} }
/*---------------------------------NEWS--------------------------------*/ /*---------------------------------NEWS--------------------------------*/

View File

@ -4,9 +4,11 @@ import type { TomOption } from "tom-select/dist/types/types";
import type { escape_html } from "tom-select/dist/types/utils"; import type { escape_html } from "tom-select/dist/types/utils";
import { import {
type CounterSchema, type CounterSchema,
type ProductTypeSchema,
type SimpleProductSchema, type SimpleProductSchema,
counterSearchCounter, counterSearchCounter,
productSearchProducts, productSearchProducts,
producttypeFetchAll,
} from "#openapi"; } from "#openapi";
@registerComponent("product-ajax-select") @registerComponent("product-ajax-select")
@ -34,6 +36,37 @@ export class ProductAjaxSelect extends AjaxSelect {
} }
} }
@registerComponent("product-type-ajax-select")
export class ProductTypeAjaxSelect extends AjaxSelect {
protected valueField = "id";
protected labelField = "name";
protected searchField = ["name"];
private productTypes = null as ProductTypeSchema[];
protected async search(query: string): Promise<TomOption[]> {
// The production database has a grand total of 26 product types
// and the filter logic is really simple.
// Thus, it's appropriate to fetch all product types during first use,
// then to reuse the result again and again.
if (this.productTypes === null) {
this.productTypes = (await producttypeFetchAll()).data || null;
}
return this.productTypes.filter((t) =>
t.name.toLowerCase().includes(query.toLowerCase()),
);
}
protected renderOption(item: ProductTypeSchema, sanitize: typeof escape_html) {
return `<div class="select-item">
<span class="select-item-text">${sanitize(item.name)}</span>
</div>`;
}
protected renderItem(item: ProductTypeSchema, sanitize: typeof escape_html) {
return `<span>${sanitize(item.name)}</span>`;
}
}
@registerComponent("counter-ajax-select") @registerComponent("counter-ajax-select")
export class CounterAjaxSelect extends AjaxSelect { export class CounterAjaxSelect extends AjaxSelect {
protected valueField = "id"; protected valueField = "id";

View File

@ -3,6 +3,7 @@ import { csv } from "#core:utils/csv";
import { History, getCurrentUrlParams, updateQueryString } from "#core:utils/history"; import { History, getCurrentUrlParams, updateQueryString } from "#core:utils/history";
import type { NestedKeyOf } from "#core:utils/types"; import type { NestedKeyOf } from "#core:utils/types";
import { showSaveFilePicker } from "native-file-system-adapter"; import { showSaveFilePicker } from "native-file-system-adapter";
import type TomSelect from "tom-select";
import { import {
type ProductSchema, type ProductSchema,
type ProductSearchProductsDetailedData, type ProductSearchProductsDetailedData,
@ -58,6 +59,7 @@ document.addEventListener("alpine:init", () => {
productStatus: "" as "active" | "archived" | "both", productStatus: "" as "active" | "archived" | "both",
search: "", search: "",
productTypes: [] as string[],
pageSize: defaultPageSize, pageSize: defaultPageSize,
page: defaultPage, page: defaultPage,
@ -65,17 +67,22 @@ document.addEventListener("alpine:init", () => {
const url = getCurrentUrlParams(); const url = getCurrentUrlParams();
this.search = url.get("search") || ""; this.search = url.get("search") || "";
this.productStatus = url.get("productStatus") ?? "active"; this.productStatus = url.get("productStatus") ?? "active";
const widget = this.$refs.productTypesInput.widget as TomSelect;
widget.on("change", (items: string[]) => {
this.productTypes = [...items];
});
await this.load(); await this.load();
for (const param of ["search", "productStatus"]) { const searchParams = ["search", "productStatus", "productTypes"];
for (const param of searchParams) {
this.$watch(param, () => { this.$watch(param, () => {
this.page = defaultPage; this.page = defaultPage;
}); });
} }
for (const param of ["search", "productStatus", "page"]) { for (const param of [...searchParams, "page"]) {
this.$watch(param, async (value: string) => { this.$watch(param, async (value: string) => {
updateQueryString(param, value, History.Replace); updateQueryString(param, value, History.Replace);
this.nbPages = 0; this.nbPages = 0;
this.products = {};
await this.load(); await this.load();
}); });
} }
@ -100,6 +107,8 @@ document.addEventListener("alpine:init", () => {
search: search, search: search,
// biome-ignore lint/style/useNamingConvention: api is in snake_case // biome-ignore lint/style/useNamingConvention: api is in snake_case
is_archived: isArchived, is_archived: isArchived,
// biome-ignore lint/style/useNamingConvention: api is in snake_case
product_type: this.productTypes,
}, },
}; };
}, },

View File

@ -6,45 +6,60 @@
{% endblock %} {% endblock %}
{% block additional_js %} {% block additional_js %}
<script type="module" src="{{ static("bundled/counter/components/ajax-select-index.ts") }}"></script>
<script type="module" src="{{ static("bundled/counter/product-list-index.ts") }}"></script> <script type="module" src="{{ static("bundled/counter/product-list-index.ts") }}"></script>
{% endblock %} {% endblock %}
{% block additional_css %} {% block additional_css %}
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}"> <link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
<link rel="stylesheet" href="{{ static("counter/css/admin.scss") }}"> <link rel="stylesheet" href="{{ static("counter/css/admin.scss") }}">
<link rel="stylesheet" href="{{ static("bundled/core/components/ajax-select-index.css") }}">
<link rel="stylesheet" href="{{ static("core/components/ajax-select.scss") }}">
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<main x-data="productList"> <main x-data="productList">
<h4 class="margin-bottom">{% trans %}Filter products{% endtrans %}</h4>
<form id="search-form" class="margin-bottom"> <form id="search-form" class="margin-bottom">
<h4 class="margin-bottom">{% trans %}Filter products{% endtrans %}</h4> <div class="row gap-4x">
<fieldset>
<label for="search-input">{% trans %}Product name{% endtrans %}</label>
<input
id="search-input"
type="text"
name="search"
x-model.debounce.500ms="search"
/>
</fieldset>
<fieldset>
<legend>{% trans %}Product state{% endtrans %}</legend>
<div class="row">
<input type="radio" id="filter-active-products" x-model="productStatus" value="active">
<label for="filter-active-products">{% trans %}Active products{% endtrans %}</label>
</div>
<div class="row">
<input type="radio" id="filter-inactive-products" x-model="productStatus" value="archived">
<label for="filter-inactive-products">{% trans %}Archived products{% endtrans %}</label>
</div>
<div class="row">
<input type="radio" id="filter-all-products" x-model="productStatus" value="both">
<label for="filter-all-products">{% trans %}All products{% endtrans %}</label>
</div>
</fieldset>
</div>
<fieldset> <fieldset>
<label for="search-input">{% trans %}Product name{% endtrans %}</label> <label for="type-search-input">{% trans %}Product type{% endtrans %}</label>
<input <product-type-ajax-select
id="search-input" id="type-search-input"
type="text" name="product-type"
name="search" x-ref="productTypesInput"
x-model.debounce.500ms="search" multiple
/> >
</fieldset> </product-type-ajax-select>
<fieldset>
<div class="row">
<input type="radio" id="filter-active-products" x-model="productStatus" value="active">
<label for="filter-active-products">{% trans %}Active products{% endtrans %}</label>
</div>
<div class="row">
<input type="radio" id="filter-inactive-products" x-model="productStatus" value="archived">
<label for="filter-inactive-products">{% trans %}Archived products{% endtrans %}</label>
</div>
<div class="row">
<input type="radio" id="filter-all-products" x-model="productStatus" value="both">
<label for="filter-all-products">{% trans %}All products{% endtrans %}</label>
</div>
</fieldset> </fieldset>
</form> </form>
<h3 @click="console.log(totalCount, nbPages())" class="margin-bottom"> <h3 class="margin-bottom">{% trans %}Product list{% endtrans %}</h3>
{% trans %}Product list{% endtrans %}
</h3>
<div class="row margin-bottom"> <div class="row margin-bottom">
<a href="{{ url('counter:new_product') }}" class="btn btn-blue"> <a href="{{ url('counter:new_product') }}" class="btn btn-blue">

View File

@ -1,8 +1,12 @@
from pydantic import TypeAdapter from pydantic import TypeAdapter
from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple
from counter.models import Counter, Product from counter.models import Counter, Product, ProductType
from counter.schemas import SimpleProductSchema, SimplifiedCounterSchema from counter.schemas import (
ProductTypeSchema,
SimpleProductSchema,
SimplifiedCounterSchema,
)
_js = ["bundled/counter/components/ajax-select-index.ts"] _js = ["bundled/counter/components/ajax-select-index.ts"]
@ -33,3 +37,17 @@ class AutoCompleteSelectMultipleProduct(AutoCompleteSelectMultiple):
model = Product model = Product
adapter = TypeAdapter(list[SimpleProductSchema]) adapter = TypeAdapter(list[SimpleProductSchema])
js = _js js = _js
class AutoCompleteSelectProductType(AutoCompleteSelect):
component_name = "product-type-ajax-select"
model = ProductType
adapter = TypeAdapter(list[ProductTypeSchema])
js = _js
class AutoCompleteSelectMultipleProductType(AutoCompleteSelectMultiple):
component_name = "product-type-ajax-select"
model = ProductType
adapter = TypeAdapter(list[ProductTypeSchema])
js = _js

View File

@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-19 10:43+0100\n" "POT-Creation-Date: 2024-12-21 02:15+0100\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n" "Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@ -765,7 +765,7 @@ msgstr "Opération liée : "
#: core/templates/core/create.jinja:12 core/templates/core/edit.jinja:7 #: core/templates/core/create.jinja:12 core/templates/core/edit.jinja:7
#: core/templates/core/edit.jinja:15 core/templates/core/edit.jinja:20 #: core/templates/core/edit.jinja:15 core/templates/core/edit.jinja:20
#: core/templates/core/file_edit.jinja:8 #: core/templates/core/file_edit.jinja:8
#: core/templates/core/macros_pages.jinja:25 #: core/templates/core/macros_pages.jinja:26
#: core/templates/core/page_prop.jinja:11 #: core/templates/core/page_prop.jinja:11
#: core/templates/core/user_godfathers.jinja:61 #: core/templates/core/user_godfathers.jinja:61
#: core/templates/core/user_godfathers_tree.jinja:85 #: core/templates/core/user_godfathers_tree.jinja:85
@ -971,7 +971,7 @@ msgstr "Comptoir"
msgid "Products" msgid "Products"
msgstr "Produits" msgstr "Produits"
#: club/forms.py:168 counter/templates/counter/product_list.jinja:37 #: club/forms.py:168 counter/templates/counter/product_list.jinja:43
msgid "Archived products" msgid "Archived products"
msgstr "Produits archivés" msgstr "Produits archivés"
@ -1146,7 +1146,7 @@ msgid "There are no members in this club."
msgstr "Il n'y a pas de membres dans ce club." msgstr "Il n'y a pas de membres dans ce club."
#: club/templates/club/club_members.jinja:80 #: club/templates/club/club_members.jinja:80
#: core/templates/core/file_detail.jinja:19 core/views/forms.py:305 #: core/templates/core/file_detail.jinja:19 core/views/forms.py:309
#: launderette/views.py:208 trombi/templates/trombi/detail.jinja:19 #: launderette/views.py:208 trombi/templates/trombi/detail.jinja:19
msgid "Add" msgid "Add"
msgstr "Ajouter" msgstr "Ajouter"
@ -1192,7 +1192,7 @@ msgid "Show"
msgstr "Montrer" msgstr "Montrer"
#: club/templates/club/club_sellings.jinja:38 #: club/templates/club/club_sellings.jinja:38
#: counter/templates/counter/product_list.jinja:59 #: counter/templates/counter/product_list.jinja:74
msgid "Download as cvs" msgid "Download as cvs"
msgstr "Télécharger en CSV" msgstr "Télécharger en CSV"
@ -2038,8 +2038,8 @@ msgid ""
"The groups this user belongs to. A user will get all permissions granted to " "The groups this user belongs to. A user will get all permissions granted to "
"each of their groups." "each of their groups."
msgstr "" msgstr ""
"Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes les " "Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes "
"permissions de chacun de ses groupes." "les permissions de chacun de ses groupes."
#: core/models.py:277 #: core/models.py:277
msgid "profile" msgid "profile"
@ -2764,11 +2764,11 @@ msgstr "Tout désélectionner"
msgid "You're seeing the history of page \"%(page_name)s\"" msgid "You're seeing the history of page \"%(page_name)s\""
msgstr "Vous consultez l'historique de la page \"%(page_name)s\"" msgstr "Vous consultez l'historique de la page \"%(page_name)s\""
#: core/templates/core/macros_pages.jinja:8 #: core/templates/core/macros_pages.jinja:10
msgid "last" msgid "last"
msgstr "actuel" msgstr "actuel"
#: core/templates/core/macros_pages.jinja:21 #: core/templates/core/macros_pages.jinja:22
msgid "Edit page" msgid "Edit page"
msgstr "Éditer la page" msgstr "Éditer la page"
@ -3026,7 +3026,7 @@ msgstr "Facture eboutic"
msgid "Etickets" msgid "Etickets"
msgstr "Etickets" msgstr "Etickets"
#: core/templates/core/user_account.jinja:69 core/views/user.py:633 #: core/templates/core/user_account.jinja:69 core/views/user.py:631
msgid "User has no account" msgid "User has no account"
msgstr "L'utilisateur n'a pas de compte" msgstr "L'utilisateur n'a pas de compte"
@ -3326,7 +3326,8 @@ msgstr "Outils utilisateurs"
msgid "Sith management" msgid "Sith management"
msgstr "Gestion de Sith" msgstr "Gestion de Sith"
#: core/templates/core/user_tools.jinja:21 core/views/user.py:254 #: core/templates/core/user_tools.jinja:21 core/views/forms.py:295
#: core/views/user.py:254
msgid "Groups" msgid "Groups"
msgstr "Groupes" msgstr "Groupes"
@ -3518,32 +3519,32 @@ msgstr "Blouse : montrez aux autres à quoi ressemble votre blouse !"
msgid "Bad image format, only jpeg, png, webp and gif are accepted" msgid "Bad image format, only jpeg, png, webp and gif are accepted"
msgstr "Mauvais format d'image, seuls les jpeg, png, webp et gif sont acceptés" msgstr "Mauvais format d'image, seuls les jpeg, png, webp et gif sont acceptés"
#: core/views/forms.py:302 #: core/views/forms.py:306
msgid "Godfather / Godmother" msgid "Godfather / Godmother"
msgstr "Parrain / Marraine" msgstr "Parrain / Marraine"
#: core/views/forms.py:303 #: core/views/forms.py:307
msgid "Godchild" msgid "Godchild"
msgstr "Fillot / Fillote" msgstr "Fillot / Fillote"
#: core/views/forms.py:308 counter/forms.py:78 trombi/views.py:151 #: core/views/forms.py:312 counter/forms.py:78 trombi/views.py:151
msgid "Select user" msgid "Select user"
msgstr "Choisir un utilisateur" msgstr "Choisir un utilisateur"
#: core/views/forms.py:322 #: core/views/forms.py:326
msgid "This user does not exist" msgid "This user does not exist"
msgstr "Cet utilisateur n'existe pas" msgstr "Cet utilisateur n'existe pas"
#: core/views/forms.py:324 #: core/views/forms.py:328
msgid "You cannot be related to yourself" msgid "You cannot be related to yourself"
msgstr "Vous ne pouvez pas être relié à vous-même" msgstr "Vous ne pouvez pas être relié à vous-même"
#: core/views/forms.py:336 #: core/views/forms.py:340
#, python-format #, python-format
msgid "%s is already your godfather" msgid "%s is already your godfather"
msgstr "%s est déjà votre parrain/marraine" msgstr "%s est déjà votre parrain/marraine"
#: core/views/forms.py:342 #: core/views/forms.py:346
#, python-format #, python-format
msgid "%s is already your godchild" msgid "%s is already your godchild"
msgstr "%s est déjà votre fillot/fillote" msgstr "%s est déjà votre fillot/fillote"
@ -4152,31 +4153,35 @@ msgstr ""
"aucune conséquence autre que le retrait de l'argent de votre compte." "aucune conséquence autre que le retrait de l'argent de votre compte."
#: counter/templates/counter/product_list.jinja:5 #: counter/templates/counter/product_list.jinja:5
#: counter/templates/counter/product_list.jinja:46 #: counter/templates/counter/product_list.jinja:62
msgid "Product list" msgid "Product list"
msgstr "Liste des produits" msgstr "Liste des produits"
#: counter/templates/counter/product_list.jinja:20 #: counter/templates/counter/product_list.jinja:22
msgid "Filter products" msgid "Filter products"
msgstr "Filtrer les produits" msgstr "Filtrer les produits"
#: counter/templates/counter/product_list.jinja:22 #: counter/templates/counter/product_list.jinja:27
msgid "Product name" msgid "Product name"
msgstr "Nom du produit" msgstr "Nom du produit"
#: counter/templates/counter/product_list.jinja:33 #: counter/templates/counter/product_list.jinja:36
#, fuzzy msgid "Product state"
#| msgid "Archived products" msgstr "Etat du produit"
#: counter/templates/counter/product_list.jinja:39
msgid "Active products" msgid "Active products"
msgstr "Produits archivés" msgstr "Produits actifs"
#: counter/templates/counter/product_list.jinja:41 #: counter/templates/counter/product_list.jinja:47
#, fuzzy
#| msgid "products"
msgid "All products" msgid "All products"
msgstr "produits" msgstr "Tous les produits"
#: counter/templates/counter/product_list.jinja:51 #: counter/templates/counter/product_list.jinja:52
msgid "Product type"
msgstr "Type de produit"
#: counter/templates/counter/product_list.jinja:66
msgid "New product" msgid "New product"
msgstr "Nouveau produit" msgstr "Nouveau produit"