mirror of
https://github.com/ae-utbm/sith.git
synced 2024-12-22 15:51:19 +00:00
ajaxify the product admin page
This commit is contained in:
parent
3fc260a12c
commit
39b36aa509
@ -22,10 +22,13 @@ type PaginatedEndpoint<T> = <ThrowOnError extends boolean = false>(
|
||||
|
||||
// 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 <T>(
|
||||
endpoint: PaginatedEndpoint<T>,
|
||||
options?: PaginatedRequest,
|
||||
) => {
|
||||
): Promise<T[]> => {
|
||||
const maxPerPage = 199;
|
||||
const queryParams = options ?? {};
|
||||
queryParams.query = queryParams.query ?? {};
|
||||
|
@ -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;
|
||||
|
88
counter/static/bundled/counter/product-list-index.ts
Normal file
88
counter/static/bundled/counter/product-list-index.ts
Normal file
@ -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<ProductType, ProductSchema[]>;
|
||||
|
||||
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<GroupedProducts>((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;
|
||||
},
|
||||
}));
|
||||
});
|
@ -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" %}
|
||||
<p><a href="{{ url('counter:new_product') }}">{% trans %}New product{% endtrans %}</a></p>
|
||||
{% endif %}
|
||||
<h3>{% trans %}Product list{% endtrans %}</h3>
|
||||
{%- for product_type, products in object_list -%}
|
||||
<h4>{{ product_type or _("Uncategorized") }}</h4>
|
||||
<ul>
|
||||
{%- for product in products -%}
|
||||
<li><a href="{{ url('counter:product_edit', product_id=product.id) }}">{{ product.name }} ({{ product.code }})</a></li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
{%- else -%}
|
||||
{% trans %}There is no products in this website.{% endtrans %}
|
||||
{%- endfor -%}
|
||||
{% block additional_js %}
|
||||
<script type="module" src="{{ static("bundled/counter/product-list-index.ts") }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main x-data="productList">
|
||||
<form id="search-form">
|
||||
<h4>{% trans %}Filter products{% endtrans %}</h4>
|
||||
<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>
|
||||
<div class="row">
|
||||
<input type="radio" id="filter-active-products" x-model="isArchived" value="false">
|
||||
<label for="filter-active-products">{% trans %}Active products{% endtrans %}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="radio" id="filter-inactive-products" x-model="isArchived" value="true">
|
||||
<label for="filter-inactive-products">{% trans %}Archived products{% endtrans %}</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<input type="radio" id="filter-all-products" x-model="isArchived" value="">
|
||||
<label for="filter-all-products">{% trans %}All products{% endtrans %}</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<h3 @click="console.log(totalCount, nbPages())">{% trans %}Product list{% endtrans %}</h3>
|
||||
|
||||
<a href="{{ url('counter:new_product') }}" class="btn btn-blue">
|
||||
{% trans %}New product{% endtrans %} <i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<template x-if="loading">
|
||||
<section :aria-busy="loading"></section>
|
||||
</template>
|
||||
|
||||
|
||||
<template x-for="[category, cat_products] of Object.entries(products)" :key="category">
|
||||
<section>
|
||||
<h4 x-text="category"></h4>
|
||||
<ul>
|
||||
<template x-for="p in cat_products" :key="p.id">
|
||||
<li><a :href="p.url" x-text="`${p.name} (${p.code})`"></a></li>
|
||||
</template>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
{{ paginate_alpine("page", "nbPages") }}
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
@ -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/<int:product_id>/",
|
||||
|
@ -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):
|
||||
|
@ -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",
|
||||
|
@ -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 !"
|
||||
|
Loading…
Reference in New Issue
Block a user