mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-10-31 00:53:08 +00:00 
			
		
		
		
	Merge pull request #959 from ae-utbm/counter-click-step-4
Make counter click client side first
This commit is contained in:
		
							
								
								
									
										25
									
								
								counter/static/bundled/counter/basket.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								counter/static/bundled/counter/basket.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import type { Product } from "#counter:counter/types"; | ||||
|  | ||||
| export class BasketItem { | ||||
|   quantity: number; | ||||
|   product: Product; | ||||
|   quantityForTrayPrice: number; | ||||
|   errors: string[]; | ||||
|  | ||||
|   constructor(product: Product, quantity: number) { | ||||
|     this.quantity = quantity; | ||||
|     this.product = product; | ||||
|     this.errors = []; | ||||
|   } | ||||
|  | ||||
|   getBonusQuantity(): number { | ||||
|     if (!this.product.hasTrayPrice) { | ||||
|       return 0; | ||||
|     } | ||||
|     return Math.floor(this.quantity / this.product.quantityForTrayPrice); | ||||
|   } | ||||
|  | ||||
|   sum(): number { | ||||
|     return (this.quantity - this.getBonusQuantity()) * this.product.price; | ||||
|   } | ||||
| } | ||||
| @@ -15,6 +15,10 @@ export class CounterProductSelect extends AutoCompleteSelectBase { | ||||
|     return ["FIN", "ANN"]; | ||||
|   } | ||||
|  | ||||
|   public getSelectedProduct(): [number, string] { | ||||
|     return parseProduct(this.widget.getValue() as string); | ||||
|   } | ||||
|  | ||||
|   protected attachBehaviors(): void { | ||||
|     this.allowMultipleProducts(); | ||||
|   } | ||||
|   | ||||
| @@ -1,42 +1,100 @@ | ||||
| import { exportToHtml } from "#core:utils/globals"; | ||||
| import type TomSelect from "tom-select"; | ||||
|  | ||||
| interface CounterConfig { | ||||
|   csrfToken: string; | ||||
|   clickApiUrl: string; | ||||
|   sessionBasket: Record<number, BasketItem>; | ||||
|   customerBalance: number; | ||||
|   customerId: number; | ||||
| } | ||||
| interface BasketItem { | ||||
|   // biome-ignore lint/style/useNamingConvention: talking with python | ||||
|   bonus_qty: number; | ||||
|   price: number; | ||||
|   qty: number; | ||||
| } | ||||
| import { BasketItem } from "#counter:counter/basket"; | ||||
| import type { CounterConfig, ErrorMessage } from "#counter:counter/types"; | ||||
|  | ||||
| exportToHtml("loadCounter", (config: CounterConfig) => { | ||||
|   document.addEventListener("alpine:init", () => { | ||||
|     Alpine.data("counter", () => ({ | ||||
|       basket: config.sessionBasket, | ||||
|       basket: {} as Record<string, BasketItem>, | ||||
|       errors: [], | ||||
|       customerBalance: config.customerBalance, | ||||
|       codeField: undefined, | ||||
|       alertMessage: { | ||||
|         content: "", | ||||
|         show: false, | ||||
|         timeout: null, | ||||
|       }, | ||||
|  | ||||
|       init() { | ||||
|         // Fill the basket with the initial data | ||||
|         for (const entry of config.formInitial) { | ||||
|           if (entry.id !== undefined && entry.quantity !== undefined) { | ||||
|             this.addToBasket(entry.id, entry.quantity); | ||||
|             this.basket[entry.id].errors = entry.errors ?? []; | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         this.codeField = this.$refs.codeField; | ||||
|         this.codeField.widget.focus(); | ||||
|  | ||||
|         // It's quite tricky to manually apply attributes to the management part | ||||
|         // of a formset so we dynamically apply it here | ||||
|         this.$refs.basketManagementForm | ||||
|           .querySelector("#id_form-TOTAL_FORMS") | ||||
|           .setAttribute(":value", "getBasketSize()"); | ||||
|       }, | ||||
|  | ||||
|       removeFromBasket(id: string) { | ||||
|         delete this.basket[id]; | ||||
|       }, | ||||
|  | ||||
|       addToBasket(id: string, quantity: number): ErrorMessage { | ||||
|         const item: BasketItem = | ||||
|           this.basket[id] || new BasketItem(config.products[id], 0); | ||||
|  | ||||
|         const oldQty = item.quantity; | ||||
|         item.quantity += quantity; | ||||
|  | ||||
|         if (item.quantity <= 0) { | ||||
|           delete this.basket[id]; | ||||
|           return ""; | ||||
|         } | ||||
|  | ||||
|         this.basket[id] = item; | ||||
|  | ||||
|         if (this.sumBasket() > this.customerBalance) { | ||||
|           item.quantity = oldQty; | ||||
|           if (item.quantity === 0) { | ||||
|             delete this.basket[id]; | ||||
|           } | ||||
|           return gettext("Not enough money"); | ||||
|         } | ||||
|  | ||||
|         return ""; | ||||
|       }, | ||||
|  | ||||
|       getBasketSize() { | ||||
|         return Object.keys(this.basket).length; | ||||
|       }, | ||||
|  | ||||
|       sumBasket() { | ||||
|         if (!this.basket || Object.keys(this.basket).length === 0) { | ||||
|         if (this.getBasketSize() === 0) { | ||||
|           return 0; | ||||
|         } | ||||
|         const total = Object.values(this.basket).reduce( | ||||
|           (acc: number, cur: BasketItem) => acc + cur.qty * cur.price, | ||||
|           (acc: number, cur: BasketItem) => acc + cur.sum(), | ||||
|           0, | ||||
|         ) as number; | ||||
|         return total / 100; | ||||
|         return total; | ||||
|       }, | ||||
|  | ||||
|       showAlertMessage(message: string) { | ||||
|         if (this.alertMessage.timeout !== null) { | ||||
|           clearTimeout(this.alertMessage.timeout); | ||||
|         } | ||||
|         this.alertMessage.content = message; | ||||
|         this.alertMessage.show = true; | ||||
|         this.alertMessage.timeout = setTimeout(() => { | ||||
|           this.alertMessage.show = false; | ||||
|           this.alertMessage.timeout = null; | ||||
|         }, 2000); | ||||
|       }, | ||||
|  | ||||
|       addToBasketWithMessage(id: string, quantity: number) { | ||||
|         const message = this.addToBasket(id, quantity); | ||||
|         if (message.length > 0) { | ||||
|           this.showAlertMessage(message); | ||||
|         } | ||||
|       }, | ||||
|  | ||||
|       onRefillingSuccess(event: CustomEvent) { | ||||
| @@ -50,33 +108,36 @@ exportToHtml("loadCounter", (config: CounterConfig) => { | ||||
|         this.codeField.widget.focus(); | ||||
|       }, | ||||
|  | ||||
|       async handleCode(event: SubmitEvent) { | ||||
|         const widget: TomSelect = this.codeField.widget; | ||||
|         const code = (widget.getValue() as string).toUpperCase(); | ||||
|         if (this.codeField.getOperationCodes().includes(code)) { | ||||
|           $(event.target).submit(); | ||||
|         } else { | ||||
|           await this.handleAction(event); | ||||
|       finish() { | ||||
|         if (this.getBasketSize() === 0) { | ||||
|           this.showAlertMessage(gettext("You can't send an empty basket.")); | ||||
|           return; | ||||
|         } | ||||
|         widget.clear(); | ||||
|         widget.focus(); | ||||
|         this.$refs.basketForm.submit(); | ||||
|       }, | ||||
|  | ||||
|       async handleAction(event: SubmitEvent) { | ||||
|         const payload = $(event.target).serialize(); | ||||
|         const request = new Request(config.clickApiUrl, { | ||||
|           method: "POST", | ||||
|           body: payload, | ||||
|           headers: { | ||||
|             // biome-ignore lint/style/useNamingConvention: this goes into http headers | ||||
|             Accept: "application/json", | ||||
|             "X-CSRFToken": config.csrfToken, | ||||
|           }, | ||||
|         }); | ||||
|         const response = await fetch(request); | ||||
|         const json = await response.json(); | ||||
|         this.basket = json.basket; | ||||
|         this.errors = json.errors; | ||||
|       cancel() { | ||||
|         location.href = config.cancelUrl; | ||||
|       }, | ||||
|  | ||||
|       handleCode() { | ||||
|         const [quantity, code] = this.codeField.getSelectedProduct() as [ | ||||
|           number, | ||||
|           string, | ||||
|         ]; | ||||
|  | ||||
|         if (this.codeField.getOperationCodes().includes(code.toUpperCase())) { | ||||
|           if (code === "ANN") { | ||||
|             this.cancel(); | ||||
|           } | ||||
|           if (code === "FIN") { | ||||
|             this.finish(); | ||||
|           } | ||||
|         } else { | ||||
|           this.addToBasketWithMessage(code, quantity); | ||||
|         } | ||||
|         this.codeField.widget.clear(); | ||||
|         this.codeField.widget.focus(); | ||||
|       }, | ||||
|     })); | ||||
|   }); | ||||
| @@ -85,7 +146,7 @@ exportToHtml("loadCounter", (config: CounterConfig) => { | ||||
| $(() => { | ||||
|   /* Accordion UI between basket and refills */ | ||||
|   // biome-ignore lint/suspicious/noExplicitAny: dealing with legacy jquery | ||||
|   ($("#click_form") as any).accordion({ | ||||
|   ($("#click-form") as any).accordion({ | ||||
|     heightStyle: "content", | ||||
|     activate: () => $(".focus").focus(), | ||||
|   }); | ||||
|   | ||||
							
								
								
									
										25
									
								
								counter/static/bundled/counter/types.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								counter/static/bundled/counter/types.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| type ErrorMessage = string; | ||||
|  | ||||
| export interface InitialFormData { | ||||
|   /* Used to refill the form when the backend raises an error */ | ||||
|   id?: keyof Record<string, Product>; | ||||
|   quantity?: number; | ||||
|   errors?: string[]; | ||||
| } | ||||
|  | ||||
| export interface CounterConfig { | ||||
|   customerBalance: number; | ||||
|   customerId: number; | ||||
|   products: Record<string, Product>; | ||||
|   formInitial: InitialFormData[]; | ||||
|   cancelUrl: string; | ||||
| } | ||||
|  | ||||
| export interface Product { | ||||
|   id: string; | ||||
|   code: string; | ||||
|   name: string; | ||||
|   price: number; | ||||
|   hasTrayPrice: boolean; | ||||
|   quantityForTrayPrice: number; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user