From 820557a26857629bcfbb165abc26171558a051d4 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Tue, 5 Dec 2023 14:33:45 +0700 Subject: [PATCH] change save file method, add file changes history --- .../recipe-details.component.html | 661 ++++++++-------- .../recipe-details.component.ts | 361 +++++---- .../recipe-list/recipe-list.component.html | 114 +-- .../recipe-list/recipe-list.component.ts | 192 ++--- .../features/recipes/recipes.component.html | 600 ++++++++------- .../app/features/recipes/recipes.component.ts | 691 +++++++++-------- server/data/commit.go | 82 ++ server/helpers/misc.go | 238 ++++-- server/models/recipe.go | 319 ++++---- server/routers/recipe.go | 726 ++++++++++-------- server/services/cli/cli.go | 44 -- server/services/recipe/recipe.go | 396 +++++----- 12 files changed, 2388 insertions(+), 2036 deletions(-) create mode 100644 server/data/commit.go delete mode 100644 server/services/cli/cli.go 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 6c366de..ecec70b 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 @@ -1,329 +1,332 @@ -
-
-
-
-
-
- {{ recipeDetailForm.getRawValue().name }} -
-
|
-
- {{ recipeDetailForm.getRawValue().otherName }} -
-
-
-
-

Last Modify

-

- {{ - recipeDetailForm.getRawValue().lastModified - | date : "dd/MM/yyyy HH:mm:ss" - }} -

-
-
-
- -
-
-
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- -
-
-
-

- -

-
-
-

- -

-
-
-

- -

-
-
-

- -

-
-
-
-

- -

-
-
-
-

- -

-
-
-

- -

-
-
-

- -

-
-
-

- -

-
-
-
- - -
-
- -
- -
-
- - - -
+
+
+
+
+
+
+ {{ recipeDetailForm.getRawValue().name }} +
+
|
+
+ {{ recipeDetailForm.getRawValue().otherName }} +
+
+
+
+

Last Modify

+

+ {{ + recipeDetailForm.getRawValue().lastModified + | date : "dd/MM/yyyy HH:mm:ss" + }} +

+
+
+
+ +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+
+
+

+ +

+
+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+
+

+ +

+
+
+
+ + + + + +
+
+ +
+ +
+
+ + + +
diff --git a/client/src/app/features/recipes/recipe-details/recipe-details.component.ts b/client/src/app/features/recipes/recipe-details/recipe-details.component.ts index 08141e5..6dd519e 100644 --- a/client/src/app/features/recipes/recipe-details/recipe-details.component.ts +++ b/client/src/app/features/recipes/recipe-details/recipe-details.component.ts @@ -1,158 +1,203 @@ -import { CommonModule, DatePipe } from '@angular/common'; -import { Component, EventEmitter, OnInit } from '@angular/core'; -import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; -import { Observable, first } from 'rxjs'; -import { RecipeService } from 'src/app/core/services/recipe.service'; -import { ConfirmModal } from 'src/app/shared/modal/confirm/confirm-modal.component'; -import { animate, style, transition, trigger } from '@angular/animations'; -import { RecipeListComponent } from './recipe-list/recipe-list.component'; -import { - RecipeDetail, - RecipeDetailMat, -} from 'src/app/core/models/recipe.model'; -import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord'; -import { isEqual } from 'lodash'; - -@Component({ - selector: 'app-recipe-details', - templateUrl: './recipe-details.component.html', - standalone: true, - imports: [ - CommonModule, - RouterLink, - ReactiveFormsModule, - ConfirmModal, - DatePipe, - RecipeListComponent, - ], - animations: [ - trigger('inOutAnimation', [ - transition(':enter', [ - style({ opacity: 0 }), - animate('1s ease-out', style({ opacity: 1 })), - ]), - ]), - ], -}) -export class RecipeDetailsComponent implements OnInit { - title: string = 'Recipe Detail'; - - recipeDetail$!: Observable; - - isLoaded: boolean = false; - isMatLoaded: boolean = false; - - actionRecord: ActionRecord = - new ActionRecord(); - - recipeOriginalDetail!: typeof this.recipeDetailForm.value; - - constructor( - private _formBuilder: FormBuilder, - private _route: ActivatedRoute, - private _router: Router, - private _recipeService: RecipeService - ) {} - - productCode!: string; - - recipeDetailForm = this._formBuilder.group({ - productCode: '', - name: '', - otherName: '', - description: '', - otherDescription: '', - lastModified: new Date(), - price: 0, - isUse: false, - isShow: false, - disable: false, - }); - - ngOnInit() { - this.productCode = this._route.snapshot.params['productCode']; - - this.recipeDetail$ = this._recipeService - .getRecipeDetail(this.productCode) - .pipe(first()); - this.recipeDetail$.subscribe((detail) => { - this.recipeDetailForm.patchValue(detail); - this.isLoaded = true; - this.recipeOriginalDetail = { ...this.recipeDetailForm.getRawValue() }; - }); - - this.recipeDetailForm.valueChanges.subscribe(this.onRecipeDetailFormChange); - - // snap recipe detail form value - - this.actionRecord.registerOnAddAction((currAction, allAction) => { - if (currAction.type === 'recipeListData') { - switch (currAction.action) { - case 'add': - break; - case 'delete': - break; - } - } - console.log('Action Record', allAction); - }); - } - - showConfirmSaveModal: EventEmitter = new EventEmitter(); - showConfirmCloseModal: EventEmitter = new EventEmitter(); - - confirmSave = { - title: 'The changes detected!', - message: 'Do you want to save changes?', - confirmCallBack: () => { - console.log('confirm save'); - // TODO: update value in targeted recipe - // this._recipeService.editChanges( - // this._recipeService.getCurrentCountry(), - // this._recipeService.getCurrentFile(), - // { - // ...this.recipeDetail, - // } - // ); - console.log('Sending changes'); - this._router.navigate(['/recipes']); - }, - }; - - confirmClose = { - title: 'The changes will be lost!', - message: 'Do you want to close without saving?', - confirmCallBack: () => { - console.log('confirm close'); - this._router.navigate(['/recipes']); - }, - }; - - onPressConfirmSave() { - if (this.isValueChanged) { - this.showConfirmSaveModal.emit(true); - } else { - this._router.navigate(['/recipes']); - } - } - - onPressConfirmClose() { - if (this.isValueChanged) { - this.showConfirmCloseModal.emit(true); - } else { - this._router.navigate(['/recipes']); - } - } - - isValueChanged: boolean = false; - - onRecipeDetailFormChange(recipeDetail: typeof this.recipeDetailForm.value) { - console.log('Recipe Detail Form Changed', recipeDetail); - } - - onRecipeListFormChange(isValueChanged: boolean) { - console.log('Recipe List Form Changed', isValueChanged); - this.isValueChanged ||= isValueChanged; - } -} +import { CommonModule, DatePipe } from '@angular/common'; +import { Component, EventEmitter, OnInit } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Observable, first } from 'rxjs'; +import { RecipeService } from 'src/app/core/services/recipe.service'; +import { ConfirmModal } from 'src/app/shared/modal/confirm/confirm-modal.component'; +import { animate, style, transition, trigger } from '@angular/animations'; +import { RecipeListComponent } from './recipe-list/recipe-list.component'; +import { + RecipeDetail, + RecipeDetailMat, +} from 'src/app/core/models/recipe.model'; +import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord'; +import { isEqual } from 'lodash'; +import { UserService } from 'src/app/core/services/user.service'; + +@Component({ + selector: 'app-recipe-details', + templateUrl: './recipe-details.component.html', + standalone: true, + imports: [ + CommonModule, + RouterLink, + ReactiveFormsModule, + ConfirmModal, + DatePipe, + RecipeListComponent, + ], + animations: [ + trigger('inOutAnimation', [ + transition(':enter', [ + style({ opacity: 0 }), + animate('1s ease-out', style({ opacity: 1 })), + ]), + ]), + ], +}) +export class RecipeDetailsComponent implements OnInit { + title: string = 'Recipe Detail'; + + recipeDetail$!: Observable; + + isLoaded: boolean = false; + isMatLoaded: boolean = false; + + actionRecord: ActionRecord = + new ActionRecord(); + + recipeOriginalDetail!: typeof this.recipeDetailForm.value; + + commit_msg :string = ""; + + constructor( + private _formBuilder: FormBuilder, + private _route: ActivatedRoute, + private _router: Router, + private _recipeService: RecipeService, + private _userService: UserService + ) {} + + productCode!: string; + + recipeDetailForm = this._formBuilder.group({ + productCode: '', + name: '', + otherName: '', + Description: '', + otherDescription: '', + lastModified: new Date(), + price: 0, + isUse: false, + isShow: false, + disable: false, + }); + + repl = [] + + ngOnInit() { + this.productCode = this._route.snapshot.params['productCode']; + + this.recipeDetail$ = this._recipeService + .getRecipeDetail(this.productCode) + .pipe(first()); + this.recipeDetail$.subscribe((detail) => { + + console.log('Recipe Detail', detail); + + this.recipeDetailForm.patchValue(detail); + this.isLoaded = true; + this.recipeOriginalDetail = { ...this.recipeDetailForm.getRawValue() }; + }); + + this.recipeDetailForm.valueChanges.subscribe(this.onRecipeDetailFormChange); + + // snap recipe detail form value + + this.actionRecord.registerOnAddAction((currAction, allAction) => { + if (currAction.type === 'recipeListData') { + switch (currAction.action) { + case 'add': + break; + case 'delete': + break; + } + } + console.log('Action Record', allAction); + }); + } + + showConfirmSaveModal: EventEmitter = new EventEmitter(); + showConfirmCloseModal: EventEmitter = new EventEmitter(); + + confirmSave = { + title: 'The changes detected!', + message: 'Do you want to save changes?', + confirmCallBack: () => { + console.log('confirm save'); + + // get username + let username:string = "" + this._userService.getCurrentUser().subscribe((user) => { + username = user.user.name; + + + + let to_send = { + edit_by: username, + commit_msg: this.commit_msg, + productCode: this.productCode, + name: this.recipeDetailForm.getRawValue().name, + otherName: this.recipeDetailForm.getRawValue().otherName, + Description: this.recipeDetailForm.getRawValue().Description, + otherDescription: this.recipeDetailForm.getRawValue().otherDescription, + LastChange: this.recipeDetailForm.getRawValue().lastModified, + price: this.recipeDetailForm.getRawValue().price, + // isUse: this, + // isShow: null, + // disable: null, + recipes: [ + ...this.repl + ], + } + + + // TODO: update value in targeted recipe + this._recipeService.editChanges( + this._recipeService.getCurrentCountry(), + this._recipeService.getCurrentFile(), + { + ...to_send, + } + ); + console.log('Sending changes'); + this._router.navigate(['/recipes']); + }) + + + + }, + }; + + onKeyUpCommitMsg(e: any){ + this.commit_msg = e.target.value; + } + + confirmClose = { + title: 'The changes will be lost!', + message: 'Do you want to close without saving?', + confirmCallBack: () => { + console.log('confirm close'); + this._router.navigate(['/recipes']); + }, + }; + + onPressConfirmSave() { + if (this.isValueChanged) { + this.showConfirmSaveModal.emit(true); + } else { + this._router.navigate(['/recipes']); + } + } + + onPressConfirmClose() { + if (this.isValueChanged) { + this.showConfirmCloseModal.emit(true); + } else { + this._router.navigate(['/recipes']); + } + } + + isValueChanged: boolean = false; + + onRecipeDetailFormChange(recipeDetail: typeof this.recipeDetailForm.value) { + console.log('Recipe Detail Form Changed', recipeDetail); + } + + onRecipeListFormChange(repl: unknown[]) { + console.log('Recipe List Form Changed', repl); + this.repl = repl as never[]; + this.isValueChanged ||= repl != undefined ? true : false; + } +} diff --git a/client/src/app/features/recipes/recipe-details/recipe-list/recipe-list.component.html b/client/src/app/features/recipes/recipe-details/recipe-list/recipe-list.component.html index 9b37428..f879928 100644 --- a/client/src/app/features/recipes/recipe-details/recipe-list/recipe-list.component.html +++ b/client/src/app/features/recipes/recipe-details/recipe-list/recipe-list.component.html @@ -1,57 +1,57 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Is UseMaterial IDMaterial NameMixOrderStir TimePowder GramPowder TimeSyrup GramSyrup TimeWater ColdWater Yield
- - - - - - - - - - - - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Is UseMaterial IDMaterial NameMixOrderStir TimePowder GramPowder TimeSyrup GramSyrup TimeWater ColdWater Yield
+ + + + + + + + + + + + + + + + + + + + + +
diff --git a/client/src/app/features/recipes/recipe-details/recipe-list/recipe-list.component.ts b/client/src/app/features/recipes/recipe-details/recipe-list/recipe-list.component.ts index 8295062..0c0cbea 100644 --- a/client/src/app/features/recipes/recipe-details/recipe-list/recipe-list.component.ts +++ b/client/src/app/features/recipes/recipe-details/recipe-list/recipe-list.component.ts @@ -1,90 +1,102 @@ -import { NgFor, NgIf } from '@angular/common'; -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { - FormArray, - FormBuilder, - FormControl, - FormGroup, - ReactiveFormsModule, -} from '@angular/forms'; -import { isEqual, sortBy } from 'lodash'; -import { first } from 'rxjs'; -import { - RecipeDetail, - RecipeDetailMat, -} from 'src/app/core/models/recipe.model'; -import { RecipeService } from 'src/app/core/services/recipe.service'; -import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord'; - -@Component({ - selector: 'app-recipe-list', - templateUrl: './recipe-list.component.html', - standalone: true, - imports: [NgIf, NgFor, ReactiveFormsModule], -}) -export class RecipeListComponent implements OnInit { - @Input({ required: true }) productCode!: string; - @Output() recipeListFormChange = new EventEmitter(); - - isMatLoaded: boolean = false; - - constructor( - private _recipeService: RecipeService, - private _formBuilder: FormBuilder - ) {} - - recipeListForm = this._formBuilder.group( - { - recipeListData: this._formBuilder.array([]), - }, - { updateOn: 'blur' } - ); - - private _recipeListOriginalArray!: RecipeDetailMat[]; - - ngOnInit(): void { - this._recipeService - .getRecipeDetailMat(this.productCode) - .pipe(first()) - .subscribe(({ result }) => { - this._recipeListOriginalArray = result; - result.forEach((recipeDetailMat: RecipeDetailMat) => { - this.recipeListData.push( - this._formBuilder.group({ - isUse: recipeDetailMat.isUse, - materialID: recipeDetailMat.materialID, - name: [{ value: recipeDetailMat.name, disabled: true }], - mixOrder: recipeDetailMat.mixOrder, - stirTime: recipeDetailMat.stirTime, - powderGram: recipeDetailMat.powderGram, - powderTime: recipeDetailMat.powderTime, - syrupGram: recipeDetailMat.syrupGram, - syrupTime: recipeDetailMat.syrupTime, - waterCold: recipeDetailMat.waterCold, - waterYield: recipeDetailMat.waterYield, - }) - ); - }); - this.isMatLoaded = true; - }); - - this.recipeListForm.valueChanges.subscribe((value) => { - console.log(value.recipeListData); - console.log(this._recipeListOriginalArray); - if ( - !isEqual( - sortBy(value, 'materialID'), - sortBy(this._recipeListOriginalArray, 'materialID') - ) - ) { - this.recipeListFormChange.emit(true); - } else { - this.recipeListFormChange.emit(false); - } - }); - } - - get recipeListData(): FormArray { - return this.recipeListForm.get('recipeListData') as FormArray; - } -} +import { NgFor, NgIf } from '@angular/common'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { + FormArray, + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, +} from '@angular/forms'; +import { forEach, isEqual, sortBy } from 'lodash'; +import { first } from 'rxjs'; +import { + RecipeDetail, + RecipeDetailMat, +} from 'src/app/core/models/recipe.model'; +import { RecipeService } from 'src/app/core/services/recipe.service'; +import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord'; + +@Component({ + selector: 'app-recipe-list', + templateUrl: './recipe-list.component.html', + standalone: true, + imports: [NgIf, NgFor, ReactiveFormsModule], +}) +export class RecipeListComponent implements OnInit { + @Input({ required: true }) productCode!: string; + @Output() recipeListFormChange = new EventEmitter(); + + isMatLoaded: boolean = false; + + constructor( + private _recipeService: RecipeService, + private _formBuilder: FormBuilder + ) {} + + recipeListForm = this._formBuilder.group( + { + recipeListData: this._formBuilder.array([]), + }, + { updateOn: 'blur' } + ); + + private _recipeListOriginalArray!: RecipeDetailMat[]; + + ngOnInit(): void { + this._recipeService + .getRecipeDetailMat(this.productCode) + .pipe(first()) + .subscribe(({ result }) => { + this._recipeListOriginalArray = result; + result.forEach((recipeDetailMat: RecipeDetailMat) => { + this.recipeListData.push( + this._formBuilder.group({ + isUse: recipeDetailMat.isUse, + materialPathId: recipeDetailMat.materialPathId, + name: [{ value: recipeDetailMat.name, disabled: true }], + mixOrder: recipeDetailMat.mixOrder, + stirTime: recipeDetailMat.stirTime, + powderGram: recipeDetailMat.powderGram, + powderTime: recipeDetailMat.powderTime, + syrupGram: recipeDetailMat.syrupGram, + syrupTime: recipeDetailMat.syrupTime, + waterCold: recipeDetailMat.waterCold, + waterYield: recipeDetailMat.waterYield, + }) + ); + }); + this.isMatLoaded = true; + }); + + this.recipeListForm.valueChanges.subscribe((value) => { + console.log(value.recipeListData); + console.log(this._recipeListOriginalArray); + if ( + !isEqual( + sortBy(value, 'materialID'), + sortBy(this._recipeListOriginalArray, 'materialID') + ) + ) { + + let emitted_res: any[] = [] + + // force type change. temporary solution + forEach(value.recipeListData!, (recipeDetailMat: any) => { + recipeDetailMat.materialPathId = parseInt(recipeDetailMat.materialPathId!) + emitted_res.push(recipeDetailMat) + }) + + this.recipeListFormChange.emit(emitted_res as unknown[]); + } else { + this.recipeListFormChange.emit([]); + } + + + + }); + } + + get recipeListData(): FormArray { + return this.recipeListForm.get('recipeListData') as FormArray; + } +} diff --git a/client/src/app/features/recipes/recipes.component.html b/client/src/app/features/recipes/recipes.component.html index 0fba628..679e57f 100644 --- a/client/src/app/features/recipes/recipes.component.html +++ b/client/src/app/features/recipes/recipes.component.html @@ -1,288 +1,312 @@ -
- - - - - - - - - - - - - - - - - - - - -
-
-
-
- Recipe Version {{ recipesDashboard.configNumber }} | - {{ recipesDashboard.filename }} -
-
- - - - - - -
-
- Last Updated: - {{ - recipesDashboard.configNumber | date : "dd-MMM-yyyy hh:mm:ss" - }} -
-
-
-
-
-
- - - -

{{ item.name }}

- {{ item.id }} -
-
- -
- -
- - -
-
-
-
-
-
- - -
- {{ header }} - - -
-
- - - {{ recipe.productCode }} - - {{ recipe.name }} - {{ recipe.otherName }}{{ recipe.description }} - {{ recipe.lastUpdated | date : "dd-MMM-yyyy hh:mm:ss" }} - - - - - -
- -
-
- -
-
-
- - -
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ Recipe Version {{ recipesDashboard.configNumber }} | + {{ recipesDashboard.filename }} +
+
+ + + + + + +
+ + + + + + + + + + + +
+ Last Updated: + {{ + recipesDashboard.configNumber | date : "dd-MMM-yyyy hh:mm:ss" + }} +
+
+
+
+
+
+ + + +

{{ item.name }}

+ {{ item.id }} +
+
+ +
+ +
+ + + +
+
+
+
+
+
+ + +
+ {{ header }} + + +
+
+ + + {{ recipe.productCode }} + + {{ recipe.name }} + {{ recipe.otherName }}{{ recipe.description }} + {{ recipe.lastUpdated | date : "dd-MMM-yyyy hh:mm:ss" }} + + + + + +
+ +
+
+ +
+
+
+
diff --git a/client/src/app/features/recipes/recipes.component.ts b/client/src/app/features/recipes/recipes.component.ts index 04153c5..9160701 100644 --- a/client/src/app/features/recipes/recipes.component.ts +++ b/client/src/app/features/recipes/recipes.component.ts @@ -1,335 +1,356 @@ -import { - Component, - ElementRef, - OnDestroy, - OnInit, - ViewChild, -} from '@angular/core'; -import { CommonModule, DatePipe } from '@angular/common'; -import { - Recipe, - Recipe01, - RecipeOverview, - RecipesDashboard, -} from 'src/app/core/models/recipe.model'; -import { RecipeService } from 'src/app/core/services/recipe.service'; -import { environment } from 'src/environments/environment'; -import { RecipeModalComponent } from 'src/app/shared/modal/recipe-details/recipe-modal.component'; -import { - BehaviorSubject, - Observable, - Subscription, - finalize, - map, - tap, -} from 'rxjs'; -import * as lodash from 'lodash'; -import { RouterLink } from '@angular/router'; -import { NgSelectModule } from '@ng-select/ng-select'; -import { FormsModule } from '@angular/forms'; -import { MaterialService } from 'src/app/core/services/material.service'; - -@Component({ - selector: 'app-recipes', - standalone: true, - imports: [ - CommonModule, - RouterLink, - DatePipe, - RecipeModalComponent, - NgSelectModule, - FormsModule, - ], - templateUrl: './recipes.component.html', -}) -export class RecipesComponent implements OnInit, OnDestroy { - recipesDashboard$!: Observable; - recipeOverviewList!: RecipeOverview[]; - selectMaterialFilter: number[] | null = null; - materialList: { id: number; name: string | number }[] | null = null; - - tableHeads: string[] = [ - 'Product Code', - 'Name', - 'Other Name', - 'Description', - 'Last Updated', - ]; - private offset = 0; - private take = 20; - - // isLoaded: boolean = false; - isLoadMore: boolean = true; - isHasMore: boolean = true; - - private searchStr = ''; - private oldSearchStr = ''; - - tableCtx?: ElementRef; - - @ViewChild('table', { static: false }) set content(table: ElementRef) { - // expose element ref for other fn - this.tableCtx = table; - - table.nativeElement.addEventListener( - 'scroll', - () => { - if (this.isHasMore === false) { - return; - } - - const { scrollTop, scrollHeight, clientHeight } = table.nativeElement; - const isBottom = scrollTop + clientHeight >= scrollHeight - 10; - if (isBottom && !this.isLoadMore) { - this.isLoadMore = true; - this._recipeService - .getRecipeOverview({ - offset: this.offset, - take: this.take, - search: this.oldSearchStr, - filename: this._recipeService.getCurrentFile(), - country: this._recipeService.getCurrentCountry(), - materialIds: this.selectMaterialFilter || [], - }) - .subscribe(({ result, hasMore, totalCount }) => { - if (this.recipeOverviewList) { - this.recipeOverviewList = - this.recipeOverviewList.concat(result); - } else { - this.recipeOverviewList = result; - } - this.offset += 10; - this.isHasMore = hasMore; - this.isLoadMore = false; - }); - } - }, - { passive: true } - ); - } - - constructor( - private _recipeService: RecipeService, - private _materialService: MaterialService - ) {} - - ngOnInit(): void { - this.recipesDashboard$ = this._recipeService - .getRecipesDashboard({ - filename: this._recipeService.getCurrentFile(), - country: this._recipeService.getCurrentCountry(), - }) - .pipe( - finalize(() => { - this._recipeService - .getRecipeOverview({ - offset: this.offset, - take: this.take, - search: this.oldSearchStr, - filename: this._recipeService.getCurrentFile(), - country: this._recipeService.getCurrentCountry(), - materialIds: this.selectMaterialFilter || [], - }) - .subscribe(({ result, hasMore, totalCount }) => { - this.recipeOverviewList = result; - this.offset += 10; - this.isHasMore = hasMore; - this.isLoadMore = false; - }); - }) - ); - - this._materialService - .getMaterialCodes() - .pipe( - map((mat) => - mat.map((m) => ({ - id: m.materialID, - name: m.PackageDescription || m.materialID, - })) - ) - ) - .subscribe((materials) => { - this.materialList = materials; - }); - - this.initRecipeSelection(); - } - - setSearch(event: Event) { - this.searchStr = (event.target as HTMLInputElement).value; - } - - search(event: Event) { - this.offset = 0; - this.isLoadMore = true; - this.oldSearchStr = this.searchStr; - this._recipeService - .getRecipeOverview({ - offset: this.offset, - take: this.take, - search: this.oldSearchStr, - filename: this._recipeService.getCurrentFile(), - country: this._recipeService.getCurrentCountry(), - materialIds: this.selectMaterialFilter || [], - }) - .subscribe(({ result, hasMore, totalCount }) => { - this.recipeOverviewList = result; - this.offset += 10; - this.isHasMore = hasMore; - this.isLoadMore = false; - }); - } - - // Recipe Version selection - currentCountryFilter: BehaviorSubject = new BehaviorSubject( - '' - ); - currentFileFilter: BehaviorSubject = new BehaviorSubject(''); - recipeCountryFiltered: string[] = []; - recipeFileCountries: string[] = []; - - currentCountryFilterSubScription: Subscription | null = null; - currentFileFilterSubScription: Subscription | null = null; - - selectedCountry: string | null = null; - isCountrySelected: boolean = false; - - private countryInputEl?: ElementRef; - private fileInputEl?: ElementRef; - - @ViewChild('countryInput', { static: false }) set countryInput( - countryInput: ElementRef - ) { - this.countryInputEl = countryInput; - } - - @ViewChild('fileInput', { static: false }) set fileInput( - fileInput: ElementRef - ) { - this.fileInputEl = fileInput; - } - - private firstTimeOpenModal = true; - - initRecipeSelection() { - if (this._recipeService.getRecipeFileCountries().length == 0) { - this._recipeService.getRecipeCountries().subscribe((countries) => { - this.recipeCountryFiltered = countries; - }); - } - } - - setCountryFilter(event: Event) { - this.currentCountryFilter.next((event.target as HTMLInputElement).value); - } - - setFileFilter(event: Event) { - this.currentFileFilter.next((event.target as HTMLInputElement).value); - } - - getRecipeCountries() { - if (this.firstTimeOpenModal) { - this.countryInputEl!.nativeElement.blur(); - this.firstTimeOpenModal = false; - } - this.currentCountryFilterSubScription = this.currentCountryFilter.subscribe( - (c) => { - const countries = this._recipeService.getRecipeFileCountries(); - if (countries.length > 0) { - this.recipeCountryFiltered = lodash.filter(countries, (country) => - country.toLowerCase().includes(c.toLowerCase()) - ); - } - } - ); - } - - getRecipeFiles() { - this.currentFileFilterSubScription = this.currentFileFilter.subscribe( - (c) => { - if (this.selectedCountry === null) { - return; - } - - const fileNames = this._recipeService.getRecipeFileNames( - this.selectedCountry - ); - if (fileNames && fileNames.length > 0) { - this.recipeFileCountries = lodash.filter(fileNames, (file) => - file.toLowerCase().includes(c.toLowerCase()) - ); - } else { - this._recipeService - .getRecipeFiles(this.selectedCountry) - .subscribe((files) => { - this.recipeFileCountries = lodash.filter(files, (file) => - file.toLowerCase().includes(c.toLowerCase()) - ); - }); - } - console.log(this.recipeFileCountries); - } - ); - } - - countrySelected(country: string) { - this.selectedCountry = country; - this.isCountrySelected = true; - localStorage.setItem('currentRecipeCountry', country); - } - - loadRecipe(recipeFileName: string) { - // clear all recipes - this.offset = 0; - this.isHasMore = true; - this.isLoadMore = true; - this.oldSearchStr = ''; - localStorage.setItem('currentRecipeFile', recipeFileName); - - this.recipesDashboard$ = this._recipeService.getRecipesDashboard({ - filename: recipeFileName, - country: this.selectedCountry!, - }); - - this._recipeService - .getRecipeOverview({ - offset: this.offset, - take: this.take, - search: this.oldSearchStr, - filename: recipeFileName, - country: this.selectedCountry!, - materialIds: this.selectMaterialFilter || [], - }) - .subscribe(({ result, hasMore, totalCount }) => { - this.recipeOverviewList = result; - this.offset += 10; - this.isHasMore = hasMore; - this.isLoadMore = false; - }); - } - - // end of Recipe Version selection - - openJsonTab() { - window.open( - environment.api + - `/recipes/${this._recipeService.getCurrentCountry()}/${this._recipeService.getCurrentFile()}/json`, - '_blank' - ); - } - - scrollToTop() { - const table = this.tableCtx!.nativeElement; - table.scrollTo({ top: 0, behavior: 'smooth' }); - } - - ngOnDestroy(): void { - if (this.currentCountryFilterSubScription) { - this.currentCountryFilterSubScription.unsubscribe(); - } - if (this.currentFileFilterSubScription) { - this.currentFileFilterSubScription.unsubscribe(); - } - } -} +import { + Component, + ElementRef, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; +import { CommonModule, DatePipe } from '@angular/common'; +import { + Recipe, + Recipe01, + RecipeOverview, + RecipesDashboard, +} from 'src/app/core/models/recipe.model'; +import { RecipeService } from 'src/app/core/services/recipe.service'; +import { environment } from 'src/environments/environment'; +import { RecipeModalComponent } from 'src/app/shared/modal/recipe-details/recipe-modal.component'; +import { + BehaviorSubject, + Observable, + Subscription, + finalize, + map, + tap, +} from 'rxjs'; +import * as lodash from 'lodash'; +import { RouterLink } from '@angular/router'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { FormsModule } from '@angular/forms'; +import { MaterialService } from 'src/app/core/services/material.service'; + +@Component({ + selector: 'app-recipes', + standalone: true, + imports: [ + CommonModule, + RouterLink, + DatePipe, + RecipeModalComponent, + NgSelectModule, + FormsModule, + ], + templateUrl: './recipes.component.html', +}) +export class RecipesComponent implements OnInit, OnDestroy { + recipesDashboard$!: Observable; + recipeOverviewList!: RecipeOverview[]; + selectMaterialFilter: number[] | null = null; + materialList: { id: number; name: string | number }[] | null = null; + + tableHeads: string[] = [ + 'Product Code', + 'Name', + 'Other Name', + 'Description', + 'Last Updated', + ]; + private offset = 0; + private take = 20; + + // isLoaded: boolean = false; + isLoadMore: boolean = true; + isHasMore: boolean = true; + + private searchStr = ''; + private oldSearchStr = ''; + + savedTmpfiles: string[] = []; + + tableCtx?: ElementRef; + + @ViewChild('table', { static: false }) set content(table: ElementRef) { + // expose element ref for other fn + this.tableCtx = table; + + table.nativeElement.addEventListener( + 'scroll', + () => { + if (this.isHasMore === false) { + return; + } + + const { scrollTop, scrollHeight, clientHeight } = table.nativeElement; + const isBottom = scrollTop + clientHeight >= scrollHeight - 10; + if (isBottom && !this.isLoadMore) { + this.isLoadMore = true; + this._recipeService + .getRecipeOverview({ + offset: this.offset, + take: this.take, + search: this.oldSearchStr, + filename: this._recipeService.getCurrentFile(), + country: this._recipeService.getCurrentCountry(), + materialIds: this.selectMaterialFilter || [], + }) + .subscribe(({ result, hasMore, totalCount }) => { + if (this.recipeOverviewList) { + this.recipeOverviewList = + this.recipeOverviewList.concat(result); + } else { + this.recipeOverviewList = result; + } + this.offset += 10; + this.isHasMore = hasMore; + this.isLoadMore = false; + }); + } + }, + { passive: true } + ); + } + + constructor( + private _recipeService: RecipeService, + private _materialService: MaterialService + ) {} + + ngOnInit(): void { + this.recipesDashboard$ = this._recipeService + .getRecipesDashboard({ + filename: this._recipeService.getCurrentFile(), + country: this._recipeService.getCurrentCountry(), + }) + .pipe( + finalize(() => { + this._recipeService + .getRecipeOverview({ + offset: this.offset, + take: this.take, + search: this.oldSearchStr, + filename: this._recipeService.getCurrentFile(), + country: this._recipeService.getCurrentCountry(), + materialIds: this.selectMaterialFilter || [], + }) + .subscribe(({ result, hasMore, totalCount }) => { + this.recipeOverviewList = result; + this.offset += 10; + this.isHasMore = hasMore; + this.isLoadMore = false; + }); + }) + ); + + this._recipeService.getSavedTmp( + this._recipeService.getCurrentCountry(), + this._recipeService.getCurrentFile() + ).subscribe({ + next: (files:any) => { + console.log("Obtain saves: ", typeof files); + if(files != undefined && typeof files === 'object'){ + // console.log("Obtain saves object: ", files.files[0], typeof files); + this.savedTmpfiles = files.files; + } + + }, + }) + + this._materialService + .getMaterialCodes() + .pipe( + map((mat) => + mat.map((m) => ({ + id: m.materialID, + name: m.PackageDescription || m.materialID, + })) + ) + ) + .subscribe((materials) => { + this.materialList = materials; + }); + + this.initRecipeSelection(); + } + + setSearch(event: Event) { + this.searchStr = (event.target as HTMLInputElement).value; + } + + search(event: Event) { + this.offset = 0; + this.isLoadMore = true; + this.oldSearchStr = this.searchStr; + this._recipeService + .getRecipeOverview({ + offset: this.offset, + take: this.take, + search: this.oldSearchStr, + filename: this._recipeService.getCurrentFile(), + country: this._recipeService.getCurrentCountry(), + materialIds: this.selectMaterialFilter || [], + }) + .subscribe(({ result, hasMore, totalCount }) => { + this.recipeOverviewList = result; + this.offset += 10; + this.isHasMore = hasMore; + this.isLoadMore = false; + }); + } + + // Recipe Version selection + currentCountryFilter: BehaviorSubject = new BehaviorSubject( + '' + ); + currentFileFilter: BehaviorSubject = new BehaviorSubject(''); + recipeCountryFiltered: string[] = []; + recipeFileCountries: string[] = []; + + currentCountryFilterSubScription: Subscription | null = null; + currentFileFilterSubScription: Subscription | null = null; + + selectedCountry: string | null = null; + isCountrySelected: boolean = false; + + private countryInputEl?: ElementRef; + private fileInputEl?: ElementRef; + + @ViewChild('countryInput', { static: false }) set countryInput( + countryInput: ElementRef + ) { + this.countryInputEl = countryInput; + } + + @ViewChild('fileInput', { static: false }) set fileInput( + fileInput: ElementRef + ) { + this.fileInputEl = fileInput; + } + + private firstTimeOpenModal = true; + + initRecipeSelection() { + if (this._recipeService.getRecipeFileCountries().length == 0) { + this._recipeService.getRecipeCountries().subscribe((countries) => { + this.recipeCountryFiltered = countries; + }); + } + } + + setCountryFilter(event: Event) { + this.currentCountryFilter.next((event.target as HTMLInputElement).value); + } + + setFileFilter(event: Event) { + this.currentFileFilter.next((event.target as HTMLInputElement).value); + } + + getRecipeCountries() { + if (this.firstTimeOpenModal) { + this.countryInputEl!.nativeElement.blur(); + this.firstTimeOpenModal = false; + } + this.currentCountryFilterSubScription = this.currentCountryFilter.subscribe( + (c) => { + const countries = this._recipeService.getRecipeFileCountries(); + if (countries.length > 0) { + this.recipeCountryFiltered = lodash.filter(countries, (country) => + country.toLowerCase().includes(c.toLowerCase()) + ); + } + } + ); + } + + getRecipeFiles() { + this.currentFileFilterSubScription = this.currentFileFilter.subscribe( + (c) => { + if (this.selectedCountry === null) { + return; + } + + const fileNames = this._recipeService.getRecipeFileNames( + this.selectedCountry + ); + if (fileNames && fileNames.length > 0) { + this.recipeFileCountries = lodash.filter(fileNames, (file) => + file.toLowerCase().includes(c.toLowerCase()) + ); + } else { + this._recipeService + .getRecipeFiles(this.selectedCountry) + .subscribe((files) => { + this.recipeFileCountries = lodash.filter(files, (file) => + file.toLowerCase().includes(c.toLowerCase()) + ); + }); + } + console.log(this.recipeFileCountries); + } + ); + } + + countrySelected(country: string) { + this.selectedCountry = country; + this.isCountrySelected = true; + localStorage.setItem('currentRecipeCountry', country); + } + + loadRecipe(recipeFileName: string) { + // clear all recipes + this.offset = 0; + this.isHasMore = true; + this.isLoadMore = true; + this.oldSearchStr = ''; + localStorage.setItem('currentRecipeFile', recipeFileName); + + this.recipesDashboard$ = this._recipeService.getRecipesDashboard({ + filename: recipeFileName, + country: this.selectedCountry!, + }); + + this._recipeService + .getRecipeOverview({ + offset: this.offset, + take: this.take, + search: this.oldSearchStr, + filename: recipeFileName, + country: this.selectedCountry!, + materialIds: this.selectMaterialFilter || [], + }) + .subscribe(({ result, hasMore, totalCount }) => { + this.recipeOverviewList = result; + this.offset += 10; + this.isHasMore = hasMore; + this.isLoadMore = false; + }); + } + + // end of Recipe Version selection + + openJsonTab() { + window.open( + environment.api + + `/recipes/${this._recipeService.getCurrentCountry()}/${this._recipeService.getCurrentFile()}/json`, + '_blank' + ); + } + + // get tmp files + openTmpFilesList(){ + // TODO: get tmp files to display or + } + + scrollToTop() { + const table = this.tableCtx!.nativeElement; + table.scrollTo({ top: 0, behavior: 'smooth' }); + } + + ngOnDestroy(): void { + if (this.currentCountryFilterSubScription) { + this.currentCountryFilterSubScription.unsubscribe(); + } + if (this.currentFileFilterSubScription) { + this.currentFileFilterSubScription.unsubscribe(); + } + } +} diff --git a/server/data/commit.go b/server/data/commit.go new file mode 100644 index 0000000..2a0c227 --- /dev/null +++ b/server/data/commit.go @@ -0,0 +1,82 @@ +package data + +import ( + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" + "go.uber.org/zap" + + "crypto/rand" + "encoding/hex" +) + +var schema = ` +CREATE TABLE IF NOT EXISTS commit_log ( + id VARCHAR(255) PRIMARY KEY, + msg VARCHAR(255), + created_at DATETIME, + editor VARCHAR(255), + change_file VARCHAR(255) +); +` + +type CommitLog struct { + Id string `db:"id"` + Msg string `db:"msg"` + Created_at string `db:"created_at"` + Editor string `db:"editor"` + Change_file string `db:"change_file"` +} + +func HashCommit(n int) (string, error) { + // _, err := h.Write([]byte(target)) + // if err != nil { + // Log.Debug("Error when hashing commit", zap.Error(err)) + // return "", err + // } + // return string(h.Sum(nil)), nil + byt := make([]byte, n) + _, err := rand.Read(byt) + if err != nil { + Log.Debug("Error when hashing commit", zap.Error(err)) + return "", err + } + return hex.EncodeToString(byt), nil +} + +func Insert(c *CommitLog) error { + commit_db, err := sqlx.Connect("sqlite3", "./data/database.db") + if err != nil { + Log.Fatal("Error when connecting to database", zap.Error(err)) + } + // init table in db + commit_db.MustExec(schema) + + _, err = commit_db.NamedExec("INSERT INTO commit_log (id, msg, created_at, editor, change_file) VALUES (:id, :msg, :created_at, :editor, :change_file)", c) + if err != nil { + Log.Error("Error when insert commit log", zap.Error(err)) + return err + } + + return nil +} + +// func GetCommitLogOfFilename(filename string) ([]CommitLog, error) { +//} +// cut .json, split then get pos 2, check `change_file` startwith "filename" then return all quries + +func GetCommitLogs() ([]CommitLog, error) { + commit_db, err := sqlx.Connect("sqlite3", "./data/database.db") + if err != nil { + Log.Fatal("Error when connecting to database", zap.Error(err)) + } + + var commits []CommitLog + err = commit_db.Get(&commits, "SELECT * FROM commit_log", nil) + + if err != nil { + Log.Error("Error when get commit log", zap.Error(err)) + return nil, err + } + + return commits, nil +} diff --git a/server/helpers/misc.go b/server/helpers/misc.go index bc4cf2e..4d033e1 100644 --- a/server/helpers/misc.go +++ b/server/helpers/misc.go @@ -1,58 +1,180 @@ -package helpers - -import "fmt" - -// DynamicCompare compares two values dynamically and returns true if they are equal. -// -// Parameters: -// -// - s: The first value to compare. -// -// - u: The second value to compare. -// -// Returns: -// -// - bool: True if the values are equal, false otherwise. -// -// - error: An error if the values cannot be compared. -func DynamicCompare(s interface{}, u interface{}) (bool, error) { - switch t := s.(type) { - case bool: - u, ok := u.(bool) - if !ok { - return false, fmt.Errorf("[bool] cannot compare %T and %T, %v and %v", t, u, s, u) - } - return t == u, nil - case string: - u, ok := u.(string) - if !ok { - return false, fmt.Errorf("[string] cannot compare %T and %T, %v and %v", t, u, s, u) - } - return t == u, nil - case int: - u, ok := u.(int) - if !ok { - return false, fmt.Errorf("[int] cannot compare %T and %T, %v and %v", t, u, s, u) - } - return t == u, nil - case float64: - u, ok := u.(float64) - if !ok { - return false, fmt.Errorf("[float64] cannot compare %T and %T, %v and %v", t, u, s, u) - } - return t == u, nil - case nil: - return t == u, nil - case []interface{}: - for i := range t { - if ok, err := DynamicCompare(t[i], u); err != nil { - return false, err - } else if !ok { - return false, nil - } - } - default: - return false, fmt.Errorf("[unknown] not in case. Cannot compare %T and %T, %v and %v", t, u, s, u) - } - return false, fmt.Errorf("[unknown] unexpected error") -} +package helpers + +import ( + "fmt" + "os" + "recipe-manager/services/logger" + "strconv" + "strings" + + "go.uber.org/zap" +) + +var ( + Log = logger.GetInstance() +) + +// DynamicCompare compares two values dynamically and returns true if they are equal. +// +// Parameters: +// +// - s: The first value to compare. +// +// - u: The second value to compare. +// +// Returns: +// +// - bool: True if the values are equal, false otherwise. +// +// - error: An error if the values cannot be compared. +func DynamicCompare(s interface{}, u interface{}) (bool, error) { + switch t := s.(type) { + case bool: + u, ok := u.(bool) + if !ok { + return false, fmt.Errorf("[bool] cannot compare %T and %T, %v and %v", t, u, s, u) + } + return t == u, nil + case string: + u, ok := u.(string) + if !ok { + return false, fmt.Errorf("[string] cannot compare %T and %T, %v and %v", t, u, s, u) + } + return t == u, nil + case int: + u, ok := u.(int) + if !ok { + return false, fmt.Errorf("[int] cannot compare %T and %T, %v and %v", t, u, s, u) + } + return t == u, nil + case float64: + u, ok := u.(float64) + // Log.Debug("[helpers] DynamicCompare", zap.Any("u", u), zap.Any("ok", ok), zap.Any("test_compare*(t==u)", t == u)) + + if t == u { + return t == u, nil + } + + if !ok { + return false, fmt.Errorf("[float64] cannot compare %T and %T, %v and %v", t, u, s, u) + } + return t == u, nil + case nil: + return t == u, nil + case []interface{}: + for i := range t { + if ok, err := DynamicCompare(t[i], u); err != nil { + return false, err + } else if !ok { + return false, nil + } + } + break + case map[string]interface{}: + for _, v := range t { + if ok, err := DynamicCompare(v, u); err != nil { + return false, err + } else if !ok { + return false, nil + } + } + break + default: + return false, fmt.Errorf("[unknown] not in case. Cannot compare %T and %T, %v and %v", t, u, s, u) + } + + if u == nil { + return false, fmt.Errorf("[empty] the compared value is nil") + } + + return false, fmt.Errorf("[unknown] unexpected error. [old] %v and [new] %v", s, u) +} + +func GetTempFile(filename string, user string, suffix int) string { + + // Check if the temp file exist + _, err := os.Stat(filename) + + // Log.Debug("[helpers] GetTempFile", zap.Any("filename", filename), zap.Any("suffix", suffix), zap.Any("err", err)) + + // file not exists + if os.IsNotExist(err) { + // Create temp file + if suffix == 0 { + Log.Debug("[helpers] Suffix 0 GetTempFile", zap.Any("filename", filename)) + return strings.Replace(filename, ".json", "_"+user+".tmp"+strconv.Itoa(suffix), 1) + } + + // change extension from json to tmp + filename = strings.Replace(filename, ".json", "_"+user+".tmp"+strconv.Itoa(suffix), 1) + Log.Debug("[helpers] GetTempFile", zap.Any("filename", filename)) + return filename + } else { + + if strings.Contains(filename, ".tmp") { + return GetTempFile(strings.Replace(filename, "_"+user+".tmp"+strconv.Itoa(suffix-1), "_"+user+".tmp"+strconv.Itoa(suffix), 1), user, suffix+1) + } + + // recursive call + return GetTempFile(strings.Replace(filename, ".json", "_"+user+".tmp"+strconv.Itoa(suffix), 1), user, suffix+1) + } +} + +// func PackTempToRealFile(data *data.Data, countryID string, filename string) { + +// // list file that end with .tmp* +// files, err := filepath.Glob(filename + ".tmp*") + +// // for all files, read and get configNumber + +// if err != nil { +// Log.Error("[helpers] PackTempToRealFile", zap.Error(err)) +// } + +// // get configNumber from actual filename.json + +// // +// base_recipe := data.GetRecipe(countryID, filename) + +// // read file and apply tmp file from 0 to tmpX. +// // - if there is more than 1 user that access this file at the same time, +// // pack in order, and if conflict, stop + +// if len(files) == 0 { +// return +// } + +// // TODO: must check the changes + +// for _, file := range files { +// var tmpdata models.Recipe +// tmpfile, err := os.Open(file) +// if err != nil { +// return +// } +// _ = json.NewDecoder(tmpfile).Decode(&tmpdata) +// // apply change + +// // = tmpdata.Recipe01 + +// for key, val := range tmpdata.Recipe01 { + +// test_bol, err := DynamicCompare(base_recipe.Recipe01[key], val) + +// if err != nil { +// Log.Error("[helpers] PackTempToRealFile", zap.Error(err)) +// } + +// if !test_bol { +// base_recipe.Recipe01[key] = val +// } + +// } + +// } + +// // verify changes between tmpX and actual filename.json + +// // if changes, rename tmpX to filename (version +1) .json + +// } diff --git a/server/models/recipe.go b/server/models/recipe.go index c091e16..00bcee7 100644 --- a/server/models/recipe.go +++ b/server/models/recipe.go @@ -1,159 +1,160 @@ -package models - -import "encoding/json" - -type Recipe struct { - Timestamp string `json:"Timestamp"` - MachineSetting MachineSetting `json:"MachineSetting"` - Recipe01 []Recipe01 `json:"Recipe01"` - Topping Topping `json:"Topping"` - MaterialCode []MaterialCode `json:"MaterialCode"` - MaterialSetting []MaterialSetting `json:"MaterialSetting"` -} - -type MachineSetting struct { - RecipeTag string `json:"RecipeTag"` - StrTextShowError []string `json:"strTextShowError"` - ConfigNumber int `json:"configNumber"` - TemperatureMax int `json:"temperatureMax"` - TemperatureMin int `json:"temperatureMin"` -} - -type MaterialCode struct { - PackageDescription string `json:"PackageDescription"` - RefillValuePerStep int `json:"RefillValuePerStep"` - MaterialID uint64 `json:"materialID"` - MaterialCode string `json:"materialCode"` -} - -type MaterialSetting struct { - AlarmIDWhenOffline int `json:"AlarmIDWhenOffline"` - BeanChannel bool `json:"BeanChannel"` - CanisterType string `json:"CanisterType"` - DrainTimer int `json:"DrainTimer"` - IsEquipment bool `json:"IsEquipment"` - LeavesChannel bool `json:"LeavesChannel"` - LowToOffline int `json:"LowToOffline"` - MaterialStatus int `json:"MaterialStatus"` - PowderChannel bool `json:"PowderChannel"` - RefillUnitGram bool `json:"RefillUnitGram"` - RefillUnitMilliliters bool `json:"RefillUnitMilliliters"` - RefillUnitPCS bool `json:"RefillUnitPCS"` - ScheduleDrainType int `json:"ScheduleDrainType"` - SodaChannel bool `json:"SodaChannel"` - StockAdjust int `json:"StockAdjust"` - SyrupChannel bool `json:"SyrupChannel"` - ID uint64 `json:"id"` - IDAlternate int `json:"idAlternate"` - IsUse bool `json:"isUse"` - PayRettryMaxCount int `json:"pay_rettry_max_count"` - FeedMode string `json:"feed_mode"` - MaterialParameter string `json:"MaterialParameter"` -} - -type Recipe01 struct { - Description string `json:"Description"` - ExtendID int `json:"ExtendID"` - OnTOP bool `json:"OnTOP"` - LastChange string `json:"LastChange"` - MenuStatus int `json:"MenuStatus"` - RemainingCups json.Number `json:"RemainingCups"` - StringParam string `json:"StringParam"` - TextForWarningBeforePay []string `json:"TextForWarningBeforePay"` - CashPrice int `json:"cashPrice"` - Changerecipe string `json:"changerecipe"` - Disable bool `json:"disable"` - Disable_by_cup bool `json:"disable_by_cup"` - Disable_by_ice bool `json:"disable_by_ice"` - EncoderCount int `json:"EncoderCount"` - ID int `json:"id"` - IsUse bool `json:"isUse"` - IsShow bool `json:"isShow"` - Name string `json:"name"` - NonCashPrice int `json:"nonCashPrice"` - OtherDescription string `json:"otherDescription"` - OtherName string `json:"otherName"` - ProductCode string `json:"productCode"` - Recipes []MatRecipe `json:"recipes"` - SubMenu []Recipe01 `json:"SubMenu"` - ToppingSet []ToppingSet `json:"ToppingSet"` - Total_time int `json:"total_time"` - Total_weight int `json:"total_weight"` - UriData string `json:"uriData"` - UseGram bool `json:"useGram"` - Weight_float int `json:"weight_float"` -} - -func (r *Recipe01) ToMap() map[string]interface{} { - var m map[string]interface{} - recipeRecord, _ := json.Marshal(r) - json.Unmarshal(recipeRecord, &m) - return m -} - -func (r *Recipe01) FromMap(m map[string]interface{}) Recipe01 { - recipeRecord, _ := json.Marshal(m) - json.Unmarshal(recipeRecord, &r) - return *r -} - -type MatRecipe struct { - MixOrder int `json:"MixOrder"` - FeedParameter int `json:"FeedParameter"` - FeedPattern int `json:"FeedPattern"` - IsUse bool `json:"isUse"` - MaterialPathId int `json:"materialPathId"` - PowderGram int `json:"powderGram"` - PowderTime int `json:"powderTime"` - StirTime int `json:"stirTime"` - SyrupGram int `json:"syrupGram"` - SyrupTime int `json:"syrupTime"` - WaterCold int `json:"waterCold"` - WaterYield int `json:"waterYield"` -} - -type ToppingSet struct { - ListGroupID []int `json:"ListGroupID"` - DefaultIDSelect int `json:"defaultIDSelect"` - GroupID string `json:"groupID"` - IsUse bool `json:"isUse"` -} - -type Topping struct { - ToppingGroup []ToppingGroup `json:"ToppingGroup"` - ToppingList []ToppingList `json:"ToppingList"` -} - -type ToppingGroup struct { - Desc string `json:"Desc"` - GroupID int `json:"groupID"` - IDDefault int `json:"idDefault"` - IDInGroup string `json:"idInGroup"` - InUse bool `json:"inUse"` - Name string `json:"name"` - OtherName string `json:"otherName"` -} - -type ToppingList struct { - ExtendID int `json:"ExtendID"` - OnTOP bool `json:"OnTOP"` - MenuStatus int `json:"MenuStatus"` - CashPrice int `json:"cashPrice"` - Disable bool `json:"disable"` - Disable_by_cup bool `json:"disable_by_cup"` - Disable_by_ice bool `json:"disable_by_ice"` - EncoderCount int `json:"EncoderCount"` - ID int `json:"id"` - IsUse bool `json:"isUse"` - IsShow bool `json:"isShow"` - StringParam string `json:"stringParam"` - Name string `json:"name"` - NonCashPrice int `json:"nonCashPrice"` - OtherName string `json:"otherName"` - ProductCode string `json:"productCode"` - Recipes []MatRecipe `json:"recipes"` - Total_time int `json:"total_time"` - Total_weight int `json:"total_weight"` - UseGram bool `json:"useGram"` - Weight_float int `json:"weight_float"` -} +package models + +import "encoding/json" + +type Recipe struct { + Timestamp string `json:"Timestamp"` + MachineSetting MachineSetting `json:"MachineSetting"` + Recipe01 []Recipe01 `json:"Recipe01"` + Topping Topping `json:"Topping"` + MaterialCode []MaterialCode `json:"MaterialCode"` + MaterialSetting []MaterialSetting `json:"MaterialSetting"` +} + +type MachineSetting struct { + RecipeTag string `json:"RecipeTag"` + StrTextShowError []string `json:"strTextShowError"` + ConfigNumber int `json:"configNumber"` + Comment []string `json:"Comment"` + TemperatureMax int `json:"temperatureMax"` + TemperatureMin int `json:"temperatureMin"` +} + +type MaterialCode struct { + PackageDescription string `json:"PackageDescription"` + RefillValuePerStep int `json:"RefillValuePerStep"` + MaterialID uint64 `json:"materialID"` + MaterialCode string `json:"materialCode"` +} + +type MaterialSetting struct { + AlarmIDWhenOffline int `json:"AlarmIDWhenOffline"` + BeanChannel bool `json:"BeanChannel"` + CanisterType string `json:"CanisterType"` + DrainTimer int `json:"DrainTimer"` + IsEquipment bool `json:"IsEquipment"` + LeavesChannel bool `json:"LeavesChannel"` + LowToOffline int `json:"LowToOffline"` + MaterialStatus int `json:"MaterialStatus"` + PowderChannel bool `json:"PowderChannel"` + RefillUnitGram bool `json:"RefillUnitGram"` + RefillUnitMilliliters bool `json:"RefillUnitMilliliters"` + RefillUnitPCS bool `json:"RefillUnitPCS"` + ScheduleDrainType int `json:"ScheduleDrainType"` + SodaChannel bool `json:"SodaChannel"` + StockAdjust int `json:"StockAdjust"` + SyrupChannel bool `json:"SyrupChannel"` + ID uint64 `json:"id"` + IDAlternate int `json:"idAlternate"` + IsUse bool `json:"isUse"` + PayRettryMaxCount int `json:"pay_rettry_max_count"` + FeedMode string `json:"feed_mode"` + MaterialParameter string `json:"MaterialParameter"` +} + +type Recipe01 struct { + Description string `json:"Description"` + ExtendID int `json:"ExtendID"` + OnTOP bool `json:"OnTOP"` + LastChange string `json:"LastChange"` + MenuStatus int `json:"MenuStatus"` + RemainingCups json.Number `json:"RemainingCups"` + StringParam string `json:"StringParam"` + TextForWarningBeforePay []string `json:"TextForWarningBeforePay"` + CashPrice int `json:"cashPrice"` + Changerecipe string `json:"changerecipe"` + Disable bool `json:"disable"` + Disable_by_cup bool `json:"disable_by_cup"` + Disable_by_ice bool `json:"disable_by_ice"` + EncoderCount int `json:"EncoderCount"` + ID int `json:"id"` + IsUse bool `json:"isUse"` + IsShow bool `json:"isShow"` + Name string `json:"name"` + NonCashPrice int `json:"nonCashPrice"` + OtherDescription string `json:"otherDescription"` + OtherName string `json:"otherName"` + ProductCode string `json:"productCode"` + Recipes []MatRecipe `json:"recipes"` + SubMenu []Recipe01 `json:"SubMenu"` + ToppingSet []ToppingSet `json:"ToppingSet"` + Total_time int `json:"total_time"` + Total_weight int `json:"total_weight"` + UriData string `json:"uriData"` + UseGram bool `json:"useGram"` + Weight_float int `json:"weight_float"` +} + +func (r *Recipe01) ToMap() map[string]interface{} { + var m map[string]interface{} + recipeRecord, _ := json.Marshal(r) + json.Unmarshal(recipeRecord, &m) + return m +} + +func (r *Recipe01) FromMap(m map[string]interface{}) Recipe01 { + recipeRecord, _ := json.Marshal(m) + json.Unmarshal(recipeRecord, &r) + return *r +} + +type MatRecipe struct { + MixOrder int `json:"MixOrder"` + FeedParameter int `json:"FeedParameter"` + FeedPattern int `json:"FeedPattern"` + IsUse bool `json:"isUse"` + MaterialPathId int `json:"materialPathId"` + PowderGram int `json:"powderGram"` + PowderTime int `json:"powderTime"` + StirTime int `json:"stirTime"` + SyrupGram int `json:"syrupGram"` + SyrupTime int `json:"syrupTime"` + WaterCold int `json:"waterCold"` + WaterYield int `json:"waterYield"` +} + +type ToppingSet struct { + ListGroupID []int `json:"ListGroupID"` + DefaultIDSelect int `json:"defaultIDSelect"` + GroupID string `json:"groupID"` + IsUse bool `json:"isUse"` +} + +type Topping struct { + ToppingGroup []ToppingGroup `json:"ToppingGroup"` + ToppingList []ToppingList `json:"ToppingList"` +} + +type ToppingGroup struct { + Desc string `json:"Desc"` + GroupID int `json:"groupID"` + IDDefault int `json:"idDefault"` + IDInGroup string `json:"idInGroup"` + InUse bool `json:"inUse"` + Name string `json:"name"` + OtherName string `json:"otherName"` +} + +type ToppingList struct { + ExtendID int `json:"ExtendID"` + OnTOP bool `json:"OnTOP"` + MenuStatus int `json:"MenuStatus"` + CashPrice int `json:"cashPrice"` + Disable bool `json:"disable"` + Disable_by_cup bool `json:"disable_by_cup"` + Disable_by_ice bool `json:"disable_by_ice"` + EncoderCount int `json:"EncoderCount"` + ID int `json:"id"` + IsUse bool `json:"isUse"` + IsShow bool `json:"isShow"` + StringParam string `json:"stringParam"` + Name string `json:"name"` + NonCashPrice int `json:"nonCashPrice"` + OtherName string `json:"otherName"` + ProductCode string `json:"productCode"` + Recipes []MatRecipe `json:"recipes"` + Total_time int `json:"total_time"` + Total_weight int `json:"total_weight"` + UseGram bool `json:"useGram"` + Weight_float int `json:"weight_float"` +} diff --git a/server/routers/recipe.go b/server/routers/recipe.go index c3b94f9..5204f01 100644 --- a/server/routers/recipe.go +++ b/server/routers/recipe.go @@ -1,326 +1,400 @@ -package routers - -import ( - "encoding/json" - "fmt" - "net/http" - "os" - "path" - "recipe-manager/contracts" - "recipe-manager/data" - "recipe-manager/helpers" - "recipe-manager/models" - "recipe-manager/services/logger" - "recipe-manager/services/recipe" - "recipe-manager/services/sheet" - "strconv" - "strings" - - "github.com/go-chi/chi/v5" - "go.uber.org/zap" -) - -type RecipeRouter struct { - data *data.Data - sheetService sheet.SheetService - recipeService recipe.RecipeService -} - -var ( - Log = logger.GetInstance() -) - -func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter { - return &RecipeRouter{ - data: data, - recipeService: recipeService, - sheetService: sheetService, - } -} - -func (rr *RecipeRouter) Route(r chi.Router) { - r.Route("/recipes", func(r chi.Router) { - r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - - country := r.URL.Query().Get("country") - filename := r.URL.Query().Get("filename") - - result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{ - Country: country, - Filename: filename, - }) - - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - - json.NewEncoder(w).Encode(result) - }) - - r.Get("/overview", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - var take, offset uint64 = 10, 0 - if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil { - offset = newOffset - } - - if newTake, err := strconv.ParseUint(r.URL.Query().Get("take"), 10, 64); err == nil { - take = newTake - } - - country := r.URL.Query().Get("country") - filename := r.URL.Query().Get("filename") - materialIds := r.URL.Query().Get("materialIds") - - var materialIdsUint []int - for _, v := range strings.Split(materialIds, ",") { - materialIdUint, err := strconv.ParseUint(v, 10, 64) - if err != nil || materialIdUint == 0 { - continue - } - materialIdsUint = append(materialIdsUint, int(materialIdUint)) - } - - result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{ - Take: int(take), - Skip: int(offset), - Search: r.URL.Query().Get("search"), - Country: country, - Filename: filename, - MatIds: materialIdsUint, - }) - - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - - json.NewEncoder(w).Encode(result) - }) - - r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - productCode := chi.URLParam(r, "product_code") - - // recipe := rr.data.GetRecipe01() - // recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE") - - // var recipeResult *models.Recipe01 - // recipeMetaDataResult := map[string]string{} - - // for _, v := range recipe { - // if v.ProductCode == productCode { - // recipeResult = &v - // break - // } - // } - - // for _, v := range recipeMetaData { - // if v[0].(string) == productCode { - // recipeMetaDataResult = map[string]string{ - // "productCode": v[0].(string), - // "name": v[1].(string), - // "otherName": v[2].(string), - // "description": v[3].(string), - // "otherDescription": v[4].(string), - // "picture": v[5].(string), - // } - // break - // } - // } - - // if recipeResult == nil { - // http.Error(w, "Not Found", http.StatusNotFound) - // return - // } - - // json.NewEncoder(w).Encode(map[string]interface{}{ - // "recipe": recipeResult, - // "recipeMetaData": recipeMetaDataResult, - // }) - - result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{ - Filename: r.URL.Query().Get("filename"), - Country: r.URL.Query().Get("country"), - ProductCode: productCode, - }) - - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - - json.NewEncoder(w).Encode(result) - }) - - r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - productCode := chi.URLParam(r, "product_code") - - result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{ - Filename: r.URL.Query().Get("filename"), - Country: r.URL.Query().Get("country"), - ProductCode: productCode, - }) - - if err != nil { - http.Error(w, err.Error(), http.StatusNotFound) - return - } - - json.NewEncoder(w).Encode(result) - }) - - r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) { - country := chi.URLParam(r, "country") - filename := chi.URLParam(r, "filename") - - w.Header().Add("Content-Type", "application/json") - countryID, err := rr.data.GetCountryIDByName(country) - if err != nil { - http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound) - return - } - json.NewEncoder(w).Encode(rr.data.GetRecipe(countryID, filename)) - }) - - r.Get("/versions", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - // get key from map - keys := []string{} - for k := range rr.data.AllRecipeFiles { - countryName, err := rr.data.GetCountryNameByID(k) - if err != nil { - continue - } - keys = append(keys, countryName) - } - json.NewEncoder(w).Encode(keys) - }) - - r.Get("/versions/{country}", func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - - countryName := chi.URLParam(r, "country") - countryID, err := rr.data.GetCountryIDByName(countryName) - if err != nil { - http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", countryName), http.StatusNotFound) - return - } - files := []string{} - for _, v := range rr.data.AllRecipeFiles[countryID] { - files = append(files, v.Name) - } - json.NewEncoder(w).Encode(files) - }) - - r.Get("/test/sheet", func(w http.ResponseWriter, r *http.Request) { - result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE") - - mapResult := []map[string]string{} - - for _, v := range result { - mapResult = append(mapResult, map[string]string{ - "productCode": v[0].(string), - "name": v[1].(string), - "otherName": v[2].(string), - "description": v[3].(string), - "otherDescription": v[4].(string), - "picture": v[5].(string), - }) - } - json.NewEncoder(w).Encode(mapResult) - }) - - r.Post("/edit/{country}/{filename}", func(w http.ResponseWriter, r *http.Request) { - Log.Debug("Edit: ", zap.String("path", r.RequestURI)) - filename := chi.URLParam(r, "filename") - country := chi.URLParam(r, "country") - - countryID, err := rr.data.GetCountryIDByName(country) - if err != nil { - http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound) - return - } - - target_recipe := rr.data.GetRecipe(countryID, filename) - - Log.Debug("Target => ", zap.Any("target", target_recipe.MachineSetting.ConfigNumber)) - - // check request structure - - // FIXME: Request structure bug. Case-sensitive, likely bug at client - // uncomment the below code to view the bug - - // var change_request map[string]interface{} - // err = json.NewDecoder(r.Body).Decode(&change_request) - // if err != nil { - // Log.Error("Decode in request failed: ", zap.Error(err)) - // } - // Log.Debug("Request => ", zap.Any("request", change_request)) - - // Body - var changes models.Recipe01 - err = json.NewDecoder(r.Body).Decode(&changes) - if err != nil { - Log.Error("Decode in request failed: ", zap.Error(err)) - } - - Log.Debug("Changes: ", zap.Any("changes", changes)) - // TODO: find the matched pd - target_menu, err := rr.data.GetRecipe01ByProductCode(filename, countryID, changes.ProductCode) - - if err != nil { - Log.Error("Error when get recipe by product code", zap.Error(err)) - return - } - - menu_map := target_menu.ToMap() - change_map := changes.ToMap() - - // Find changes - for key, val := range menu_map { - - test_bool, err := helpers.DynamicCompare(val, change_map[key]) - - if err != nil { - Log.Error("DynamicCompare in request failed: ", zap.Error(err)) - } - - if !test_bool { - menu_map[key] = change_map[key] - } - } - - // Apply changes - tempRecipe := models.Recipe01{} - tempRecipe = tempRecipe.FromMap(menu_map) - rr.data.SetValuesToRecipe(tempRecipe) - Log.Debug("ApplyChange", zap.Any("status", "passed")) - - // check if changed - // Log.Debug("Check if changed", zap.Any("result", rr.data.GetRecipe01ByProductCode(changes.ProductCode))) - - file, _ := os.Create(path.Join("./cofffeemachineConfig", countryID, filename)) - if err != nil { - Log.Error("Error when tried to create file", zap.Error(err)) - return - } - - encoder := json.NewEncoder(file) - encoder.SetIndent("", " ") - err = encoder.Encode(rr.data.GetRecipe(countryID, filename)) - - if err != nil { - Log.Error("Error when write file", zap.Error(err)) - } - - w.Header().Add("Content-Type", "application/json") - json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "OK", - }) - }) - }) -} +package routers + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "path" + "recipe-manager/contracts" + "recipe-manager/data" + "recipe-manager/helpers" + "recipe-manager/models" + "recipe-manager/services/logger" + "recipe-manager/services/recipe" + "recipe-manager/services/sheet" + "strconv" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "go.uber.org/zap" +) + +type RecipeRouter struct { + data *data.Data + sheetService sheet.SheetService + recipeService recipe.RecipeService +} + +var ( + Log = logger.GetInstance() +) + +func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter { + return &RecipeRouter{ + data: data, + recipeService: recipeService, + sheetService: sheetService, + } +} + +func (rr *RecipeRouter) Route(r chi.Router) { + r.Route("/recipes", func(r chi.Router) { + r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + + country := r.URL.Query().Get("country") + filename := r.URL.Query().Get("filename") + + result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{ + Country: country, + Filename: filename, + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(result) + }) + + r.Get("/overview", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + var take, offset uint64 = 10, 0 + if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil { + offset = newOffset + } + + if newTake, err := strconv.ParseUint(r.URL.Query().Get("take"), 10, 64); err == nil { + take = newTake + } + + country := r.URL.Query().Get("country") + filename := r.URL.Query().Get("filename") + materialIds := r.URL.Query().Get("materialIds") + + var materialIdsUint []int + for _, v := range strings.Split(materialIds, ",") { + materialIdUint, err := strconv.ParseUint(v, 10, 64) + if err != nil || materialIdUint == 0 { + continue + } + materialIdsUint = append(materialIdsUint, int(materialIdUint)) + } + + result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{ + Take: int(take), + Skip: int(offset), + Search: r.URL.Query().Get("search"), + Country: country, + Filename: filename, + MatIds: materialIdsUint, + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(result) + }) + + r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + productCode := chi.URLParam(r, "product_code") + + // recipe := rr.data.GetRecipe01() + // recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE") + + // var recipeResult *models.Recipe01 + // recipeMetaDataResult := map[string]string{} + + // for _, v := range recipe { + // if v.ProductCode == productCode { + // recipeResult = &v + // break + // } + // } + + // for _, v := range recipeMetaData { + // if v[0].(string) == productCode { + // recipeMetaDataResult = map[string]string{ + // "productCode": v[0].(string), + // "name": v[1].(string), + // "otherName": v[2].(string), + // "description": v[3].(string), + // "otherDescription": v[4].(string), + // "picture": v[5].(string), + // } + // break + // } + // } + + // if recipeResult == nil { + // http.Error(w, "Not Found", http.StatusNotFound) + // return + // } + + // json.NewEncoder(w).Encode(map[string]interface{}{ + // "recipe": recipeResult, + // "recipeMetaData": recipeMetaDataResult, + // }) + + result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{ + Filename: r.URL.Query().Get("filename"), + Country: r.URL.Query().Get("country"), + ProductCode: productCode, + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(result) + }) + + r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + productCode := chi.URLParam(r, "product_code") + + result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{ + Filename: r.URL.Query().Get("filename"), + Country: r.URL.Query().Get("country"), + ProductCode: productCode, + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + json.NewEncoder(w).Encode(result) + }) + + r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) { + country := chi.URLParam(r, "country") + filename := chi.URLParam(r, "filename") + + w.Header().Add("Content-Type", "application/json") + countryID, err := rr.data.GetCountryIDByName(country) + if err != nil { + http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound) + return + } + json.NewEncoder(w).Encode(rr.data.GetRecipe(countryID, filename)) + }) + + r.Get("/versions", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + // get key from map + keys := []string{} + for k := range rr.data.AllRecipeFiles { + countryName, err := rr.data.GetCountryNameByID(k) + if err != nil { + continue + } + keys = append(keys, countryName) + } + json.NewEncoder(w).Encode(keys) + }) + + r.Get("/versions/{country}", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + + countryName := chi.URLParam(r, "country") + countryID, err := rr.data.GetCountryIDByName(countryName) + if err != nil { + http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", countryName), http.StatusNotFound) + return + } + files := []string{} + for _, v := range rr.data.AllRecipeFiles[countryID] { + files = append(files, v.Name) + } + json.NewEncoder(w).Encode(files) + }) + + r.Get("/test/sheet", func(w http.ResponseWriter, r *http.Request) { + result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE") + + mapResult := []map[string]string{} + + for _, v := range result { + mapResult = append(mapResult, map[string]string{ + "productCode": v[0].(string), + "name": v[1].(string), + "otherName": v[2].(string), + "Description": v[3].(string), + "otherDescription": v[4].(string), + "picture": v[5].(string), + }) + } + json.NewEncoder(w).Encode(mapResult) + }) + + r.Post("/edit/{country}/{filename}", func(w http.ResponseWriter, r *http.Request) { + Log.Debug("Edit: ", zap.String("path", r.RequestURI)) + filename := chi.URLParam(r, "filename") + country := chi.URLParam(r, "country") + + countryID, err := rr.data.GetCountryIDByName(country) + if err != nil { + http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound) + return + } + + target_recipe := rr.data.GetRecipe(countryID, filename) + + Log.Debug("Target => ", zap.Any("target", target_recipe.MachineSetting.ConfigNumber)) + + // check request structure + + // Body + var ch_map map[string]interface{} + var changes models.Recipe01 + + err = json.NewDecoder(r.Body).Decode(&ch_map) + if err != nil { + Log.Error("Decode in request failed: ", zap.Error(err)) + } + + // commit request + editor := ch_map["edit_by"].(string) + Log.Debug("requester", zap.Any("editor", editor)) + commit_msg := ch_map["commit_msg"].(string) + Log.Debug("commit_msg", zap.Any("commit_msg", commit_msg)) + + changes = changes.FromMap(ch_map) + + Log.Debug("Changes: ", zap.Any("changes", changes)) + // TODO: find the matched pd + target_menu, err := rr.data.GetRecipe01ByProductCode(filename, countryID, changes.ProductCode) + + if err != nil { + Log.Error("Error when get recipe by product code", zap.Error(err)) + return + } + + menu_map := target_menu.ToMap() + change_map := changes.ToMap() + + // Find changes + for key, val := range menu_map { + + test_bool, err := helpers.DynamicCompare(val, change_map[key]) + + if err != nil { + Log.Error("DynamicCompare in request failed: ", zap.Error(err)) + } + + if !test_bool { + menu_map[key] = change_map[key] + } + } + + // Apply changes + tempRecipe := models.Recipe01{} + tempRecipe = tempRecipe.FromMap(menu_map) + rr.data.SetValuesToRecipe(tempRecipe) + Log.Debug("ApplyChange", zap.Any("status", "passed")) + + // check if changed + // Log.Debug("Check if changed", zap.Any("result", rr.data.GetRecipe01ByProductCode(changes.ProductCode))) + + // target saved filename + saved_filename := path.Join("./cofffeemachineConfig", countryID, filename) + + // store @ temporary file + temp_file_name := helpers.GetTempFile(saved_filename, editor, 0) + + // TODO: push this change, editor, commit_msg into db + + // gen hash + commit_hash, err := data.HashCommit(8) + + if err != nil { + Log.Error("Error when hash commit", zap.Error(err)) + return + } + + commit := data.CommitLog{ + + Id: commit_hash, + Msg: commit_msg, + Created_at: time.Now().Format("2006-01-02 15:04:05"), + Editor: editor, + Change_file: temp_file_name, + } + + err = data.Insert(&commit) + Log.Debug("Commit", zap.Any("attr", commit)) + + if err != nil { + Log.Error("Error when insert commit log", zap.Error(err)) + return + } + + file, _ := os.Create(temp_file_name) + if err != nil { + Log.Error("Error when tried to create file", zap.Error(err)) + return + } + + encoder := json.NewEncoder(file) + encoder.SetIndent("", " ") + // err = encoder.Encode(rr.data.GetRecipe(countryID, temp_file_name)) + err = encoder.Encode(rr.data.GetRecipe(countryID, filename)) + + if err != nil { + Log.Error("Error when write file", zap.Error(err)) + } + + w.Header().Add("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "commit_id": commit_hash, + }) + }) + + // get saved files + r.Get("/saved/{country}/{filename_version_only}", func(w http.ResponseWriter, r *http.Request) { + file_version := chi.URLParam(r, "filename_version_only") + country := chi.URLParam(r, "country") + + countryID, err := rr.data.GetCountryIDByName(country) + if err != nil { + http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound) + return + } + + recipe_root_path := "./cofffeemachineConfig/" + + // structure + full_file_name_targets := []string{} + + files, err := os.ReadDir(recipe_root_path + countryID) + + if err != nil { + Log.Error("Error when read directory", zap.Error(err)) + return + } + + for _, file := range files { + Log.Debug("File: ", zap.Any("file", file.Name())) + if strings.Contains(file.Name(), file_version) && strings.Contains(file.Name(), ".tmp") { + full_file_name_targets = append(full_file_name_targets, file.Name()) + } + } + + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{"files": full_file_name_targets}) + + Log.Debug("Saved Files: ", zap.Any("files", full_file_name_targets)) + }) + }) +} diff --git a/server/services/cli/cli.go b/server/services/cli/cli.go deleted file mode 100644 index 7c564db..0000000 --- a/server/services/cli/cli.go +++ /dev/null @@ -1,44 +0,0 @@ -package cli - -import ( - "bufio" - "os" - "recipe-manager/services/logger" - "strings" - - "go.uber.org/zap" -) - -var ( - log_inst = logger.GetInstance() - disable_cli = false - debug = logger.GetDbgState() -) - -func CommandLineListener() { - debug = logger.GetDbgState() - reader := bufio.NewReader(os.Stdin) - for !disable_cli { - input, _ := reader.ReadString('\n') - input = strings.TrimSpace(input) - - switch input { - case "debug": - logger.EnableDebug(!logger.GetDbgState()) - debug = logger.GetDbgState() - // log_inst.Info("Debug mode enable from cli", zap.Bool("enable", logger.GetDbgState())) - case "ctl": - if debug { - log_inst.Debug("CMD > ", zap.String("CMD", input)) - } - default: - if debug { - // log_inst.Debug("CMD > ", zap.String("CMD", input)) - - // Add functions here! - } else { - log_inst.Error("INVALID CMD or CMD DISABLED", zap.String("CMD", input)) - } - } - } -} diff --git a/server/services/recipe/recipe.go b/server/services/recipe/recipe.go index 51bab85..b48064b 100644 --- a/server/services/recipe/recipe.go +++ b/server/services/recipe/recipe.go @@ -1,192 +1,204 @@ -package recipe - -import ( - "fmt" - "recipe-manager/contracts" - "recipe-manager/data" - "recipe-manager/models" - "sort" - "strings" -) - -type RecipeService interface { - GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error) - GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error) - - GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) - GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) -} - -type recipeService struct { - db *data.Data -} - -// GetRecipeDetail implements RecipeService. -func (rs *recipeService) GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) { - - recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode) - - if err != nil { - return contracts.RecipeDetailResponse{}, err - } - - result := contracts.RecipeDetailResponse{ - Name: recipe.Name, - OtherName: recipe.OtherName, - Description: recipe.Description, - OtherDescription: recipe.OtherDescription, - LastUpdated: recipe.LastChange, - Picture: recipe.UriData[len("img="):], // remove "img=" prefix - } - - return result, nil -} - -// GetRecipeDetailMat implements RecipeService. -func (rs *recipeService) GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) { - countryID, err := rs.db.GetCountryIDByName(request.Country) - - if err != nil { - return contracts.RecipeDetailMatListResponse{}, fmt.Errorf("country name: %s not found", request.Country) - } - - recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode) - - if err != nil { - return contracts.RecipeDetailMatListResponse{}, err - } - - matIds := []uint64{} - for _, v := range recipe.Recipes { - if v.IsUse { - matIds = append(matIds, uint64(v.MaterialPathId)) - } - } - - matsCode := rs.db.GetMaterialCode(matIds, countryID, request.Filename) - - result := contracts.RecipeDetailMatListResponse{ - Result: []contracts.RecipeDetailMat{}, - } - - for _, v := range recipe.Recipes { - for _, mat := range matsCode { - if v.MaterialPathId == int(mat.MaterialID) { - result.Result = append(result.Result, contracts.RecipeDetailMat{ - IsUse: v.IsUse, - MaterialID: mat.MaterialID, - Name: mat.PackageDescription, - MixOrder: v.MixOrder, - FeedParameter: v.FeedParameter, - FeedPattern: v.FeedPattern, - MaterialPathId: v.MaterialPathId, - PowderGram: v.PowderGram, - PowderTime: v.PowderTime, - StirTime: v.StirTime, - SyrupGram: v.SyrupGram, - SyrupTime: v.SyrupTime, - WaterCold: v.WaterCold, - WaterYield: v.WaterYield, - }) - break - } - } - } - - // sort by id - sort.Slice(result.Result, func(i, j int) bool { - return result.Result[i].MaterialID < result.Result[j].MaterialID - }) - - return result, nil -} - -func (rs *recipeService) GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error) { - countryID, err := rs.db.GetCountryIDByName(request.Country) - - if err != nil { - return contracts.RecipeDashboardResponse{}, fmt.Errorf("country name: %s not found", request.Country) - } - - recipe := rs.db.GetRecipe(countryID, request.Filename) - - result := contracts.RecipeDashboardResponse{ - ConfigNumber: recipe.MachineSetting.ConfigNumber, - LastUpdated: recipe.Timestamp, - Filename: request.Filename, - } - - return result, nil -} - -func (rs *recipeService) GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error) { - countryID, err := rs.db.GetCountryIDByName(request.Country) - - if err != nil { - return contracts.RecipeOverviewResponse{}, fmt.Errorf("country name: %s not found", request.Country) - } - recipe := rs.db.GetRecipe(countryID, request.Filename) - recipeFilter := recipe.Recipe01 - - result := contracts.RecipeOverviewResponse{} - - if request.Search != "" { - searchResult := []models.Recipe01{} - for _, v := range recipeFilter { - if strings.Contains(strings.ToLower(v.ProductCode), strings.ToLower(request.Search)) || - strings.Contains(strings.ToLower(v.Name), strings.ToLower(request.Search)) || - strings.Contains(strings.ToLower(v.OtherName), strings.ToLower(request.Search)) { - searchResult = append(searchResult, v) - } - } - recipeFilter = searchResult - } - - if len(request.MatIds) > 0 { - matIdsFiltered := []models.Recipe01{} - for _, v := range recipeFilter { - for _, matID := range request.MatIds { - for _, recipe := range v.Recipes { - if recipe.IsUse && recipe.MaterialPathId == matID { - matIdsFiltered = append(matIdsFiltered, v) - } - } - } - } - recipeFilter = matIdsFiltered - } - - // Map to contracts.RecipeOverview - for _, v := range recipeFilter { - result.Result = append(result.Result, contracts.RecipeOverview{ - ID: v.ID, - ProductCode: v.ProductCode, - Name: v.Name, - OtherName: v.OtherName, - Description: v.Description, - LastUpdated: v.LastChange, - }) - } - - result.TotalCount = len(result.Result) - - result.HasMore = result.TotalCount >= request.Take+request.Skip - if result.HasMore { - result.Result = result.Result[request.Skip : request.Take+request.Skip] - sort.Slice(result.Result, func(i, j int) bool { - return result.Result[i].ID < result.Result[j].ID - }) - } else if result.TotalCount > request.Skip { - result.Result = result.Result[request.Skip:] - } else { - result.Result = []contracts.RecipeOverview{} - } - - return result, nil -} - -func NewRecipeService(db *data.Data) RecipeService { - return &recipeService{ - db: db, - } -} +package recipe + +import ( + "fmt" + "recipe-manager/contracts" + "recipe-manager/data" + "recipe-manager/models" + "recipe-manager/services/logger" + "sort" + "strings" + + "go.uber.org/zap" +) + +var ( + Log = logger.GetInstance() +) + +type RecipeService interface { + GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error) + GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error) + + GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) + GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) +} + +type recipeService struct { + db *data.Data +} + +// GetRecipeDetail implements RecipeService. +func (rs *recipeService) GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) { + + Log.Debug("GetRecipeDetail", zap.Any("request", request)) + + recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode) + + if err != nil { + return contracts.RecipeDetailResponse{}, err + } + + // DEBUG: picture + Log.Debug("GetRecipeDetail", zap.String("picture", recipe.UriData)) + + result := contracts.RecipeDetailResponse{ + Name: recipe.Name, + OtherName: recipe.OtherName, + Description: recipe.Description, + OtherDescription: recipe.OtherDescription, + LastUpdated: recipe.LastChange, + Picture: recipe.UriData[len("img="):], // remove "img=" prefix + } + + return result, nil +} + +// GetRecipeDetailMat implements RecipeService. +func (rs *recipeService) GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) { + countryID, err := rs.db.GetCountryIDByName(request.Country) + + if err != nil { + return contracts.RecipeDetailMatListResponse{}, fmt.Errorf("country name: %s not found", request.Country) + } + + recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode) + + if err != nil { + return contracts.RecipeDetailMatListResponse{}, err + } + + matIds := []uint64{} + for _, v := range recipe.Recipes { + if v.IsUse { + matIds = append(matIds, uint64(v.MaterialPathId)) + } + } + + matsCode := rs.db.GetMaterialCode(matIds, countryID, request.Filename) + + result := contracts.RecipeDetailMatListResponse{ + Result: []contracts.RecipeDetailMat{}, + } + + for _, v := range recipe.Recipes { + for _, mat := range matsCode { + if v.MaterialPathId == int(mat.MaterialID) { + result.Result = append(result.Result, contracts.RecipeDetailMat{ + IsUse: v.IsUse, + MaterialID: mat.MaterialID, + Name: mat.PackageDescription, + MixOrder: v.MixOrder, + FeedParameter: v.FeedParameter, + FeedPattern: v.FeedPattern, + MaterialPathId: v.MaterialPathId, + PowderGram: v.PowderGram, + PowderTime: v.PowderTime, + StirTime: v.StirTime, + SyrupGram: v.SyrupGram, + SyrupTime: v.SyrupTime, + WaterCold: v.WaterCold, + WaterYield: v.WaterYield, + }) + break + } + } + } + + // sort by id + // sort.Slice(result.Result, func(i, j int) bool { + // return result.Result[i].MaterialID < result.Result[j].MaterialID + // }) + + return result, nil +} + +func (rs *recipeService) GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error) { + countryID, err := rs.db.GetCountryIDByName(request.Country) + + if err != nil { + return contracts.RecipeDashboardResponse{}, fmt.Errorf("country name: %s not found", request.Country) + } + + recipe := rs.db.GetRecipe(countryID, request.Filename) + + result := contracts.RecipeDashboardResponse{ + ConfigNumber: recipe.MachineSetting.ConfigNumber, + LastUpdated: recipe.Timestamp, + Filename: request.Filename, + } + + return result, nil +} + +func (rs *recipeService) GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error) { + countryID, err := rs.db.GetCountryIDByName(request.Country) + + if err != nil { + return contracts.RecipeOverviewResponse{}, fmt.Errorf("country name: %s not found", request.Country) + } + recipe := rs.db.GetRecipe(countryID, request.Filename) + recipeFilter := recipe.Recipe01 + + result := contracts.RecipeOverviewResponse{} + + if request.Search != "" { + searchResult := []models.Recipe01{} + for _, v := range recipeFilter { + if strings.Contains(strings.ToLower(v.ProductCode), strings.ToLower(request.Search)) || + strings.Contains(strings.ToLower(v.Name), strings.ToLower(request.Search)) || + strings.Contains(strings.ToLower(v.OtherName), strings.ToLower(request.Search)) { + searchResult = append(searchResult, v) + } + } + recipeFilter = searchResult + } + + if len(request.MatIds) > 0 { + matIdsFiltered := []models.Recipe01{} + for _, v := range recipeFilter { + for _, matID := range request.MatIds { + for _, recipe := range v.Recipes { + if recipe.IsUse && recipe.MaterialPathId == matID { + matIdsFiltered = append(matIdsFiltered, v) + } + } + } + } + recipeFilter = matIdsFiltered + } + + // Map to contracts.RecipeOverview + for _, v := range recipeFilter { + result.Result = append(result.Result, contracts.RecipeOverview{ + ID: v.ID, + ProductCode: v.ProductCode, + Name: v.Name, + OtherName: v.OtherName, + Description: v.Description, + LastUpdated: v.LastChange, + }) + } + + result.TotalCount = len(result.Result) + + result.HasMore = result.TotalCount >= request.Take+request.Skip + if result.HasMore { + result.Result = result.Result[request.Skip : request.Take+request.Skip] + sort.Slice(result.Result, func(i, j int) bool { + return result.Result[i].ID < result.Result[j].ID + }) + } else if result.TotalCount > request.Skip { + result.Result = result.Result[request.Skip:] + } else { + result.Result = []contracts.RecipeOverview{} + } + + return result, nil +} + +func NewRecipeService(db *data.Data) RecipeService { + return &recipeService{ + db: db, + } +}