diff --git a/core/static/core/forms.scss b/core/static/core/forms.scss index 42a4d719..36f41d94 100644 --- a/core/static/core/forms.scss +++ b/core/static/core/forms.scss @@ -107,7 +107,7 @@ form { } } - label { + label, legend { display: block; margin-bottom: 8px; diff --git a/core/static/core/style.scss b/core/static/core/style.scss index e094e215..61eb71e0 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -429,6 +429,16 @@ body { .row { display: flex; 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--------------------------------*/ diff --git a/counter/static/bundled/counter/components/ajax-select-index.ts b/counter/static/bundled/counter/components/ajax-select-index.ts index a2d61a48..08caa15e 100644 --- a/counter/static/bundled/counter/components/ajax-select-index.ts +++ b/counter/static/bundled/counter/components/ajax-select-index.ts @@ -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 CounterSchema, + type ProductTypeSchema, type SimpleProductSchema, counterSearchCounter, productSearchProducts, + producttypeFetchAll, } from "#openapi"; @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 { + // 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 `
+ ${sanitize(item.name)} +
`; + } + + protected renderItem(item: ProductTypeSchema, sanitize: typeof escape_html) { + return `${sanitize(item.name)}`; + } +} + @registerComponent("counter-ajax-select") export class CounterAjaxSelect extends AjaxSelect { protected valueField = "id"; diff --git a/counter/static/bundled/counter/product-list-index.ts b/counter/static/bundled/counter/product-list-index.ts index acc4e8ef..4c08eae5 100644 --- a/counter/static/bundled/counter/product-list-index.ts +++ b/counter/static/bundled/counter/product-list-index.ts @@ -3,6 +3,7 @@ import { csv } from "#core:utils/csv"; import { History, getCurrentUrlParams, updateQueryString } from "#core:utils/history"; import type { NestedKeyOf } from "#core:utils/types"; import { showSaveFilePicker } from "native-file-system-adapter"; +import type TomSelect from "tom-select"; import { type ProductSchema, type ProductSearchProductsDetailedData, @@ -58,6 +59,7 @@ document.addEventListener("alpine:init", () => { productStatus: "" as "active" | "archived" | "both", search: "", + productTypes: [] as string[], pageSize: defaultPageSize, page: defaultPage, @@ -65,17 +67,22 @@ document.addEventListener("alpine:init", () => { const url = getCurrentUrlParams(); this.search = url.get("search") || ""; 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(); - for (const param of ["search", "productStatus"]) { + const searchParams = ["search", "productStatus", "productTypes"]; + for (const param of searchParams) { this.$watch(param, () => { this.page = defaultPage; }); } - for (const param of ["search", "productStatus", "page"]) { + for (const param of [...searchParams, "page"]) { this.$watch(param, async (value: string) => { updateQueryString(param, value, History.Replace); this.nbPages = 0; - this.products = {}; await this.load(); }); } @@ -100,6 +107,8 @@ document.addEventListener("alpine:init", () => { search: search, // biome-ignore lint/style/useNamingConvention: api is in snake_case is_archived: isArchived, + // biome-ignore lint/style/useNamingConvention: api is in snake_case + product_type: this.productTypes, }, }; }, diff --git a/counter/templates/counter/product_list.jinja b/counter/templates/counter/product_list.jinja index 7280e119..3a52e747 100644 --- a/counter/templates/counter/product_list.jinja +++ b/counter/templates/counter/product_list.jinja @@ -6,45 +6,60 @@ {% endblock %} {% block additional_js %} + {% endblock %} {% block additional_css %} + + {% endblock %} {% block content %}
+

{% trans %}Filter products{% endtrans %}

-

{% trans %}Filter products{% endtrans %}

+
+ +
+ + +
+
+ {% trans %}Product state{% endtrans %} +
+ + +
+
+ + +
+
+ + +
+
+
- - -
-
-
- - -
-
- - -
-
- - -
+ + +
-

- {% trans %}Product list{% endtrans %} -

+

{% trans %}Product list{% endtrans %}

diff --git a/counter/widgets/select.py b/counter/widgets/select.py index 78c92862..875c0aca 100644 --- a/counter/widgets/select.py +++ b/counter/widgets/select.py @@ -1,8 +1,12 @@ from pydantic import TypeAdapter from core.views.widgets.select import AutoCompleteSelect, AutoCompleteSelectMultiple -from counter.models import Counter, Product -from counter.schemas import SimpleProductSchema, SimplifiedCounterSchema +from counter.models import Counter, Product, ProductType +from counter.schemas import ( + ProductTypeSchema, + SimpleProductSchema, + SimplifiedCounterSchema, +) _js = ["bundled/counter/components/ajax-select-index.ts"] @@ -33,3 +37,17 @@ class AutoCompleteSelectMultipleProduct(AutoCompleteSelectMultiple): model = Product adapter = TypeAdapter(list[SimpleProductSchema]) 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 diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index d5959e35..39f50561 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "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" "Last-Translator: Maréchal \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/edit.jinja:15 core/templates/core/edit.jinja:20 #: 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/user_godfathers.jinja:61 #: core/templates/core/user_godfathers_tree.jinja:85 @@ -971,7 +971,7 @@ msgstr "Comptoir" msgid "Products" 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" 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." #: 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 msgid "Add" msgstr "Ajouter" @@ -1192,7 +1192,7 @@ msgid "Show" msgstr "Montrer" #: 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" 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 " "each of their groups." msgstr "" -"Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes les " -"permissions de chacun de ses groupes." +"Les groupes auxquels cet utilisateur appartient. Un utilisateur aura toutes " +"les permissions de chacun de ses groupes." #: core/models.py:277 msgid "profile" @@ -2764,11 +2764,11 @@ msgstr "Tout désélectionner" msgid "You're seeing the history of 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" msgstr "actuel" -#: core/templates/core/macros_pages.jinja:21 +#: core/templates/core/macros_pages.jinja:22 msgid "Edit page" msgstr "Éditer la page" @@ -3026,7 +3026,7 @@ msgstr "Facture eboutic" msgid "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" msgstr "L'utilisateur n'a pas de compte" @@ -3326,7 +3326,8 @@ msgstr "Outils utilisateurs" msgid "Sith management" 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" 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" 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" msgstr "Parrain / Marraine" -#: core/views/forms.py:303 +#: core/views/forms.py:307 msgid "Godchild" 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" msgstr "Choisir un utilisateur" -#: core/views/forms.py:322 +#: core/views/forms.py:326 msgid "This user does not exist" msgstr "Cet utilisateur n'existe pas" -#: core/views/forms.py:324 +#: core/views/forms.py:328 msgid "You cannot be related to yourself" msgstr "Vous ne pouvez pas être relié à vous-même" -#: core/views/forms.py:336 +#: core/views/forms.py:340 #, python-format msgid "%s is already your godfather" msgstr "%s est déjà votre parrain/marraine" -#: core/views/forms.py:342 +#: core/views/forms.py:346 #, python-format msgid "%s is already your godchild" 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." #: counter/templates/counter/product_list.jinja:5 -#: counter/templates/counter/product_list.jinja:46 +#: counter/templates/counter/product_list.jinja:62 msgid "Product list" msgstr "Liste des produits" -#: counter/templates/counter/product_list.jinja:20 +#: counter/templates/counter/product_list.jinja:22 msgid "Filter products" msgstr "Filtrer les produits" -#: counter/templates/counter/product_list.jinja:22 +#: counter/templates/counter/product_list.jinja:27 msgid "Product name" msgstr "Nom du produit" -#: counter/templates/counter/product_list.jinja:33 -#, fuzzy -#| msgid "Archived products" +#: counter/templates/counter/product_list.jinja:36 +msgid "Product state" +msgstr "Etat du produit" + +#: counter/templates/counter/product_list.jinja:39 msgid "Active products" -msgstr "Produits archivés" +msgstr "Produits actifs" -#: counter/templates/counter/product_list.jinja:41 -#, fuzzy -#| msgid "products" +#: counter/templates/counter/product_list.jinja:47 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" msgstr "Nouveau produit"