mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-10 11:59:23 +00:00
Make products filterable by product type
This commit is contained in:
@ -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<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")
|
||||
export class CounterAjaxSelect extends AjaxSelect {
|
||||
protected valueField = "id";
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -6,45 +6,60 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_css %}
|
||||
<link rel="stylesheet" href="{{ static("core/components/card.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 %}
|
||||
|
||||
{% block content %}
|
||||
<main x-data="productList">
|
||||
<h4 class="margin-bottom">{% trans %}Filter products{% endtrans %}</h4>
|
||||
<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>
|
||||
<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="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>
|
||||
<label for="type-search-input">{% trans %}Product type{% endtrans %}</label>
|
||||
<product-type-ajax-select
|
||||
id="type-search-input"
|
||||
name="product-type"
|
||||
x-ref="productTypesInput"
|
||||
multiple
|
||||
>
|
||||
</product-type-ajax-select>
|
||||
</fieldset>
|
||||
</form>
|
||||
<h3 @click="console.log(totalCount, nbPages())" class="margin-bottom">
|
||||
{% trans %}Product list{% endtrans %}
|
||||
</h3>
|
||||
<h3 class="margin-bottom">{% trans %}Product list{% endtrans %}</h3>
|
||||
|
||||
<div class="row margin-bottom">
|
||||
<a href="{{ url('counter:new_product') }}" class="btn btn-blue">
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user