diff --git a/core/static/bundled/utils/api.ts b/core/static/bundled/utils/api.ts index ac647cd7..5d72b3b6 100644 --- a/core/static/bundled/utils/api.ts +++ b/core/static/bundled/utils/api.ts @@ -22,10 +22,13 @@ type PaginatedEndpoint = ( // TODO : If one day a test workflow is made for JS in this project // please test this function. A all cost. +/** + * Load complete dataset from paginated routes. + */ export const paginated = async ( endpoint: PaginatedEndpoint, options?: PaginatedRequest, -) => { +): Promise => { const maxPerPage = 199; const queryParams = options ?? {}; queryParams.query = queryParams.query ?? {}; diff --git a/core/static/core/style.scss b/core/static/core/style.scss index d7a396d1..88bb6cd2 100644 --- a/core/static/core/style.scss +++ b/core/static/core/style.scss @@ -198,6 +198,9 @@ body { margin: 20px auto 0; /*---------------------------------NAV---------------------------------*/ + a.btn { + display: inline-block; + } .btn { font-size: 15px; font-weight: normal; @@ -409,6 +412,11 @@ body { } } + .row { + display: flex; + flex-wrap: wrap; + } + /*---------------------------------NEWS--------------------------------*/ #news { display: flex; diff --git a/counter/static/bundled/counter/product-list-index.ts b/counter/static/bundled/counter/product-list-index.ts new file mode 100644 index 00000000..80cd8627 --- /dev/null +++ b/counter/static/bundled/counter/product-list-index.ts @@ -0,0 +1,88 @@ +import { History, getCurrentUrlParams, updateQueryString } from "#core:utils/history"; +import { + type ProductSchema, + productSearchProductsDetailed, + type ProductSearchProductsDetailedData, +} from "#openapi"; + +type ProductType = string; +type GroupedProducts = Record; + +const defaultPageSize = 100; +const defaultPage = 1; + +document.addEventListener("alpine:init", () => { + Alpine.data("productList", () => ({ + loading: false, + csvLoading: false, + products: {} as GroupedProducts, + + /** Total number of elements corresponding to the current query. */ + nbPages: 0, + + isArchived: null as boolean, + search: "", + pageSize: defaultPageSize, + page: defaultPage, + + async init() { + const url = getCurrentUrlParams(); + this.search = url.get("search") || ""; + this.isArchived = url.get("isArchived") ?? false; + await this.load(); + for (const param of ["search", "isArchived"]) { + this.$watch(param, () => { + this.page = defaultPage; + }); + } + for (const param of ["search", "isArchived", "page"]) { + this.$watch(param, async (value: string) => { + updateQueryString(param, value, History.Replace); + this.nbPages = 0; + this.products = {}; + await this.load(); + }); + } + }, + + /** + * Build the object containing the query parameters corresponding + * to the current filters + */ + getQueryParams(): ProductSearchProductsDetailedData { + const search = this.search.length > 0 ? this.search : null; + const archived = ["true", "false"].includes(this.isArchived) + ? this.isArchived + : undefined; + return { + query: { + page: this.page, + // biome-ignore lint/style/useNamingConvention: api is in snake_case + page_size: this.pageSize, + search: search, + // biome-ignore lint/style/useNamingConvention: api is in snake_case + is_archived: archived, + }, + }; + }, + + /** + * Fetch the products corresponding to the current filters + */ + async load() { + this.loading = true; + const options = this.getQueryParams(); + const resp = await productSearchProductsDetailed(options); + this.nbPages = Math.ceil(resp.data.count / defaultPageSize); + this.products = resp.data.results.reduce((acc, curr) => { + const key = curr.product_type?.name ?? gettext("Uncategorized"); + if (!(key in acc)) { + acc[key] = []; + } + acc[key].push(curr); + return acc; + }, {}); + this.loading = false; + }, + })); +}); diff --git a/counter/templates/counter/product_list.jinja b/counter/templates/counter/product_list.jinja index 881d7800..92d955cc 100644 --- a/counter/templates/counter/product_list.jinja +++ b/counter/templates/counter/product_list.jinja @@ -1,26 +1,61 @@ {% extends "core/base.jinja" %} +{% from "core/macros.jinja" import paginate_alpine %} {% block title %} {% trans %}Product list{% endtrans %} {% endblock %} -{% block content %} - {% if current_tab == "products" %} -

{% trans %}New product{% endtrans %}

- {% endif %} -

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

- {%- for product_type, products in object_list -%} -

{{ product_type or _("Uncategorized") }}

- - {%- else -%} - {% trans %}There is no products in this website.{% endtrans %} - {%- endfor -%} +{% block additional_js %} + {% endblock %} +{% block content %} +
+
+

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

+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+

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

+ + {% trans %}New product{% endtrans %} + + - + + {{ paginate_alpine("page", "nbPages") }} +
+{% endblock %} diff --git a/counter/urls.py b/counter/urls.py index 91564a8b..885b4b14 100644 --- a/counter/urls.py +++ b/counter/urls.py @@ -16,8 +16,6 @@ from django.urls import path from counter.views.admin import ( - ActiveProductListView, - ArchivedProductListView, CounterCreateView, CounterDeleteView, CounterEditPropView, @@ -27,6 +25,7 @@ from counter.views.admin import ( CounterStatView, ProductCreateView, ProductEditView, + ProductListView, ProductTypeCreateView, ProductTypeEditView, ProductTypeListView, @@ -108,12 +107,7 @@ urlpatterns = [ CashSummaryEditView.as_view(), name="cash_summary_edit", ), - path("admin/product/list/", ActiveProductListView.as_view(), name="product_list"), - path( - "admin/product/list_archived/", - ArchivedProductListView.as_view(), - name="product_list_archived", - ), + path("admin/product/", ProductListView.as_view(), name="product_list"), path("admin/product/create/", ProductCreateView.as_view(), name="new_product"), path( "admin/product//", diff --git a/counter/views/admin.py b/counter/views/admin.py index aa7e2c50..ab46581b 100644 --- a/counter/views/admin.py +++ b/counter/views/admin.py @@ -12,19 +12,16 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # -import itertools from datetime import timedelta -from operator import itemgetter from django.conf import settings from django.core.exceptions import PermissionDenied -from django.db.models import F from django.forms import CheckboxSelectMultiple from django.forms.models import modelform_factory from django.shortcuts import get_object_or_404 from django.urls import reverse, reverse_lazy from django.utils import timezone -from django.views.generic import DetailView, ListView +from django.views.generic import DetailView, ListView, TemplateView from django.views.generic.edit import CreateView, DeleteView, UpdateView from core.utils import get_semester_code, get_start_of_semester @@ -125,40 +122,9 @@ class ProductTypeEditView(CounterAdminTabsMixin, CounterAdminMixin, UpdateView): current_tab = "products" -class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, ListView): - model = Product - queryset = Product.objects.values("id", "name", "code", "product_type__name") - template_name = "counter/product_list.jinja" - ordering = [ - F("product_type__order").asc(nulls_last=True), - "product_type", - "name", - ] - - def get_context_data(self, **kwargs): - res = super().get_context_data(**kwargs) - res["object_list"] = itertools.groupby( - res["object_list"], key=itemgetter("product_type__name") - ) - return res - - -class ArchivedProductListView(ProductListView): - """A list view for the admins.""" - - current_tab = "archive" - - def get_queryset(self): - return super().get_queryset().filter(archived=True) - - -class ActiveProductListView(ProductListView): - """A list view for the admins.""" - +class ProductListView(CounterAdminTabsMixin, CounterAdminMixin, TemplateView): current_tab = "products" - - def get_queryset(self): - return super().get_queryset().filter(archived=False) + template_name = "counter/product_list.jinja" class ProductCreateView(CounterAdminTabsMixin, CounterAdminMixin, CreateView): diff --git a/counter/views/mixins.py b/counter/views/mixins.py index 2e88f54c..b72d6694 100644 --- a/counter/views/mixins.py +++ b/counter/views/mixins.py @@ -93,11 +93,6 @@ class CounterAdminTabsMixin(TabedViewMixin): "slug": "products", "name": _("Products"), }, - { - "url": reverse_lazy("counter:product_list_archived"), - "slug": "archive", - "name": _("Archived products"), - }, { "url": reverse_lazy("counter:product_type_list"), "slug": "product_types", diff --git a/locale/fr/LC_MESSAGES/djangojs.po b/locale/fr/LC_MESSAGES/djangojs.po index 414bb603..43d19dbd 100644 --- a/locale/fr/LC_MESSAGES/djangojs.po +++ b/locale/fr/LC_MESSAGES/djangojs.po @@ -122,6 +122,38 @@ msgstr "photos.%(extension)s" msgid "captured.%s" msgstr "capture.%s" +#: counter/static/bundled/counter/product-list-index.ts:39 +msgid "name" +msgstr "nom" + +#: counter/static/bundled/counter/product-list-index.ts:42 +msgid "product type" +msgstr "type de produit" + +#: counter/static/bundled/counter/product-list-index.ts:44 +msgid "limit age" +msgstr "limite d'âge" + +#: counter/static/bundled/counter/product-list-index.ts:45 +msgid "purchase price" +msgstr "prix d'achat" + +#: counter/static/bundled/counter/product-list-index.ts:46 +msgid "selling price" +msgstr "prix de vente" + +#: counter/static/bundled/counter/product-list-index.ts:47 +msgid "archived" +msgstr "archivé" + +#: counter/static/bundled/counter/product-list-index.ts:114 +msgid "Uncategorized" +msgstr "Sans catégorie" + +#: counter/static/bundled/counter/product-list-index.ts:132 +msgid "products.csv" +msgstr "produits.csv" + #: counter/static/bundled/counter/product-type-index.ts:36 msgid "Products types reordered!" msgstr "Types de produits réordonnés !"