diff --git a/client/src/app/core/models/recipe.model.ts b/client/src/app/core/models/recipe.model.ts index ddc7858..0d224cc 100644 --- a/client/src/app/core/models/recipe.model.ts +++ b/client/src/app/core/models/recipe.model.ts @@ -103,8 +103,8 @@ export interface Recipe01 { } export interface Topping { - ToppingGroup: ToppingGroup; - ToppingList: ToppingList; + ToppingGroup: ToppingGroup[]; + ToppingList: ToppingList[]; } export interface ToppingGroup { @@ -157,8 +157,8 @@ export interface MatRecipe { } export interface ToppingSet { - ListGroupID: string; - defaultIDSelect: string; + ListGroupID: string[]; + defaultIDSelect: number; groupID: string; isUse: string; } diff --git a/client/src/app/core/services/topping.service.ts b/client/src/app/core/services/topping.service.ts index 867fa6c..0f282d3 100644 --- a/client/src/app/core/services/topping.service.ts +++ b/client/src/app/core/services/topping.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { environment } from 'src/environments/environment'; -import { Topping } from '../models/recipe.model'; +import { Topping, ToppingSet } from '../models/recipe.model'; @Injectable({ providedIn: 'root', @@ -22,4 +22,17 @@ export class ToppingService { } ); } + + getToppingsOfRecipe(country: string, filename: string, productCode: string): Observable { + return this._httpClient.get( + `${environment.api}/recipes/${country}/${filename}/${productCode}/toppings`, + { + params: { + country: country, + filename: filename, + }, + withCredentials: true + } + ); + } } diff --git a/client/src/app/features/recipes/recipe-details/recipe-details.component.html b/client/src/app/features/recipes/recipe-details/recipe-details.component.html index 835f26e..b7c93e3 100644 --- a/client/src/app/features/recipes/recipe-details/recipe-details.component.html +++ b/client/src/app/features/recipes/recipe-details/recipe-details.component.html @@ -96,8 +96,12 @@ [productCode]="productCode" (recipeListFormChange)="onRecipeListFormChange($event)" > -
+ +
{ - this.topping = data; + this._toppingService.getToppingsOfRecipe(this.department, this._recipeService.getCurrentFile(), this.productCode).subscribe((data) => { + this.toppingSet = data; + console.log('Toppings', data); }) // snap recipe detail form value @@ -213,7 +220,15 @@ export class RecipeDetailsComponent implements OnInit { this.isValueChanged ||= repl != undefined; } + onToppingListChange(tpl: unknown[]) { + // console.log('Topping List Form Changed', tpl); + this.tpl = tpl as never[]; + this.isValueChanged ||= tpl != undefined; + + } + isEditable(){ return this._userService.getCurrentUser()!.permissions.includes(UserPermissions.EDITOR); } + } diff --git a/client/src/app/features/recipes/recipe-details/recipe-toppingset/recipe-toppingset.component.html b/client/src/app/features/recipes/recipe-details/recipe-toppingset/recipe-toppingset.component.html new file mode 100644 index 0000000..f0f4004 --- /dev/null +++ b/client/src/app/features/recipes/recipe-details/recipe-toppingset/recipe-toppingset.component.html @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + +
Slot (Material Id)Is UseGroupDefault
{{i+1}} ({{8110+i+1}}) + + {{ item.name }} + + + + +
diff --git a/client/src/app/features/recipes/recipe-details/recipe-toppingset/recipe-toppingset.component.ts b/client/src/app/features/recipes/recipe-details/recipe-toppingset/recipe-toppingset.component.ts new file mode 100644 index 0000000..5f51596 --- /dev/null +++ b/client/src/app/features/recipes/recipe-details/recipe-toppingset/recipe-toppingset.component.ts @@ -0,0 +1,163 @@ +import { NgFor } from '@angular/common'; +import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core'; +import { FormArray, FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { forEach, isEqual } from 'lodash'; +import { Topping, ToppingGroup, ToppingSet } from 'src/app/core/models/recipe.model'; +import { RecipeService } from 'src/app/core/services/recipe.service'; +import { ToppingService } from 'src/app/core/services/topping.service'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-recipe-toppingset', + templateUrl: './recipe-toppingset.component.html', + standalone: true, + imports: [ NgFor, FormsModule, ReactiveFormsModule, CommonModule, NgSelectModule ], +}) +export class RecipeToppingsetComponent implements OnInit { + @Input() productCode!: string; + @Output() toppingSetChange = new EventEmitter(); + + toppingSetList: ToppingSet[] = []; + allToppings: Topping | undefined = undefined; + + allToppingsDefinitions: { groupId: string; name: string }[] | null = []; + + private _toppingSetOriginalArray!: ToppingSet[]; + + constructor( + private _recipeService: RecipeService, + private _toppingService: ToppingService, + private _formBuilder: FormBuilder, + ) {} + + toppingForm = this._formBuilder.group( + { + toppingList: this._formBuilder.array([]), + }, + { updateOn: 'blur' } + ); + + get toppingList(): FormArray { + return this.toppingForm.get('toppingList') as FormArray; + } + + ngOnInit(): void { + this._toppingService + .getToppingsOfRecipe( + this._recipeService.getCurrentCountry(), + this._recipeService.getCurrentFile(), + this.productCode + ) + .subscribe((data) => { + console.log("getToppingset",data); + this.toppingSetList = data; + // this.toppingForm.patchValue({toppingList: data}); + this._toppingSetOriginalArray = data; + + + data.forEach((toppingSet: ToppingSet) => { + // console.log("getToppingset",toppingSet); + + // toppingSet.ListGroupID = toppingSet.ListGroupID.map((id) => id.toString()); + + this.toppingList.push( + this._formBuilder.group({ + isUse: toppingSet.isUse, + groupID: toppingSet.groupID, + defaultIDSelect: toppingSet.defaultIDSelect, + ListGroupID: toppingSet.ListGroupID, + }) + ); + }) + + console.log("controls",this.toppingList.controls); + + // this.toppingSetChange.emit(this.toppingSetList); + }); + + + // fetch all toppings : group and list + this._toppingService + .getToppings( + this._recipeService.getCurrentCountry(), + this._recipeService.getCurrentFile() + ) + .subscribe((data) => { + this.allToppings = data; + console.log("allToppings",data); + + data.ToppingGroup.forEach((group: ToppingGroup) => { + if(this.allToppingsDefinitions != null) { + // this.allToppingsDefinitions = {}; + this.allToppingsDefinitions.push({ groupId: group.groupID, name: group.name }); + } + }) + + console.log(this.allToppingsDefinitions); + }); + + this.toppingForm.valueChanges.subscribe((value) => { + + // check if list group id is not list + //transform + for(let i = 0; i < value.toppingList!.length; i++) { + let toppingSet = value.toppingList![i] as any; + + if(!Array.isArray(toppingSet.ListGroupID)) { + toppingSet.ListGroupID = [ + parseInt(toppingSet.groupID), + 0, + 0, + 0 + ]; + } + + } + let isDiff = !isEqual(this._toppingSetOriginalArray, value.toppingList!); + + if(isDiff) { + + let newToppingSetList: any[] = []; + + forEach(value.toppingList!, (toppingSet:any) => { + toppingSet.defaultIDSelect = parseInt(toppingSet.defaultIDSelect); + newToppingSetList.push(toppingSet); + }) + + console.log("newToppingList", newToppingSetList); + this.toppingSetChange.emit(newToppingSetList as unknown[] ); + + } else { + + console.log("newToppingListNoChange", value.toppingList); + this.toppingSetChange.emit([]); + } + + + }) + } + + // match group id to its name + getGroupName(groupID: string) { + + // check if array + if(Array.isArray(this.allToppings!.ToppingGroup)) { + return (this.allToppings!.ToppingGroup as ToppingGroup[]).find((group) => group.groupID == groupID)?.name; + } else { + return undefined; + } + } + + openToppingList(i: any){ + console.log("select", i) + } + + currentGroupId(i: any){ + console.log("currentGroupId", i); + return (this.toppingForm.value.toppingList![i] as any).groupID as string; + } + + + compareFunc = (a: any,b: any) => a.toString() === b.toString(); +} diff --git a/client/src/app/features/recipes/recipes.component.html b/client/src/app/features/recipes/recipes.component.html index ea6cd99..6c532c0 100644 --- a/client/src/app/features/recipes/recipes.component.html +++ b/client/src/app/features/recipes/recipes.component.html @@ -204,6 +204,8 @@
+ +
Last Updated: @@ -328,7 +330,7 @@ > item.name.toLowerCase().includes(term.toLowerCase()) || item.materialId.toString().includes(term); + + + addToCopyList(data: any){ + + if(this.copyList.includes(data)){ + + let index = this.copyList.indexOf(data); + this.copyList.splice(index, 1); + } else { + this.copyList = [...this.copyList, data]; + } + + } + + + async copyToTsv(data: any){ + + await copy(transformToTSV(data)).then( (value) => { + console.log('copyToTsv', value); + }).catch( (err) => { + console.log('copyToTsvErr', err); + }); + } } diff --git a/client/src/app/shared/helpers/copy.ts b/client/src/app/shared/helpers/copy.ts new file mode 100644 index 0000000..76bc635 --- /dev/null +++ b/client/src/app/shared/helpers/copy.ts @@ -0,0 +1,29 @@ +export async function copy(source: any) { + try { + await navigator.clipboard.writeText(source); + // alert("copied"); + } catch (error) { + console.error("Async: Could not copy text: ", error); + } +} + +export function transformToTSV(data: any){ + let csv = ""; + if(Array.isArray(data)){ + for(let i = 0; i < data.length; i++){ + for(const key in data[i]){ + csv += data[i][key] + "\t"; + } + csv += "\n"; + } + } else { + for(const key in data){ + csv += data[key] + "\t"; + } + } + + // for(const key in data){ + // csv += key + "\t" + data[key] + "\t"; + // } + return csv; +} diff --git a/server/routers/recipe.go b/server/routers/recipe.go index e393163..0b07daa 100644 --- a/server/routers/recipe.go +++ b/server/routers/recipe.go @@ -33,6 +33,7 @@ type RecipeRouter struct { var ( binaryApiLock sync.Mutex + updateMutex = sync.Mutex{} ) func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService, taoLogger *logger.TaoLogger) *RecipeRouter { @@ -56,6 +57,8 @@ func (rr *RecipeRouter) Route(r chi.Router) { r.Get("/{country}/{filename}/toppings", rr.getToppings) + r.Get("/{country}/{filename}/{product_code}/toppings", rr.getToppingsOfRecipe) + r.Get("/{country}/{filename}/json", rr.getRecipeJson) r.Post("/edit/{country}/{filename}", rr.updateRecipe) @@ -347,6 +350,9 @@ func (rr *RecipeRouter) updateRecipe(w http.ResponseWriter, r *http.Request) { return } + // Lock + updateMutex.Lock() + defer updateMutex.Unlock() targetRecipe := rr.data.GetRecipe(countryID, filename) rr.taoLogger.Log.Debug("Target => ", zap.Any("target", targetRecipe.MachineSetting.ConfigNumber)) @@ -495,39 +501,28 @@ func (rr *RecipeRouter) getToppings(w http.ResponseWriter, r *http.Request) { func (rr *RecipeRouter) getToppingsOfRecipe(w http.ResponseWriter, r *http.Request) { - // countryID := chi.URLParam(r, "country") - // filename := chi.URLParam(r, "filename") - // productCode := chi.URLParam(r, "product_code") + countryID := chi.URLParam(r, "country") + filename := chi.URLParam(r, "filename") + productCode := chi.URLParam(r, "product_code") w.Header().Add("Content-Type", "application/json") // all toppings // allToppings := rr.data.GetToppings(countryID, filename) - // topps, err := rr.data.GetToppingsOfRecipe(countryID, filename, productCode) + topps, err := rr.data.GetToppingsOfRecipe(countryID, filename, productCode) - // expandedToppings := map[string]interface{}{} + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } - // for _, v := range allToppings.ToppingGroup { - // for _, t := range topps { - // if v.GroupID == t.ListGroupID[0] { - // expandedToppings[v.GroupID] = v - // break - // } - // } - // } - - // if err != nil { - // http.Error(w, err.Error(), http.StatusNotFound) - // return - // } - - // json.NewEncoder(w).Encode() + json.NewEncoder(w).Encode(topps) } func (rr *RecipeRouter) doMergeJson(w http.ResponseWriter, r *http.Request) { // TODO: v2, change to binary instead - if !binaryAPIhandler(w, r) { + if !APIhandler(w, r) { rr.taoLogger.Log.Warn("RecipeRouter.doMergeJson", zap.Error(errors.New("API is busy"))) return } else { @@ -537,7 +532,7 @@ func (rr *RecipeRouter) doMergeJson(w http.ResponseWriter, r *http.Request) { // TODO: add binary command here } -func binaryAPIhandler(w http.ResponseWriter, r *http.Request) bool { +func APIhandler(w http.ResponseWriter, r *http.Request) bool { timeout := 10 * time.Second if !lockThenTimeout(&binaryApiLock, timeout) {