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

@ -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";

View File

@ -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,
},
};
},