change save file method, add file changes history

This commit is contained in:
pakintada@gmail.com 2023-12-05 14:33:45 +07:00
parent 519749fd3a
commit 820557a268
12 changed files with 2388 additions and 2036 deletions

View file

@ -1,329 +1,332 @@
<div class="p-4"> <div class="p-4">
<form class="grid grid-cols-3 gap-4 mb-4" [formGroup]="recipeDetailForm"> <form class="grid grid-cols-3 gap-4 mb-4" [formGroup]="recipeDetailForm">
<div <div
class="block col-span-1 p-6 bg-white border border-gray-200 rounded-lg shadow" class="block col-span-1 p-6 bg-white border border-gray-200 rounded-lg shadow"
> >
<div *ngIf="isLoaded; else indicator" [@inOutAnimation]> <div *ngIf="isLoaded; else indicator" [@inOutAnimation]>
<div class="flex flex-wrap"> <div class="flex flex-wrap">
<h5 class="mb-2 text-xl font-bold text-gray-900"> <h5 class="mb-2 text-xl font-bold text-gray-900">
{{ recipeDetailForm.getRawValue().name }} {{ recipeDetailForm.getRawValue().name }}
</h5> </h5>
<h5 class="mb-2 px-3 text-xl font-bold text-gray-900">|</h5> <h5 class="mb-2 px-3 text-xl font-bold text-gray-900">|</h5>
<h5 class="mb-2 text-xl font-bold text-gray-900"> <h5 class="mb-2 text-xl font-bold text-gray-900">
{{ recipeDetailForm.getRawValue().otherName }} {{ recipeDetailForm.getRawValue().otherName }}
</h5> </h5>
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<div class="flex items-center"> <div class="flex items-center">
<p class="text-sm text-gray-500">Last Modify</p> <p class="text-sm text-gray-500">Last Modify</p>
<p class="ml-2 text-sm text-gray-900"> <p class="ml-2 text-sm text-gray-900">
{{ {{
recipeDetailForm.getRawValue().lastModified recipeDetailForm.getRawValue().lastModified
| date : "dd/MM/yyyy HH:mm:ss" | date : "dd/MM/yyyy HH:mm:ss"
}} }}
</p> </p>
</div> </div>
</div> </div>
<div class="flex p-3 items-center justify-center w-full"> <div class="flex p-3 items-center justify-center w-full">
<img <img
class="rounded-lg shadow" class="rounded-lg shadow"
src="/assets/{{ 'bn_hot_america_no' }}.png" src="/assets/{{ 'bn_hot_america_no' }}.png"
/> />
</div> </div>
</div> </div>
</div> </div>
<div <div
class="block col-span-2 p-6 bg-white border border-gray-200 rounded-lg shadow min-h-[300px]" class="block col-span-2 p-6 bg-white border border-gray-200 rounded-lg shadow min-h-[300px]"
> >
<div <div
*ngIf="isLoaded; else indicator" *ngIf="isLoaded; else indicator"
[@inOutAnimation] [@inOutAnimation]
class="grid-cols-2 w-full flex flex-col gap-2" class="grid-cols-2 w-full flex flex-col gap-2"
> >
<div class="flex items-center"> <div class="flex items-center">
<label class="label text-base mr-3 w-max" <label class="label text-base mr-3 w-max"
><span class="label-text text-base mr-3 w-max">Name:</span></label ><span class="label-text text-base mr-3 w-max">Name:</span></label
> >
<input <input
type="text" type="text"
class="input input-sm input-bordered input-ghost w-full" class="input input-sm input-bordered input-ghost w-full"
formControlName="name" formControlName="name"
/> />
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<label class="label"> <label class="label">
<span class="label-text text-base mr-3 w-max" <span class="label-text text-base mr-3 w-max"
>Other Name:</span >Other Name:</span
></label ></label
> >
<input <input
type="text" type="text"
class="input input-sm input-bordered input-ghost w-full" class="input input-sm input-bordered input-ghost w-full"
formControlName="otherName" formControlName="otherName"
/> />
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<label class="label" <label class="label"
><span class="lable-text text-base mr-3 w-max" ><span class="lable-text text-base mr-3 w-max"
>Description:</span >Description:</span
></label ></label
> >
<input <input
type="text" type="text"
class="input input-sm input-bordered input-ghost w-full" class="input input-sm input-bordered input-ghost w-full"
formControlName="description" formControlName="Description"
/> />
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<label class="label" <label class="label"
><span class="label-text text-base mr-3 w-max" ><span class="label-text text-base mr-3 w-max"
>Other Description:</span >Other Description:</span
></label ></label
> >
<input <input
type="text" type="text"
class="input input-sm input-bordered input-ghost w-full" class="input input-sm input-bordered input-ghost w-full"
formControlName="otherDescription" formControlName="otherDescription"
/> />
</div> </div>
</div> </div>
</div> </div>
<div <div
class="col-span-3 overflow-auto mb-4 rounded bg-white border border-gray-200 shadow" class="col-span-3 overflow-auto mb-4 rounded bg-white border border-gray-200 shadow"
> >
<app-recipe-list <app-recipe-list
[productCode]="productCode" [productCode]="productCode"
(recipeListFormChange)="onRecipeListFormChange($event)" (recipeListFormChange)="onRecipeListFormChange($event)"
></app-recipe-list> ></app-recipe-list>
</div> </div>
<div class="grid grid-cols-2 gap-4 mb-4"> <div class="grid grid-cols-2 gap-4 mb-4">
<div <div
class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800" class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
<div <div
class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800" class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
<div <div
class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800" class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
<div <div
class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800" class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
</div> </div>
<div <div
class="flex items-center justify-center h-48 mb-4 rounded bg-gray-50 dark:bg-gray-800" class="flex items-center justify-center h-48 mb-4 rounded bg-gray-50 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div <div
class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800" class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
<div <div
class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800" class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
<div <div
class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800" class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
<div <div
class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800" class="flex items-center justify-center rounded bg-gray-50 h-28 dark:bg-gray-800"
> >
<p class="text-2xl text-gray-400 dark:text-gray-500"> <p class="text-2xl text-gray-400 dark:text-gray-500">
<svg <svg
class="w-3.5 h-3.5" class="w-3.5 h-3.5"
aria-hidden="true" aria-hidden="true"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 18 18" viewBox="0 0 18 18"
> >
<path <path
stroke="currentColor" stroke="currentColor"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" stroke-width="2"
d="M9 1v16M1 9h16" d="M9 1v16M1 9h16"
/> />
</svg> </svg>
</p> </p>
</div> </div>
</div> </div>
<div <div
class="sticky bottom-0 col-span-3 flex justify-end bg-white rounded-full drop-shadow-2xl p-3" class="sticky bottom-0 col-span-3 flex justify-end bg-white rounded-full drop-shadow-2xl p-3"
> >
<button <!-- <div> Commit Message </div> -->
(click)="onPressConfirmClose()" <!-- <p class="text-2xl mr-8 text-gray-400 dark:text-gray-500">Commit Message</p> -->
class="btn btn-error w-36 text-white mr-2" <input type="text" name="commit-message" id="commit-message" placeholder="Commit Message" class="input input-bordered mr-8 w-full max-w-xs" [value]="productCode" (keyup)="onKeyUpCommitMsg($event)"/>
> <button
Cancel (click)="onPressConfirmClose()"
</button> class="btn btn-error w-36 text-white mr-2"
<button >
(click)="onPressConfirmSave()" Cancel
class="btn btn-success w-36 text-white" </button>
[disabled]="!isValueChanged" <button
> (click)="onPressConfirmSave()"
Save class="btn btn-success w-36 text-white"
</button> [disabled]="!isValueChanged"
</div> >
</form> Save
<ng-template #indicator> </button>
<div class="flex w-full h-full justify-center items-center"> </div>
<span class="loading loading-spinner w-20"></span> </form>
</div> <ng-template #indicator>
</ng-template> <div class="flex w-full h-full justify-center items-center">
<confirm-modal <span class="loading loading-spinner w-20"></span>
[title]="confirmSave.title" </div>
[message]="confirmSave.message" </ng-template>
[confirmCallBack]="confirmSave.confirmCallBack" <confirm-modal
[show]="showConfirmSaveModal" [title]="confirmSave.title"
></confirm-modal> [message]="confirmSave.message"
[confirmCallBack]="confirmSave.confirmCallBack"
<confirm-modal [show]="showConfirmSaveModal"
[title]="confirmClose.title" ></confirm-modal>
[message]="confirmClose.message"
[confirmCallBack]="confirmClose.confirmCallBack" <confirm-modal
[show]="showConfirmCloseModal" [title]="confirmClose.title"
></confirm-modal> [message]="confirmClose.message"
</div> [confirmCallBack]="confirmClose.confirmCallBack"
[show]="showConfirmCloseModal"
></confirm-modal>
</div>

View file

@ -1,158 +1,203 @@
import { CommonModule, DatePipe } from '@angular/common'; import { CommonModule, DatePipe } from '@angular/common';
import { Component, EventEmitter, OnInit } from '@angular/core'; import { Component, EventEmitter, OnInit } from '@angular/core';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Observable, first } from 'rxjs'; import { Observable, first } from 'rxjs';
import { RecipeService } from 'src/app/core/services/recipe.service'; import { RecipeService } from 'src/app/core/services/recipe.service';
import { ConfirmModal } from 'src/app/shared/modal/confirm/confirm-modal.component'; import { ConfirmModal } from 'src/app/shared/modal/confirm/confirm-modal.component';
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, style, transition, trigger } from '@angular/animations';
import { RecipeListComponent } from './recipe-list/recipe-list.component'; import { RecipeListComponent } from './recipe-list/recipe-list.component';
import { import {
RecipeDetail, RecipeDetail,
RecipeDetailMat, RecipeDetailMat,
} from 'src/app/core/models/recipe.model'; } from 'src/app/core/models/recipe.model';
import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord'; import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { UserService } from 'src/app/core/services/user.service';
@Component({
selector: 'app-recipe-details', @Component({
templateUrl: './recipe-details.component.html', selector: 'app-recipe-details',
standalone: true, templateUrl: './recipe-details.component.html',
imports: [ standalone: true,
CommonModule, imports: [
RouterLink, CommonModule,
ReactiveFormsModule, RouterLink,
ConfirmModal, ReactiveFormsModule,
DatePipe, ConfirmModal,
RecipeListComponent, DatePipe,
], RecipeListComponent,
animations: [ ],
trigger('inOutAnimation', [ animations: [
transition(':enter', [ trigger('inOutAnimation', [
style({ opacity: 0 }), transition(':enter', [
animate('1s ease-out', style({ opacity: 1 })), style({ opacity: 0 }),
]), animate('1s ease-out', style({ opacity: 1 })),
]), ]),
], ]),
}) ],
export class RecipeDetailsComponent implements OnInit { })
title: string = 'Recipe Detail'; export class RecipeDetailsComponent implements OnInit {
title: string = 'Recipe Detail';
recipeDetail$!: Observable<RecipeDetail>;
recipeDetail$!: Observable<RecipeDetail>;
isLoaded: boolean = false;
isMatLoaded: boolean = false; isLoaded: boolean = false;
isMatLoaded: boolean = false;
actionRecord: ActionRecord<RecipeDetail | RecipeDetailMat> =
new ActionRecord(); actionRecord: ActionRecord<RecipeDetail | RecipeDetailMat> =
new ActionRecord();
recipeOriginalDetail!: typeof this.recipeDetailForm.value;
recipeOriginalDetail!: typeof this.recipeDetailForm.value;
constructor(
private _formBuilder: FormBuilder, commit_msg :string = "";
private _route: ActivatedRoute,
private _router: Router, constructor(
private _recipeService: RecipeService private _formBuilder: FormBuilder,
) {} private _route: ActivatedRoute,
private _router: Router,
productCode!: string; private _recipeService: RecipeService,
private _userService: UserService
recipeDetailForm = this._formBuilder.group({ ) {}
productCode: '',
name: '', productCode!: string;
otherName: '',
description: '', recipeDetailForm = this._formBuilder.group({
otherDescription: '', productCode: '',
lastModified: new Date(), name: '',
price: 0, otherName: '',
isUse: false, Description: '',
isShow: false, otherDescription: '',
disable: false, lastModified: new Date(),
}); price: 0,
isUse: false,
ngOnInit() { isShow: false,
this.productCode = this._route.snapshot.params['productCode']; disable: false,
});
this.recipeDetail$ = this._recipeService
.getRecipeDetail(this.productCode) repl = []
.pipe(first());
this.recipeDetail$.subscribe((detail) => { ngOnInit() {
this.recipeDetailForm.patchValue(detail); this.productCode = this._route.snapshot.params['productCode'];
this.isLoaded = true;
this.recipeOriginalDetail = { ...this.recipeDetailForm.getRawValue() }; this.recipeDetail$ = this._recipeService
}); .getRecipeDetail(this.productCode)
.pipe(first());
this.recipeDetailForm.valueChanges.subscribe(this.onRecipeDetailFormChange); this.recipeDetail$.subscribe((detail) => {
// snap recipe detail form value console.log('Recipe Detail', detail);
this.actionRecord.registerOnAddAction((currAction, allAction) => { this.recipeDetailForm.patchValue(detail);
if (currAction.type === 'recipeListData') { this.isLoaded = true;
switch (currAction.action) { this.recipeOriginalDetail = { ...this.recipeDetailForm.getRawValue() };
case 'add': });
break;
case 'delete': this.recipeDetailForm.valueChanges.subscribe(this.onRecipeDetailFormChange);
break;
} // snap recipe detail form value
}
console.log('Action Record', allAction); this.actionRecord.registerOnAddAction((currAction, allAction) => {
}); if (currAction.type === 'recipeListData') {
} switch (currAction.action) {
case 'add':
showConfirmSaveModal: EventEmitter<boolean> = new EventEmitter<boolean>(); break;
showConfirmCloseModal: EventEmitter<boolean> = new EventEmitter<boolean>(); case 'delete':
break;
confirmSave = { }
title: 'The changes detected!', }
message: 'Do you want to save changes?', console.log('Action Record', allAction);
confirmCallBack: () => { });
console.log('confirm save'); }
// TODO: update value in targeted recipe
// this._recipeService.editChanges( showConfirmSaveModal: EventEmitter<boolean> = new EventEmitter<boolean>();
// this._recipeService.getCurrentCountry(), showConfirmCloseModal: EventEmitter<boolean> = new EventEmitter<boolean>();
// this._recipeService.getCurrentFile(),
// { confirmSave = {
// ...this.recipeDetail, title: 'The changes detected!',
// } message: 'Do you want to save changes?',
// ); confirmCallBack: () => {
console.log('Sending changes'); console.log('confirm save');
this._router.navigate(['/recipes']);
}, // get username
}; let username:string = ""
this._userService.getCurrentUser().subscribe((user) => {
confirmClose = { username = user.user.name;
title: 'The changes will be lost!',
message: 'Do you want to close without saving?',
confirmCallBack: () => {
console.log('confirm close'); let to_send = {
this._router.navigate(['/recipes']); edit_by: username,
}, commit_msg: this.commit_msg,
}; productCode: this.productCode,
name: this.recipeDetailForm.getRawValue().name,
onPressConfirmSave() { otherName: this.recipeDetailForm.getRawValue().otherName,
if (this.isValueChanged) { Description: this.recipeDetailForm.getRawValue().Description,
this.showConfirmSaveModal.emit(true); otherDescription: this.recipeDetailForm.getRawValue().otherDescription,
} else { LastChange: this.recipeDetailForm.getRawValue().lastModified,
this._router.navigate(['/recipes']); price: this.recipeDetailForm.getRawValue().price,
} // isUse: this,
} // isShow: null,
// disable: null,
onPressConfirmClose() { recipes: [
if (this.isValueChanged) { ...this.repl
this.showConfirmCloseModal.emit(true); ],
} else { }
this._router.navigate(['/recipes']);
}
} // TODO: update value in targeted recipe
this._recipeService.editChanges(
isValueChanged: boolean = false; this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile(),
onRecipeDetailFormChange(recipeDetail: typeof this.recipeDetailForm.value) { {
console.log('Recipe Detail Form Changed', recipeDetail); ...to_send,
} }
);
onRecipeListFormChange(isValueChanged: boolean) { console.log('Sending changes');
console.log('Recipe List Form Changed', isValueChanged); this._router.navigate(['/recipes']);
this.isValueChanged ||= isValueChanged; })
}
}
},
};
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;
}
}

View file

@ -1,57 +1,57 @@
<table class="table" [formGroup]="recipeListForm"> <table class="table" [formGroup]="recipeListForm">
<thead> <thead>
<tr class="bg-gray-200"> <tr class="bg-gray-200">
<th class="px-6 py-3">Is Use</th> <th class="px-6 py-3">Is Use</th>
<th class="px-6 py-3">Material ID</th> <th class="px-6 py-3">Material ID</th>
<th class="px-6 py-3">Material Name</th> <th class="px-6 py-3">Material Name</th>
<th class="px-6 py-3">MixOrder</th> <th class="px-6 py-3">MixOrder</th>
<th class="px-6 py-3">Stir Time</th> <th class="px-6 py-3">Stir Time</th>
<th class="px-6 py-3">Powder Gram</th> <th class="px-6 py-3">Powder Gram</th>
<th class="px-6 py-3">Powder Time</th> <th class="px-6 py-3">Powder Time</th>
<th class="px-6 py-3">Syrup Gram</th> <th class="px-6 py-3">Syrup Gram</th>
<th class="px-6 py-3">Syrup Time</th> <th class="px-6 py-3">Syrup Time</th>
<th class="px-6 py-3">Water Cold</th> <th class="px-6 py-3">Water Cold</th>
<th class="px-6 py-3">Water Yield</th> <th class="px-6 py-3">Water Yield</th>
</tr> </tr>
</thead> </thead>
<tbody <tbody
formArrayName="recipeListData" formArrayName="recipeListData"
*ngFor="let mat of recipeListData.controls; let i = index" *ngFor="let mat of recipeListData.controls; let i = index"
> >
<tr class="bg-white la border-b hover:bg-secondary" formGroupName="{{ i }}"> <tr class="bg-white la border-b hover:bg-secondary" formGroupName="{{ i }}">
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="checkbox" class="toggle" formControlName="isUse" /> <input type="checkbox" class="toggle" formControlName="isUse" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="materialID" /> <input type="text" class="input" formControlName="materialPathId" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="name" /> <input type="text" class="input" formControlName="name" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="mixOrder" /> <input type="text" class="input" formControlName="mixOrder" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="powderGram" /> <input type="text" class="input" formControlName="powderGram" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="powderTime" /> <input type="text" class="input" formControlName="powderTime" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="syrupGram" /> <input type="text" class="input" formControlName="syrupGram" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="syrupTime" /> <input type="text" class="input" formControlName="syrupTime" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="waterCold" /> <input type="text" class="input" formControlName="waterCold" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="waterYield" /> <input type="text" class="input" formControlName="waterYield" />
</td> </td>
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="stirTime" /> <input type="text" class="input" formControlName="stirTime" />
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -1,90 +1,102 @@
import { NgFor, NgIf } from '@angular/common'; import { NgFor, NgIf } from '@angular/common';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { import {
FormArray, FormArray,
FormBuilder, FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms'; } from '@angular/forms';
import { isEqual, sortBy } from 'lodash'; import { forEach, isEqual, sortBy } from 'lodash';
import { first } from 'rxjs'; import { first } from 'rxjs';
import { import {
RecipeDetail, RecipeDetail,
RecipeDetailMat, RecipeDetailMat,
} from 'src/app/core/models/recipe.model'; } from 'src/app/core/models/recipe.model';
import { RecipeService } from 'src/app/core/services/recipe.service'; import { RecipeService } from 'src/app/core/services/recipe.service';
import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord'; import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord';
@Component({ @Component({
selector: 'app-recipe-list', selector: 'app-recipe-list',
templateUrl: './recipe-list.component.html', templateUrl: './recipe-list.component.html',
standalone: true, standalone: true,
imports: [NgIf, NgFor, ReactiveFormsModule], imports: [NgIf, NgFor, ReactiveFormsModule],
}) })
export class RecipeListComponent implements OnInit { export class RecipeListComponent implements OnInit {
@Input({ required: true }) productCode!: string; @Input({ required: true }) productCode!: string;
@Output() recipeListFormChange = new EventEmitter<boolean>(); @Output() recipeListFormChange = new EventEmitter<unknown[]>();
isMatLoaded: boolean = false; isMatLoaded: boolean = false;
constructor( constructor(
private _recipeService: RecipeService, private _recipeService: RecipeService,
private _formBuilder: FormBuilder private _formBuilder: FormBuilder
) {} ) {}
recipeListForm = this._formBuilder.group( recipeListForm = this._formBuilder.group(
{ {
recipeListData: this._formBuilder.array([]), recipeListData: this._formBuilder.array([]),
}, },
{ updateOn: 'blur' } { updateOn: 'blur' }
); );
private _recipeListOriginalArray!: RecipeDetailMat[]; private _recipeListOriginalArray!: RecipeDetailMat[];
ngOnInit(): void { ngOnInit(): void {
this._recipeService this._recipeService
.getRecipeDetailMat(this.productCode) .getRecipeDetailMat(this.productCode)
.pipe(first()) .pipe(first())
.subscribe(({ result }) => { .subscribe(({ result }) => {
this._recipeListOriginalArray = result; this._recipeListOriginalArray = result;
result.forEach((recipeDetailMat: RecipeDetailMat) => { result.forEach((recipeDetailMat: RecipeDetailMat) => {
this.recipeListData.push( this.recipeListData.push(
this._formBuilder.group({ this._formBuilder.group({
isUse: recipeDetailMat.isUse, isUse: recipeDetailMat.isUse,
materialID: recipeDetailMat.materialID, materialPathId: recipeDetailMat.materialPathId,
name: [{ value: recipeDetailMat.name, disabled: true }], name: [{ value: recipeDetailMat.name, disabled: true }],
mixOrder: recipeDetailMat.mixOrder, mixOrder: recipeDetailMat.mixOrder,
stirTime: recipeDetailMat.stirTime, stirTime: recipeDetailMat.stirTime,
powderGram: recipeDetailMat.powderGram, powderGram: recipeDetailMat.powderGram,
powderTime: recipeDetailMat.powderTime, powderTime: recipeDetailMat.powderTime,
syrupGram: recipeDetailMat.syrupGram, syrupGram: recipeDetailMat.syrupGram,
syrupTime: recipeDetailMat.syrupTime, syrupTime: recipeDetailMat.syrupTime,
waterCold: recipeDetailMat.waterCold, waterCold: recipeDetailMat.waterCold,
waterYield: recipeDetailMat.waterYield, waterYield: recipeDetailMat.waterYield,
}) })
); );
}); });
this.isMatLoaded = true; this.isMatLoaded = true;
}); });
this.recipeListForm.valueChanges.subscribe((value) => { this.recipeListForm.valueChanges.subscribe((value) => {
console.log(value.recipeListData); console.log(value.recipeListData);
console.log(this._recipeListOriginalArray); console.log(this._recipeListOriginalArray);
if ( if (
!isEqual( !isEqual(
sortBy(value, 'materialID'), sortBy(value, 'materialID'),
sortBy(this._recipeListOriginalArray, 'materialID') sortBy(this._recipeListOriginalArray, 'materialID')
) )
) { ) {
this.recipeListFormChange.emit(true);
} else { let emitted_res: any[] = []
this.recipeListFormChange.emit(false);
} // force type change. temporary solution
}); forEach(value.recipeListData!, (recipeDetailMat: any) => {
} recipeDetailMat.materialPathId = parseInt(recipeDetailMat.materialPathId!)
emitted_res.push(recipeDetailMat)
get recipeListData(): FormArray { })
return this.recipeListForm.get('recipeListData') as FormArray;
} this.recipeListFormChange.emit(emitted_res as unknown[]);
} } else {
this.recipeListFormChange.emit([]);
}
});
}
get recipeListData(): FormArray {
return this.recipeListForm.get('recipeListData') as FormArray;
}
}

View file

@ -1,288 +1,312 @@
<main <main
class="relative overflow-auto max-h-[80%] h-[88vh] shadow-md sm:rounded-lg" class="relative overflow-auto max-h-[80%] h-[88vh] shadow-md sm:rounded-lg"
#table #table
> >
<table class="table"> <table class="table">
<caption class="p-5 text-lg font-semibold text-left text-gray-900"> <caption class="p-5 text-lg font-semibold text-left text-gray-900">
<div <div
class="divide-y divide-solid divide-gray-400" class="divide-y divide-solid divide-gray-400"
*ngIf="recipesDashboard$ | async as recipesDashboard; else loading" *ngIf="recipesDashboard$ | async as recipesDashboard; else loading"
> >
<div class="flex flex-row py-3 justify-between items-center"> <div class="flex flex-row py-3 justify-between items-center">
<div class="flex flex-col"> <div class="flex flex-col">
<span <span
>Recipe Version {{ recipesDashboard.configNumber }} | >Recipe Version {{ recipesDashboard.configNumber }} |
{{ recipesDashboard.filename }}</span {{ recipesDashboard.filename }}</span
> >
</div> </div>
<div class="flex flex-col ml-5"> <div class="flex flex-col ml-5">
<button <button
class="btn bg-primary btn-md" class="btn bg-primary btn-md"
onclick="select_file_modal.showModal()" onclick="select_file_modal.showModal()"
> >
<span class="text-base text-gray-700">เลือก Recipe ไฟล์</span> <span class="text-base text-gray-700">เลือก Recipe ไฟล์</span>
</button> </button>
<dialog id="select_file_modal" class="modal"> <dialog id="select_file_modal" class="modal">
<div <div
class="modal-box max-w-[600px] overflow-visible flex flex-col justify-center items-center gap-5" class="modal-box max-w-[600px] overflow-visible flex flex-col justify-center items-center gap-5"
> >
<h3 class="font-bold text-lg">Select Recipe File</h3> <h3 class="font-bold text-lg">Select Recipe File</h3>
<div class="flex flex-row gap-5"> <div class="flex flex-row gap-5">
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<input <input
#countryInput #countryInput
type="search" type="search"
tabindex="1" tabindex="1"
placeholder="Select Country" placeholder="Select Country"
class="input input-bordered input-sm w-full max-w-xs" class="input input-bordered input-sm w-full max-w-xs"
[value]="selectedCountry" [value]="selectedCountry"
(input)="setCountryFilter($event)" (input)="setCountryFilter($event)"
(focus)="getRecipeCountries()" (focus)="getRecipeCountries()"
/> />
<div <div
class="dropdown-content z-[1000] min-w-[200px] max-h-[500px] overflow-y-auto" class="dropdown-content z-[1000] min-w-[200px] max-h-[500px] overflow-y-auto"
> >
<ul <ul
tabindex="1" tabindex="1"
class="menu p-2 shadow bg-base-100 rounded-box w-auto" class="menu p-2 shadow bg-base-100 rounded-box w-auto"
> >
<li *ngFor="let country of recipeCountryFiltered"> <li *ngFor="let country of recipeCountryFiltered">
<a (click)="countrySelected(country)">{{ <a (click)="countrySelected(country)">{{
country country
}}</a> }}</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div *ngIf="isCountrySelected" class="dropdown dropdown-end"> <div *ngIf="isCountrySelected" class="dropdown dropdown-end">
<input <input
#fileInput #fileInput
type="search" type="search"
tabindex="1" tabindex="1"
placeholder="เลือก Recipe File" placeholder="เลือก Recipe File"
class="input input-bordered input-sm w-full max-w-xs" class="input input-bordered input-sm w-full max-w-xs"
(input)="setFileFilter($event)" (input)="setFileFilter($event)"
(focus)="getRecipeFiles()" (focus)="getRecipeFiles()"
/> />
<div <div
class="dropdown-content z-[1000] min-w-[200px] max-h-[500px] overflow-y-auto" class="dropdown-content z-[1000] min-w-[200px] max-h-[500px] overflow-y-auto"
> >
<ul <ul
tabindex="1" tabindex="1"
class="menu p-2 shadow bg-base-100 rounded-box w-auto" class="menu p-2 shadow bg-base-100 rounded-box w-auto"
> >
<li *ngFor="let file of recipeFileCountries"> <li *ngFor="let file of recipeFileCountries">
<a (click)="loadRecipe(file)">{{ file }}</a> <a (click)="loadRecipe(file)">{{ file }}</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<form method="dialog" class="modal-backdrop"> <form method="dialog" class="modal-backdrop">
<button>close</button> <button>close</button>
</form> </form>
</dialog> </dialog>
<!-- <div class="dropdown dropdown-end"> <!-- <div class="dropdown dropdown-end">
<input <input
type="text" type="text"
tabindex="0" tabindex="0"
placeholder="เลือก Recipe File" placeholder="เลือก Recipe File"
class="input input-bordered input-sm w-full max-w-xs" class="input input-bordered input-sm w-full max-w-xs"
(input)="setRecipeVersion($event)" (input)="setRecipeVersion($event)"
(focus)="getRecipeVersions()" (focus)="getRecipeVersions()"
/> />
<div class="dropdown-content z-[1] max-h-[500px] overflow-y-auto"> <div class="dropdown-content z-[1] max-h-[500px] overflow-y-auto">
<ul <ul
tabindex="0" tabindex="0"
class="menu p-2 shadow bg-base-100 rounded-box w-auto" class="menu p-2 shadow bg-base-100 rounded-box w-auto"
> >
<li *ngFor="let recipeVersion of recipeVersions"> <li *ngFor="let recipeVersion of recipeVersions">
<a (click)="loadRecipe(recipeVersion)">{{ <a (click)="loadRecipe(recipeVersion)">{{
recipeVersion recipeVersion
}}</a> }}</a>
</li> </li>
</ul> </ul>
</div> </div>
</div> --> </div> -->
</div> </div>
<div class="flex flex-col ml-auto">
<span class="" <button class="btn bg-primary btn-md border-2" *ngIf="savedTmpfiles.length > 0">
>Last Updated: <span class="text-base text-gray-700" onclick="select_savefile_modal.showModal()">โหลดเซฟ</span>
{{ </button>
recipesDashboard.configNumber | date : "dd-MMM-yyyy hh:mm:ss" <!-- todo: add modal -->
}}</span <dialog id="select_savefile_modal" class="modal">
> <div class="modal-box max-w-[600px] overflow-visible">
</div> <p class="font-bold text-lg m-2">Saved Files</p>
</div> <table class="table">
<div class="flex flex-col"> <tr class="bg-primary ">
<div class="relativ sm:rounded-lg"> <th>Name</th>
<div <th>Comment</th>
class="flex flex-col items-center justify-between p-4 space-y-3 md:flex-row md:space-y-0 md:space-x-4" <!-- <th></th> -->
> </tr>
<div class="join"> <tr class="row hover:bg-secondary" *ngFor="let file of savedTmpfiles">
<input <td>{{ file }}</td>
class="input input-bordered join-item w-[300px] bg-white" <td>"-"</td>
placeholder="Product Code, Name or Other Name" <button class="btn bg-blue-400">Select</button>
(input)="setSearch($event)" </tr>
(keydown.enter)="search($event)" </table>
/>
<ng-select </div>
[items]="materialList"
class="join-item text-base"
bindLabel="name" <form method="dialog" class="modal-backdrop">
bindValue="id" <button>close</button>
[multiple]="true" </form>
[closeOnSelect]="false" </dialog>
[(ngModel)]="selectMaterialFilter"
>
<ng-template ng-option-tmp let-item="item"> <div class="flex flex-col ml-auto">
<p class="text-xs">{{ item.name }}</p> <span class=""
<small class="text-xs text-gray-500">{{ item.id }}</small> >Last Updated:
</ng-template> {{
</ng-select> recipesDashboard.configNumber | date : "dd-MMM-yyyy hh:mm:ss"
<button class="btn join-item" (click)="search($event)"> }}</span
Search >
</button> </div>
</div> </div>
<div class="flex flex-col">
<div class="flex gap-2"> <div class="relativ sm:rounded-lg">
<button class="btn rounded-lg" (click)="openJsonTab()"> <div
View JSON class="flex flex-col items-center justify-between p-4 space-y-3 md:flex-row md:space-y-0 md:space-x-4"
</button> >
<button class="btn rounded-lg"> <div class="join">
<svg <input
class="w-6 h-6 text-gray-800 dark:text-white" class="input input-bordered join-item w-[300px] bg-white"
aria-hidden="true" placeholder="Product Code, Name or Other Name"
xmlns="http://www.w3.org/2000/svg" (input)="setSearch($event)"
fill="none" (keydown.enter)="search($event)"
viewBox="0 0 18 20" />
> <ng-select
<path [items]="materialList"
stroke="currentColor" class="join-item text-base"
stroke-linecap="round" bindLabel="name"
stroke-linejoin="round" bindValue="id"
stroke-width="1" [multiple]="true"
d="M15 6V2a.97.97 0 0 0-.933-1H5.828a2 2 0 0 0-1.414.586L1.586 4.414A2 2 0 0 0 1 5.828V18a.969.969 0 0 0 .933 1H14a1 1 0 0 0 1-1M6 1v4a1 1 0 0 1-1 1H1m6 6h9m-1.939-2.768L16.828 12l-2.767 2.768" [closeOnSelect]="false"
/> [(ngModel)]="selectMaterialFilter"
</svg> >
Export <ng-template ng-option-tmp let-item="item">
</button> <p class="text-xs">{{ item.name }}</p>
</div> <small class="text-xs text-gray-500">{{ item.id }}</small>
</div> </ng-template>
</div> </ng-select>
</div> <button class="btn join-item" (click)="search($event)">
</div> Search
</caption> </button>
<thead class="text-xs sticky top-0 text-gray-700 uppercase bg-secondary"> </div>
<tr>
<th> <div class="flex gap-2">
<label> <button class="btn rounded-lg" (click)="openJsonTab()">
<input type="checkbox" class="checkbox" /> View JSON
</label> </button>
</th> <button class="btn rounded-lg">
<th scope="col" class="px-6 py-3" *ngFor="let header of tableHeads"> <svg
<div class="flex items-center uppercase"> class="w-6 h-6 text-gray-800 dark:text-white"
{{ header }} aria-hidden="true"
<a href="#" xmlns="http://www.w3.org/2000/svg"
><svg fill="none"
class="w-3 h-3 ml-1.5" viewBox="0 0 18 20"
aria-hidden="true" >
xmlns="http://www.w3.org/2000/svg" <path
fill="currentColor" stroke="currentColor"
viewBox="0 0 24 24" stroke-linecap="round"
> stroke-linejoin="round"
<path stroke-width="1"
d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z" d="M15 6V2a.97.97 0 0 0-.933-1H5.828a2 2 0 0 0-1.414.586L1.586 4.414A2 2 0 0 0 1 5.828V18a.969.969 0 0 0 .933 1H14a1 1 0 0 0 1-1M6 1v4a1 1 0 0 1-1 1H1m6 6h9m-1.939-2.768L16.828 12l-2.767 2.768"
/> />
</svg> </svg>
</a> Export
</div> </button>
</th> <button class="btn rounded-lg"> Upgrade to {{ recipesDashboard.configNumber + 1 }}</button>
<th scope="col" class="px-6 py-3"></th> </div>
</tr> </div>
</thead> </div>
<tbody> </div>
<tr </div>
*ngFor="let recipe of recipeOverviewList" </caption>
class="bg-white la border-b hover:bg-secondary" <thead class="text-xs sticky top-0 text-gray-700 uppercase bg-secondary">
> <tr>
<th> <th>
<label> <label>
<input type="checkbox" class="checkbox" /> <input type="checkbox" class="checkbox" />
</label> </label>
</th> </th>
<td <th scope="col" class="px-6 py-3" *ngFor="let header of tableHeads">
scope="row" <div class="flex items-center uppercase">
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap" {{ header }}
> <a href="#"
{{ recipe.productCode }} ><svg
</td> class="w-3 h-3 ml-1.5"
<td aria-hidden="true"
class="px-6 max-w-fit py-4 font-medium text-gray-900 whitespace-nowrap" xmlns="http://www.w3.org/2000/svg"
> fill="currentColor"
{{ recipe.name }} viewBox="0 0 24 24"
</td> >
<td class="px-6 py-4">{{ recipe.otherName }}</td> <path
<td class="px-6 py-4 flex-wrap max-w-xs">{{ recipe.description }}</td> d="M8.574 11.024h6.852a2.075 2.075 0 0 0 1.847-1.086 1.9 1.9 0 0 0-.11-1.986L13.736 2.9a2.122 2.122 0 0 0-3.472 0L6.837 7.952a1.9 1.9 0 0 0-.11 1.986 2.074 2.074 0 0 0 1.847 1.086Zm6.852 1.952H8.574a2.072 2.072 0 0 0-1.847 1.087 1.9 1.9 0 0 0 .11 1.985l3.426 5.05a2.123 2.123 0 0 0 3.472 0l3.427-5.05a1.9 1.9 0 0 0 .11-1.985 2.074 2.074 0 0 0-1.846-1.087Z"
<td class="px-6 py-4"> />
{{ recipe.lastUpdated | date : "dd-MMM-yyyy hh:mm:ss" }} </svg>
</td> </a>
<td class="px-4 py-4 flex"> </div>
<!-- <recipe-modal productCode="{{ recipe.productCode }}"></recipe-modal> --> </th>
<a <th scope="col" class="px-6 py-3"></th>
routerLink="/recipe/{{ recipe.productCode }}" </tr>
class="btn btn-ghost" </thead>
> <tbody>
<svg <tr
class="w-6 h-6 text-gray-800 dark:text-white" *ngFor="let recipe of recipeOverviewList"
aria-hidden="true" class="bg-white la border-b hover:bg-secondary"
xmlns="http://www.w3.org/2000/svg" >
fill="none" <th>
viewBox="0 0 20 20" <label>
> <input type="checkbox" class="checkbox" />
<path </label>
stroke="currentColor" </th>
stroke-linecap="round" <td
stroke-linejoin="round" scope="row"
stroke-width="1" class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"
d="M7.75 4H19M7.75 4a2.25 2.25 0 0 1-4.5 0m4.5 0a2.25 2.25 0 0 0-4.5 0M1 4h2.25m13.5 6H19m-2.25 0a2.25 2.25 0 0 1-4.5 0m4.5 0a2.25 2.25 0 0 0-4.5 0M1 10h11.25m-4.5 6H19M7.75 16a2.25 2.25 0 0 1-4.5 0m4.5 0a2.25 2.25 0 0 0-4.5 0M1 16h2.25" >
/> {{ recipe.productCode }}
</svg> </td>
</a> <td
</td> class="px-6 max-w-fit py-4 font-medium text-gray-900 whitespace-nowrap"
</tr> >
</tbody> {{ recipe.name }}
</table> </td>
<ng-template #loading> <td class="px-6 py-4">{{ recipe.otherName }}</td>
<div <td class="px-6 py-4 flex-wrap max-w-xs">{{ recipe.description }}</td>
class="flex w-full items-center justify-center h-56 border border-gray-200 rounded-lg bg-gray-50" <td class="px-6 py-4">
> {{ recipe.lastUpdated | date : "dd-MMM-yyyy hh:mm:ss" }}
<div role="status"> </td>
<svg <td class="px-4 py-4 flex">
aria-hidden="true" <!-- <recipe-modal productCode="{{ recipe.productCode }}"></recipe-modal> -->
class="w-8 h-8 mr-2 text-gray-200 animate-spin fill-amber-600" <a
viewBox="0 0 100 101" routerLink="/recipe/{{ recipe.productCode }}"
fill="none" class="btn btn-ghost"
xmlns="http://www.w3.org/2000/svg" >
> <svg
<path class="w-6 h-6 text-gray-800 dark:text-white"
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" aria-hidden="true"
fill="currentColor" xmlns="http://www.w3.org/2000/svg"
/> fill="none"
<path viewBox="0 0 20 20"
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" >
fill="currentFill" <path
/> stroke="currentColor"
</svg> stroke-linecap="round"
</div> stroke-linejoin="round"
</div> stroke-width="1"
</ng-template> d="M7.75 4H19M7.75 4a2.25 2.25 0 0 1-4.5 0m4.5 0a2.25 2.25 0 0 0-4.5 0M1 4h2.25m13.5 6H19m-2.25 0a2.25 2.25 0 0 1-4.5 0m4.5 0a2.25 2.25 0 0 0-4.5 0M1 10h11.25m-4.5 6H19M7.75 16a2.25 2.25 0 0 1-4.5 0m4.5 0a2.25 2.25 0 0 0-4.5 0M1 16h2.25"
/>
<button </svg>
class="btn btn-circle fixed z-100 bottom-5 right-1" </a>
(click)="scrollToTop()" </td>
> </tr>
^ </tbody>
</button> </table>
</main> <ng-template #loading>
<div
class="flex w-full items-center justify-center h-56 border border-gray-200 rounded-lg bg-gray-50"
>
<div role="status">
<svg
aria-hidden="true"
class="w-8 h-8 mr-2 text-gray-200 animate-spin fill-amber-600"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
</ng-template>
</main>

View file

@ -1,335 +1,356 @@
import { import {
Component, Component,
ElementRef, ElementRef,
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common'; import { CommonModule, DatePipe } from '@angular/common';
import { import {
Recipe, Recipe,
Recipe01, Recipe01,
RecipeOverview, RecipeOverview,
RecipesDashboard, RecipesDashboard,
} from 'src/app/core/models/recipe.model'; } from 'src/app/core/models/recipe.model';
import { RecipeService } from 'src/app/core/services/recipe.service'; import { RecipeService } from 'src/app/core/services/recipe.service';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { RecipeModalComponent } from 'src/app/shared/modal/recipe-details/recipe-modal.component'; import { RecipeModalComponent } from 'src/app/shared/modal/recipe-details/recipe-modal.component';
import { import {
BehaviorSubject, BehaviorSubject,
Observable, Observable,
Subscription, Subscription,
finalize, finalize,
map, map,
tap, tap,
} from 'rxjs'; } from 'rxjs';
import * as lodash from 'lodash'; import * as lodash from 'lodash';
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { NgSelectModule } from '@ng-select/ng-select'; import { NgSelectModule } from '@ng-select/ng-select';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { MaterialService } from 'src/app/core/services/material.service'; import { MaterialService } from 'src/app/core/services/material.service';
@Component({ @Component({
selector: 'app-recipes', selector: 'app-recipes',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
RouterLink, RouterLink,
DatePipe, DatePipe,
RecipeModalComponent, RecipeModalComponent,
NgSelectModule, NgSelectModule,
FormsModule, FormsModule,
], ],
templateUrl: './recipes.component.html', templateUrl: './recipes.component.html',
}) })
export class RecipesComponent implements OnInit, OnDestroy { export class RecipesComponent implements OnInit, OnDestroy {
recipesDashboard$!: Observable<RecipesDashboard>; recipesDashboard$!: Observable<RecipesDashboard>;
recipeOverviewList!: RecipeOverview[]; recipeOverviewList!: RecipeOverview[];
selectMaterialFilter: number[] | null = null; selectMaterialFilter: number[] | null = null;
materialList: { id: number; name: string | number }[] | null = null; materialList: { id: number; name: string | number }[] | null = null;
tableHeads: string[] = [ tableHeads: string[] = [
'Product Code', 'Product Code',
'Name', 'Name',
'Other Name', 'Other Name',
'Description', 'Description',
'Last Updated', 'Last Updated',
]; ];
private offset = 0; private offset = 0;
private take = 20; private take = 20;
// isLoaded: boolean = false; // isLoaded: boolean = false;
isLoadMore: boolean = true; isLoadMore: boolean = true;
isHasMore: boolean = true; isHasMore: boolean = true;
private searchStr = ''; private searchStr = '';
private oldSearchStr = ''; private oldSearchStr = '';
tableCtx?: ElementRef; savedTmpfiles: string[] = [];
@ViewChild('table', { static: false }) set content(table: ElementRef) { tableCtx?: ElementRef;
// expose element ref for other fn
this.tableCtx = table; @ViewChild('table', { static: false }) set content(table: ElementRef) {
// expose element ref for other fn
table.nativeElement.addEventListener( this.tableCtx = table;
'scroll',
() => { table.nativeElement.addEventListener(
if (this.isHasMore === false) { 'scroll',
return; () => {
} if (this.isHasMore === false) {
return;
const { scrollTop, scrollHeight, clientHeight } = table.nativeElement; }
const isBottom = scrollTop + clientHeight >= scrollHeight - 10;
if (isBottom && !this.isLoadMore) { const { scrollTop, scrollHeight, clientHeight } = table.nativeElement;
this.isLoadMore = true; const isBottom = scrollTop + clientHeight >= scrollHeight - 10;
this._recipeService if (isBottom && !this.isLoadMore) {
.getRecipeOverview({ this.isLoadMore = true;
offset: this.offset, this._recipeService
take: this.take, .getRecipeOverview({
search: this.oldSearchStr, offset: this.offset,
filename: this._recipeService.getCurrentFile(), take: this.take,
country: this._recipeService.getCurrentCountry(), search: this.oldSearchStr,
materialIds: this.selectMaterialFilter || [], filename: this._recipeService.getCurrentFile(),
}) country: this._recipeService.getCurrentCountry(),
.subscribe(({ result, hasMore, totalCount }) => { materialIds: this.selectMaterialFilter || [],
if (this.recipeOverviewList) { })
this.recipeOverviewList = .subscribe(({ result, hasMore, totalCount }) => {
this.recipeOverviewList.concat(result); if (this.recipeOverviewList) {
} else { this.recipeOverviewList =
this.recipeOverviewList = result; this.recipeOverviewList.concat(result);
} } else {
this.offset += 10; this.recipeOverviewList = result;
this.isHasMore = hasMore; }
this.isLoadMore = false; this.offset += 10;
}); this.isHasMore = hasMore;
} this.isLoadMore = false;
}, });
{ passive: true } }
); },
} { passive: true }
);
constructor( }
private _recipeService: RecipeService,
private _materialService: MaterialService constructor(
) {} private _recipeService: RecipeService,
private _materialService: MaterialService
ngOnInit(): void { ) {}
this.recipesDashboard$ = this._recipeService
.getRecipesDashboard({ ngOnInit(): void {
filename: this._recipeService.getCurrentFile(), this.recipesDashboard$ = this._recipeService
country: this._recipeService.getCurrentCountry(), .getRecipesDashboard({
}) filename: this._recipeService.getCurrentFile(),
.pipe( country: this._recipeService.getCurrentCountry(),
finalize(() => { })
this._recipeService .pipe(
.getRecipeOverview({ finalize(() => {
offset: this.offset, this._recipeService
take: this.take, .getRecipeOverview({
search: this.oldSearchStr, offset: this.offset,
filename: this._recipeService.getCurrentFile(), take: this.take,
country: this._recipeService.getCurrentCountry(), search: this.oldSearchStr,
materialIds: this.selectMaterialFilter || [], filename: this._recipeService.getCurrentFile(),
}) country: this._recipeService.getCurrentCountry(),
.subscribe(({ result, hasMore, totalCount }) => { materialIds: this.selectMaterialFilter || [],
this.recipeOverviewList = result; })
this.offset += 10; .subscribe(({ result, hasMore, totalCount }) => {
this.isHasMore = hasMore; this.recipeOverviewList = result;
this.isLoadMore = false; this.offset += 10;
}); this.isHasMore = hasMore;
}) this.isLoadMore = false;
); });
})
this._materialService );
.getMaterialCodes()
.pipe( this._recipeService.getSavedTmp(
map((mat) => this._recipeService.getCurrentCountry(),
mat.map((m) => ({ this._recipeService.getCurrentFile()
id: m.materialID, ).subscribe({
name: m.PackageDescription || m.materialID, 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);
.subscribe((materials) => { this.savedTmpfiles = files.files;
this.materialList = materials; }
});
},
this.initRecipeSelection(); })
}
this._materialService
setSearch(event: Event) { .getMaterialCodes()
this.searchStr = (event.target as HTMLInputElement).value; .pipe(
} map((mat) =>
mat.map((m) => ({
search(event: Event) { id: m.materialID,
this.offset = 0; name: m.PackageDescription || m.materialID,
this.isLoadMore = true; }))
this.oldSearchStr = this.searchStr; )
this._recipeService )
.getRecipeOverview({ .subscribe((materials) => {
offset: this.offset, this.materialList = materials;
take: this.take, });
search: this.oldSearchStr,
filename: this._recipeService.getCurrentFile(), this.initRecipeSelection();
country: this._recipeService.getCurrentCountry(), }
materialIds: this.selectMaterialFilter || [],
}) setSearch(event: Event) {
.subscribe(({ result, hasMore, totalCount }) => { this.searchStr = (event.target as HTMLInputElement).value;
this.recipeOverviewList = result; }
this.offset += 10;
this.isHasMore = hasMore; search(event: Event) {
this.isLoadMore = false; this.offset = 0;
}); this.isLoadMore = true;
} this.oldSearchStr = this.searchStr;
this._recipeService
// Recipe Version selection .getRecipeOverview({
currentCountryFilter: BehaviorSubject<string> = new BehaviorSubject<string>( offset: this.offset,
'' take: this.take,
); search: this.oldSearchStr,
currentFileFilter: BehaviorSubject<string> = new BehaviorSubject<string>(''); filename: this._recipeService.getCurrentFile(),
recipeCountryFiltered: string[] = []; country: this._recipeService.getCurrentCountry(),
recipeFileCountries: string[] = []; materialIds: this.selectMaterialFilter || [],
})
currentCountryFilterSubScription: Subscription | null = null; .subscribe(({ result, hasMore, totalCount }) => {
currentFileFilterSubScription: Subscription | null = null; this.recipeOverviewList = result;
this.offset += 10;
selectedCountry: string | null = null; this.isHasMore = hasMore;
isCountrySelected: boolean = false; this.isLoadMore = false;
});
private countryInputEl?: ElementRef<HTMLInputElement>; }
private fileInputEl?: ElementRef<HTMLInputElement>;
// Recipe Version selection
@ViewChild('countryInput', { static: false }) set countryInput( currentCountryFilter: BehaviorSubject<string> = new BehaviorSubject<string>(
countryInput: ElementRef<HTMLInputElement> ''
) { );
this.countryInputEl = countryInput; currentFileFilter: BehaviorSubject<string> = new BehaviorSubject<string>('');
} recipeCountryFiltered: string[] = [];
recipeFileCountries: string[] = [];
@ViewChild('fileInput', { static: false }) set fileInput(
fileInput: ElementRef<HTMLInputElement> currentCountryFilterSubScription: Subscription | null = null;
) { currentFileFilterSubScription: Subscription | null = null;
this.fileInputEl = fileInput;
} selectedCountry: string | null = null;
isCountrySelected: boolean = false;
private firstTimeOpenModal = true;
private countryInputEl?: ElementRef<HTMLInputElement>;
initRecipeSelection() { private fileInputEl?: ElementRef<HTMLInputElement>;
if (this._recipeService.getRecipeFileCountries().length == 0) {
this._recipeService.getRecipeCountries().subscribe((countries) => { @ViewChild('countryInput', { static: false }) set countryInput(
this.recipeCountryFiltered = countries; countryInput: ElementRef<HTMLInputElement>
}); ) {
} this.countryInputEl = countryInput;
} }
setCountryFilter(event: Event) { @ViewChild('fileInput', { static: false }) set fileInput(
this.currentCountryFilter.next((event.target as HTMLInputElement).value); fileInput: ElementRef<HTMLInputElement>
} ) {
this.fileInputEl = fileInput;
setFileFilter(event: Event) { }
this.currentFileFilter.next((event.target as HTMLInputElement).value);
} private firstTimeOpenModal = true;
getRecipeCountries() { initRecipeSelection() {
if (this.firstTimeOpenModal) { if (this._recipeService.getRecipeFileCountries().length == 0) {
this.countryInputEl!.nativeElement.blur(); this._recipeService.getRecipeCountries().subscribe((countries) => {
this.firstTimeOpenModal = false; this.recipeCountryFiltered = countries;
} });
this.currentCountryFilterSubScription = this.currentCountryFilter.subscribe( }
(c) => { }
const countries = this._recipeService.getRecipeFileCountries();
if (countries.length > 0) { setCountryFilter(event: Event) {
this.recipeCountryFiltered = lodash.filter(countries, (country) => this.currentCountryFilter.next((event.target as HTMLInputElement).value);
country.toLowerCase().includes(c.toLowerCase()) }
);
} setFileFilter(event: Event) {
} this.currentFileFilter.next((event.target as HTMLInputElement).value);
); }
}
getRecipeCountries() {
getRecipeFiles() { if (this.firstTimeOpenModal) {
this.currentFileFilterSubScription = this.currentFileFilter.subscribe( this.countryInputEl!.nativeElement.blur();
(c) => { this.firstTimeOpenModal = false;
if (this.selectedCountry === null) { }
return; this.currentCountryFilterSubScription = this.currentCountryFilter.subscribe(
} (c) => {
const countries = this._recipeService.getRecipeFileCountries();
const fileNames = this._recipeService.getRecipeFileNames( if (countries.length > 0) {
this.selectedCountry this.recipeCountryFiltered = lodash.filter(countries, (country) =>
); country.toLowerCase().includes(c.toLowerCase())
if (fileNames && fileNames.length > 0) { );
this.recipeFileCountries = lodash.filter(fileNames, (file) => }
file.toLowerCase().includes(c.toLowerCase()) }
); );
} else { }
this._recipeService
.getRecipeFiles(this.selectedCountry) getRecipeFiles() {
.subscribe((files) => { this.currentFileFilterSubScription = this.currentFileFilter.subscribe(
this.recipeFileCountries = lodash.filter(files, (file) => (c) => {
file.toLowerCase().includes(c.toLowerCase()) if (this.selectedCountry === null) {
); return;
}); }
}
console.log(this.recipeFileCountries); const fileNames = this._recipeService.getRecipeFileNames(
} this.selectedCountry
); );
} if (fileNames && fileNames.length > 0) {
this.recipeFileCountries = lodash.filter(fileNames, (file) =>
countrySelected(country: string) { file.toLowerCase().includes(c.toLowerCase())
this.selectedCountry = country; );
this.isCountrySelected = true; } else {
localStorage.setItem('currentRecipeCountry', country); this._recipeService
} .getRecipeFiles(this.selectedCountry)
.subscribe((files) => {
loadRecipe(recipeFileName: string) { this.recipeFileCountries = lodash.filter(files, (file) =>
// clear all recipes file.toLowerCase().includes(c.toLowerCase())
this.offset = 0; );
this.isHasMore = true; });
this.isLoadMore = true; }
this.oldSearchStr = ''; console.log(this.recipeFileCountries);
localStorage.setItem('currentRecipeFile', recipeFileName); }
);
this.recipesDashboard$ = this._recipeService.getRecipesDashboard({ }
filename: recipeFileName,
country: this.selectedCountry!, countrySelected(country: string) {
}); this.selectedCountry = country;
this.isCountrySelected = true;
this._recipeService localStorage.setItem('currentRecipeCountry', country);
.getRecipeOverview({ }
offset: this.offset,
take: this.take, loadRecipe(recipeFileName: string) {
search: this.oldSearchStr, // clear all recipes
filename: recipeFileName, this.offset = 0;
country: this.selectedCountry!, this.isHasMore = true;
materialIds: this.selectMaterialFilter || [], this.isLoadMore = true;
}) this.oldSearchStr = '';
.subscribe(({ result, hasMore, totalCount }) => { localStorage.setItem('currentRecipeFile', recipeFileName);
this.recipeOverviewList = result;
this.offset += 10; this.recipesDashboard$ = this._recipeService.getRecipesDashboard({
this.isHasMore = hasMore; filename: recipeFileName,
this.isLoadMore = false; country: this.selectedCountry!,
}); });
}
this._recipeService
// end of Recipe Version selection .getRecipeOverview({
offset: this.offset,
openJsonTab() { take: this.take,
window.open( search: this.oldSearchStr,
environment.api + filename: recipeFileName,
`/recipes/${this._recipeService.getCurrentCountry()}/${this._recipeService.getCurrentFile()}/json`, country: this.selectedCountry!,
'_blank' materialIds: this.selectMaterialFilter || [],
); })
} .subscribe(({ result, hasMore, totalCount }) => {
this.recipeOverviewList = result;
scrollToTop() { this.offset += 10;
const table = this.tableCtx!.nativeElement; this.isHasMore = hasMore;
table.scrollTo({ top: 0, behavior: 'smooth' }); this.isLoadMore = false;
} });
}
ngOnDestroy(): void {
if (this.currentCountryFilterSubScription) { // end of Recipe Version selection
this.currentCountryFilterSubScription.unsubscribe();
} openJsonTab() {
if (this.currentFileFilterSubScription) { window.open(
this.currentFileFilterSubScription.unsubscribe(); 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();
}
}
}

82
server/data/commit.go Normal file
View file

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

View file

@ -1,58 +1,180 @@
package helpers package helpers
import "fmt" import (
"fmt"
// DynamicCompare compares two values dynamically and returns true if they are equal. "os"
// "recipe-manager/services/logger"
// Parameters: "strconv"
// "strings"
// - s: The first value to compare.
// "go.uber.org/zap"
// - u: The second value to compare. )
//
// Returns: var (
// Log = logger.GetInstance()
// - bool: True if the values are equal, false otherwise. )
//
// - error: An error if the values cannot be compared. // DynamicCompare compares two values dynamically and returns true if they are equal.
func DynamicCompare(s interface{}, u interface{}) (bool, error) { //
switch t := s.(type) { // Parameters:
case bool: //
u, ok := u.(bool) // - s: The first value to compare.
if !ok { //
return false, fmt.Errorf("[bool] cannot compare %T and %T, %v and %v", t, u, s, u) // - u: The second value to compare.
} //
return t == u, nil // Returns:
case string: //
u, ok := u.(string) // - bool: True if the values are equal, false otherwise.
if !ok { //
return false, fmt.Errorf("[string] cannot compare %T and %T, %v and %v", t, u, s, u) // - error: An error if the values cannot be compared.
} func DynamicCompare(s interface{}, u interface{}) (bool, error) {
return t == u, nil switch t := s.(type) {
case int: case bool:
u, ok := u.(int) u, ok := u.(bool)
if !ok { if !ok {
return false, fmt.Errorf("[int] cannot compare %T and %T, %v and %v", t, u, s, u) return false, fmt.Errorf("[bool] cannot compare %T and %T, %v and %v", t, u, s, u)
} }
return t == u, nil return t == u, nil
case float64: case string:
u, ok := u.(float64) u, ok := u.(string)
if !ok { if !ok {
return false, fmt.Errorf("[float64] cannot compare %T and %T, %v and %v", t, u, s, u) return false, fmt.Errorf("[string] cannot compare %T and %T, %v and %v", t, u, s, u)
} }
return t == u, nil return t == u, nil
case nil: case int:
return t == u, nil u, ok := u.(int)
case []interface{}: if !ok {
for i := range t { return false, fmt.Errorf("[int] cannot compare %T and %T, %v and %v", t, u, s, u)
if ok, err := DynamicCompare(t[i], u); err != nil { }
return false, err return t == u, nil
} else if !ok { case float64:
return false, nil u, ok := u.(float64)
} // Log.Debug("[helpers] DynamicCompare", zap.Any("u", u), zap.Any("ok", ok), zap.Any("test_compare*(t==u)", t == u))
}
default: if t == u {
return false, fmt.Errorf("[unknown] not in case. Cannot compare %T and %T, %v and %v", t, u, s, u) return t == u, nil
} }
return false, fmt.Errorf("[unknown] unexpected error")
} 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
// }

View file

@ -1,159 +1,160 @@
package models package models
import "encoding/json" import "encoding/json"
type Recipe struct { type Recipe struct {
Timestamp string `json:"Timestamp"` Timestamp string `json:"Timestamp"`
MachineSetting MachineSetting `json:"MachineSetting"` MachineSetting MachineSetting `json:"MachineSetting"`
Recipe01 []Recipe01 `json:"Recipe01"` Recipe01 []Recipe01 `json:"Recipe01"`
Topping Topping `json:"Topping"` Topping Topping `json:"Topping"`
MaterialCode []MaterialCode `json:"MaterialCode"` MaterialCode []MaterialCode `json:"MaterialCode"`
MaterialSetting []MaterialSetting `json:"MaterialSetting"` MaterialSetting []MaterialSetting `json:"MaterialSetting"`
} }
type MachineSetting struct { type MachineSetting struct {
RecipeTag string `json:"RecipeTag"` RecipeTag string `json:"RecipeTag"`
StrTextShowError []string `json:"strTextShowError"` StrTextShowError []string `json:"strTextShowError"`
ConfigNumber int `json:"configNumber"` ConfigNumber int `json:"configNumber"`
TemperatureMax int `json:"temperatureMax"` Comment []string `json:"Comment"`
TemperatureMin int `json:"temperatureMin"` TemperatureMax int `json:"temperatureMax"`
} TemperatureMin int `json:"temperatureMin"`
}
type MaterialCode struct {
PackageDescription string `json:"PackageDescription"` type MaterialCode struct {
RefillValuePerStep int `json:"RefillValuePerStep"` PackageDescription string `json:"PackageDescription"`
MaterialID uint64 `json:"materialID"` RefillValuePerStep int `json:"RefillValuePerStep"`
MaterialCode string `json:"materialCode"` MaterialID uint64 `json:"materialID"`
} MaterialCode string `json:"materialCode"`
}
type MaterialSetting struct {
AlarmIDWhenOffline int `json:"AlarmIDWhenOffline"` type MaterialSetting struct {
BeanChannel bool `json:"BeanChannel"` AlarmIDWhenOffline int `json:"AlarmIDWhenOffline"`
CanisterType string `json:"CanisterType"` BeanChannel bool `json:"BeanChannel"`
DrainTimer int `json:"DrainTimer"` CanisterType string `json:"CanisterType"`
IsEquipment bool `json:"IsEquipment"` DrainTimer int `json:"DrainTimer"`
LeavesChannel bool `json:"LeavesChannel"` IsEquipment bool `json:"IsEquipment"`
LowToOffline int `json:"LowToOffline"` LeavesChannel bool `json:"LeavesChannel"`
MaterialStatus int `json:"MaterialStatus"` LowToOffline int `json:"LowToOffline"`
PowderChannel bool `json:"PowderChannel"` MaterialStatus int `json:"MaterialStatus"`
RefillUnitGram bool `json:"RefillUnitGram"` PowderChannel bool `json:"PowderChannel"`
RefillUnitMilliliters bool `json:"RefillUnitMilliliters"` RefillUnitGram bool `json:"RefillUnitGram"`
RefillUnitPCS bool `json:"RefillUnitPCS"` RefillUnitMilliliters bool `json:"RefillUnitMilliliters"`
ScheduleDrainType int `json:"ScheduleDrainType"` RefillUnitPCS bool `json:"RefillUnitPCS"`
SodaChannel bool `json:"SodaChannel"` ScheduleDrainType int `json:"ScheduleDrainType"`
StockAdjust int `json:"StockAdjust"` SodaChannel bool `json:"SodaChannel"`
SyrupChannel bool `json:"SyrupChannel"` StockAdjust int `json:"StockAdjust"`
ID uint64 `json:"id"` SyrupChannel bool `json:"SyrupChannel"`
IDAlternate int `json:"idAlternate"` ID uint64 `json:"id"`
IsUse bool `json:"isUse"` IDAlternate int `json:"idAlternate"`
PayRettryMaxCount int `json:"pay_rettry_max_count"` IsUse bool `json:"isUse"`
FeedMode string `json:"feed_mode"` PayRettryMaxCount int `json:"pay_rettry_max_count"`
MaterialParameter string `json:"MaterialParameter"` FeedMode string `json:"feed_mode"`
} MaterialParameter string `json:"MaterialParameter"`
}
type Recipe01 struct {
Description string `json:"Description"` type Recipe01 struct {
ExtendID int `json:"ExtendID"` Description string `json:"Description"`
OnTOP bool `json:"OnTOP"` ExtendID int `json:"ExtendID"`
LastChange string `json:"LastChange"` OnTOP bool `json:"OnTOP"`
MenuStatus int `json:"MenuStatus"` LastChange string `json:"LastChange"`
RemainingCups json.Number `json:"RemainingCups"` MenuStatus int `json:"MenuStatus"`
StringParam string `json:"StringParam"` RemainingCups json.Number `json:"RemainingCups"`
TextForWarningBeforePay []string `json:"TextForWarningBeforePay"` StringParam string `json:"StringParam"`
CashPrice int `json:"cashPrice"` TextForWarningBeforePay []string `json:"TextForWarningBeforePay"`
Changerecipe string `json:"changerecipe"` CashPrice int `json:"cashPrice"`
Disable bool `json:"disable"` Changerecipe string `json:"changerecipe"`
Disable_by_cup bool `json:"disable_by_cup"` Disable bool `json:"disable"`
Disable_by_ice bool `json:"disable_by_ice"` Disable_by_cup bool `json:"disable_by_cup"`
EncoderCount int `json:"EncoderCount"` Disable_by_ice bool `json:"disable_by_ice"`
ID int `json:"id"` EncoderCount int `json:"EncoderCount"`
IsUse bool `json:"isUse"` ID int `json:"id"`
IsShow bool `json:"isShow"` IsUse bool `json:"isUse"`
Name string `json:"name"` IsShow bool `json:"isShow"`
NonCashPrice int `json:"nonCashPrice"` Name string `json:"name"`
OtherDescription string `json:"otherDescription"` NonCashPrice int `json:"nonCashPrice"`
OtherName string `json:"otherName"` OtherDescription string `json:"otherDescription"`
ProductCode string `json:"productCode"` OtherName string `json:"otherName"`
Recipes []MatRecipe `json:"recipes"` ProductCode string `json:"productCode"`
SubMenu []Recipe01 `json:"SubMenu"` Recipes []MatRecipe `json:"recipes"`
ToppingSet []ToppingSet `json:"ToppingSet"` SubMenu []Recipe01 `json:"SubMenu"`
Total_time int `json:"total_time"` ToppingSet []ToppingSet `json:"ToppingSet"`
Total_weight int `json:"total_weight"` Total_time int `json:"total_time"`
UriData string `json:"uriData"` Total_weight int `json:"total_weight"`
UseGram bool `json:"useGram"` UriData string `json:"uriData"`
Weight_float int `json:"weight_float"` UseGram bool `json:"useGram"`
} Weight_float int `json:"weight_float"`
}
func (r *Recipe01) ToMap() map[string]interface{} {
var m map[string]interface{} func (r *Recipe01) ToMap() map[string]interface{} {
recipeRecord, _ := json.Marshal(r) var m map[string]interface{}
json.Unmarshal(recipeRecord, &m) recipeRecord, _ := json.Marshal(r)
return m json.Unmarshal(recipeRecord, &m)
} return m
}
func (r *Recipe01) FromMap(m map[string]interface{}) Recipe01 {
recipeRecord, _ := json.Marshal(m) func (r *Recipe01) FromMap(m map[string]interface{}) Recipe01 {
json.Unmarshal(recipeRecord, &r) recipeRecord, _ := json.Marshal(m)
return *r json.Unmarshal(recipeRecord, &r)
} return *r
}
type MatRecipe struct {
MixOrder int `json:"MixOrder"` type MatRecipe struct {
FeedParameter int `json:"FeedParameter"` MixOrder int `json:"MixOrder"`
FeedPattern int `json:"FeedPattern"` FeedParameter int `json:"FeedParameter"`
IsUse bool `json:"isUse"` FeedPattern int `json:"FeedPattern"`
MaterialPathId int `json:"materialPathId"` IsUse bool `json:"isUse"`
PowderGram int `json:"powderGram"` MaterialPathId int `json:"materialPathId"`
PowderTime int `json:"powderTime"` PowderGram int `json:"powderGram"`
StirTime int `json:"stirTime"` PowderTime int `json:"powderTime"`
SyrupGram int `json:"syrupGram"` StirTime int `json:"stirTime"`
SyrupTime int `json:"syrupTime"` SyrupGram int `json:"syrupGram"`
WaterCold int `json:"waterCold"` SyrupTime int `json:"syrupTime"`
WaterYield int `json:"waterYield"` WaterCold int `json:"waterCold"`
} WaterYield int `json:"waterYield"`
}
type ToppingSet struct {
ListGroupID []int `json:"ListGroupID"` type ToppingSet struct {
DefaultIDSelect int `json:"defaultIDSelect"` ListGroupID []int `json:"ListGroupID"`
GroupID string `json:"groupID"` DefaultIDSelect int `json:"defaultIDSelect"`
IsUse bool `json:"isUse"` GroupID string `json:"groupID"`
} IsUse bool `json:"isUse"`
}
type Topping struct {
ToppingGroup []ToppingGroup `json:"ToppingGroup"` type Topping struct {
ToppingList []ToppingList `json:"ToppingList"` ToppingGroup []ToppingGroup `json:"ToppingGroup"`
} ToppingList []ToppingList `json:"ToppingList"`
}
type ToppingGroup struct {
Desc string `json:"Desc"` type ToppingGroup struct {
GroupID int `json:"groupID"` Desc string `json:"Desc"`
IDDefault int `json:"idDefault"` GroupID int `json:"groupID"`
IDInGroup string `json:"idInGroup"` IDDefault int `json:"idDefault"`
InUse bool `json:"inUse"` IDInGroup string `json:"idInGroup"`
Name string `json:"name"` InUse bool `json:"inUse"`
OtherName string `json:"otherName"` Name string `json:"name"`
} OtherName string `json:"otherName"`
}
type ToppingList struct {
ExtendID int `json:"ExtendID"` type ToppingList struct {
OnTOP bool `json:"OnTOP"` ExtendID int `json:"ExtendID"`
MenuStatus int `json:"MenuStatus"` OnTOP bool `json:"OnTOP"`
CashPrice int `json:"cashPrice"` MenuStatus int `json:"MenuStatus"`
Disable bool `json:"disable"` CashPrice int `json:"cashPrice"`
Disable_by_cup bool `json:"disable_by_cup"` Disable bool `json:"disable"`
Disable_by_ice bool `json:"disable_by_ice"` Disable_by_cup bool `json:"disable_by_cup"`
EncoderCount int `json:"EncoderCount"` Disable_by_ice bool `json:"disable_by_ice"`
ID int `json:"id"` EncoderCount int `json:"EncoderCount"`
IsUse bool `json:"isUse"` ID int `json:"id"`
IsShow bool `json:"isShow"` IsUse bool `json:"isUse"`
StringParam string `json:"stringParam"` IsShow bool `json:"isShow"`
Name string `json:"name"` StringParam string `json:"stringParam"`
NonCashPrice int `json:"nonCashPrice"` Name string `json:"name"`
OtherName string `json:"otherName"` NonCashPrice int `json:"nonCashPrice"`
ProductCode string `json:"productCode"` OtherName string `json:"otherName"`
Recipes []MatRecipe `json:"recipes"` ProductCode string `json:"productCode"`
Total_time int `json:"total_time"` Recipes []MatRecipe `json:"recipes"`
Total_weight int `json:"total_weight"` Total_time int `json:"total_time"`
UseGram bool `json:"useGram"` Total_weight int `json:"total_weight"`
Weight_float int `json:"weight_float"` UseGram bool `json:"useGram"`
} Weight_float int `json:"weight_float"`
}

View file

@ -1,326 +1,400 @@
package routers package routers
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path" "path"
"recipe-manager/contracts" "recipe-manager/contracts"
"recipe-manager/data" "recipe-manager/data"
"recipe-manager/helpers" "recipe-manager/helpers"
"recipe-manager/models" "recipe-manager/models"
"recipe-manager/services/logger" "recipe-manager/services/logger"
"recipe-manager/services/recipe" "recipe-manager/services/recipe"
"recipe-manager/services/sheet" "recipe-manager/services/sheet"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/go-chi/chi/v5"
"go.uber.org/zap" "github.com/go-chi/chi/v5"
) "go.uber.org/zap"
)
type RecipeRouter struct {
data *data.Data type RecipeRouter struct {
sheetService sheet.SheetService data *data.Data
recipeService recipe.RecipeService sheetService sheet.SheetService
} recipeService recipe.RecipeService
}
var (
Log = logger.GetInstance() var (
) Log = logger.GetInstance()
)
func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter {
return &RecipeRouter{ func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter {
data: data, return &RecipeRouter{
recipeService: recipeService, data: data,
sheetService: sheetService, recipeService: recipeService,
} sheetService: sheetService,
} }
}
func (rr *RecipeRouter) Route(r chi.Router) {
r.Route("/recipes", func(r chi.Router) { func (rr *RecipeRouter) Route(r chi.Router) {
r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) { r.Route("/recipes", func(r chi.Router) {
w.Header().Add("Content-Type", "application/json") 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") country := r.URL.Query().Get("country")
filename := r.URL.Query().Get("filename")
result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{
Country: country, result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{
Filename: filename, Country: country,
}) Filename: filename,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound) if err != nil {
return http.Error(w, err.Error(), http.StatusNotFound)
} return
}
json.NewEncoder(w).Encode(result)
}) json.NewEncoder(w).Encode(result)
})
r.Get("/overview", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") r.Get("/overview", func(w http.ResponseWriter, r *http.Request) {
var take, offset uint64 = 10, 0 w.Header().Add("Content-Type", "application/json")
if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil { var take, offset uint64 = 10, 0
offset = newOffset 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 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") country := r.URL.Query().Get("country")
materialIds := r.URL.Query().Get("materialIds") filename := r.URL.Query().Get("filename")
materialIds := r.URL.Query().Get("materialIds")
var materialIdsUint []int
for _, v := range strings.Split(materialIds, ",") { var materialIdsUint []int
materialIdUint, err := strconv.ParseUint(v, 10, 64) for _, v := range strings.Split(materialIds, ",") {
if err != nil || materialIdUint == 0 { materialIdUint, err := strconv.ParseUint(v, 10, 64)
continue if err != nil || materialIdUint == 0 {
} continue
materialIdsUint = append(materialIdsUint, int(materialIdUint)) }
} materialIdsUint = append(materialIdsUint, int(materialIdUint))
}
result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{
Take: int(take), result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{
Skip: int(offset), Take: int(take),
Search: r.URL.Query().Get("search"), Skip: int(offset),
Country: country, Search: r.URL.Query().Get("search"),
Filename: filename, Country: country,
MatIds: materialIdsUint, Filename: filename,
}) MatIds: materialIdsUint,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound) if err != nil {
return http.Error(w, err.Error(), http.StatusNotFound)
} return
}
json.NewEncoder(w).Encode(result)
}) json.NewEncoder(w).Encode(result)
})
r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) {
productCode := chi.URLParam(r, "product_code") 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") // recipe := rr.data.GetRecipe01()
// recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
// var recipeResult *models.Recipe01
// recipeMetaDataResult := map[string]string{} // var recipeResult *models.Recipe01
// recipeMetaDataResult := map[string]string{}
// for _, v := range recipe {
// if v.ProductCode == productCode { // for _, v := range recipe {
// recipeResult = &v // if v.ProductCode == productCode {
// break // recipeResult = &v
// } // break
// } // }
// }
// for _, v := range recipeMetaData {
// if v[0].(string) == productCode { // for _, v := range recipeMetaData {
// recipeMetaDataResult = map[string]string{ // if v[0].(string) == productCode {
// "productCode": v[0].(string), // recipeMetaDataResult = map[string]string{
// "name": v[1].(string), // "productCode": v[0].(string),
// "otherName": v[2].(string), // "name": v[1].(string),
// "description": v[3].(string), // "otherName": v[2].(string),
// "otherDescription": v[4].(string), // "description": v[3].(string),
// "picture": v[5].(string), // "otherDescription": v[4].(string),
// } // "picture": v[5].(string),
// break // }
// } // break
// } // }
// }
// if recipeResult == nil {
// http.Error(w, "Not Found", http.StatusNotFound) // if recipeResult == nil {
// return // http.Error(w, "Not Found", http.StatusNotFound)
// } // return
// }
// json.NewEncoder(w).Encode(map[string]interface{}{
// "recipe": recipeResult, // json.NewEncoder(w).Encode(map[string]interface{}{
// "recipeMetaData": recipeMetaDataResult, // "recipe": recipeResult,
// }) // "recipeMetaData": recipeMetaDataResult,
// })
result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{
Filename: r.URL.Query().Get("filename"), result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{
Country: r.URL.Query().Get("country"), Filename: r.URL.Query().Get("filename"),
ProductCode: productCode, Country: r.URL.Query().Get("country"),
}) ProductCode: productCode,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound) if err != nil {
return http.Error(w, err.Error(), http.StatusNotFound)
} return
}
json.NewEncoder(w).Encode(result)
}) json.NewEncoder(w).Encode(result)
})
r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) {
productCode := chi.URLParam(r, "product_code") 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"), result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{
Country: r.URL.Query().Get("country"), Filename: r.URL.Query().Get("filename"),
ProductCode: productCode, Country: r.URL.Query().Get("country"),
}) ProductCode: productCode,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound) if err != nil {
return http.Error(w, err.Error(), http.StatusNotFound)
} return
}
json.NewEncoder(w).Encode(result)
}) json.NewEncoder(w).Encode(result)
})
r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) {
country := chi.URLParam(r, "country") r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) {
filename := chi.URLParam(r, "filename") country := chi.URLParam(r, "country")
filename := chi.URLParam(r, "filename")
w.Header().Add("Content-Type", "application/json")
countryID, err := rr.data.GetCountryIDByName(country) w.Header().Add("Content-Type", "application/json")
if err != nil { countryID, err := rr.data.GetCountryIDByName(country)
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound) if err != nil {
return http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
} return
json.NewEncoder(w).Encode(rr.data.GetRecipe(countryID, filename)) }
}) 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") r.Get("/versions", func(w http.ResponseWriter, r *http.Request) {
// get key from map w.Header().Add("Content-Type", "application/json")
keys := []string{} // get key from map
for k := range rr.data.AllRecipeFiles { keys := []string{}
countryName, err := rr.data.GetCountryNameByID(k) for k := range rr.data.AllRecipeFiles {
if err != nil { countryName, err := rr.data.GetCountryNameByID(k)
continue if err != nil {
} continue
keys = append(keys, countryName) }
} keys = append(keys, countryName)
json.NewEncoder(w).Encode(keys) }
}) json.NewEncoder(w).Encode(keys)
})
r.Get("/versions/{country}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") 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) countryName := chi.URLParam(r, "country")
if err != nil { countryID, err := rr.data.GetCountryIDByName(countryName)
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", countryName), http.StatusNotFound) if err != nil {
return http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", countryName), http.StatusNotFound)
} return
files := []string{} }
for _, v := range rr.data.AllRecipeFiles[countryID] { files := []string{}
files = append(files, v.Name) for _, v := range rr.data.AllRecipeFiles[countryID] {
} files = append(files, v.Name)
json.NewEncoder(w).Encode(files) }
}) json.NewEncoder(w).Encode(files)
})
r.Get("/test/sheet", func(w http.ResponseWriter, r *http.Request) {
result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE") r.Get("/test/sheet", func(w http.ResponseWriter, r *http.Request) {
result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
mapResult := []map[string]string{}
mapResult := []map[string]string{}
for _, v := range result {
mapResult = append(mapResult, map[string]string{ for _, v := range result {
"productCode": v[0].(string), mapResult = append(mapResult, map[string]string{
"name": v[1].(string), "productCode": v[0].(string),
"otherName": v[2].(string), "name": v[1].(string),
"description": v[3].(string), "otherName": v[2].(string),
"otherDescription": v[4].(string), "Description": v[3].(string),
"picture": v[5].(string), "otherDescription": v[4].(string),
}) "picture": v[5].(string),
} })
json.NewEncoder(w).Encode(mapResult) }
}) 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)) r.Post("/edit/{country}/{filename}", func(w http.ResponseWriter, r *http.Request) {
filename := chi.URLParam(r, "filename") Log.Debug("Edit: ", zap.String("path", r.RequestURI))
country := chi.URLParam(r, "country") filename := chi.URLParam(r, "filename")
country := chi.URLParam(r, "country")
countryID, err := rr.data.GetCountryIDByName(country)
if err != nil { countryID, err := rr.data.GetCountryIDByName(country)
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound) if err != nil {
return http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
} return
}
target_recipe := rr.data.GetRecipe(countryID, filename)
target_recipe := rr.data.GetRecipe(countryID, filename)
Log.Debug("Target => ", zap.Any("target", target_recipe.MachineSetting.ConfigNumber))
Log.Debug("Target => ", zap.Any("target", target_recipe.MachineSetting.ConfigNumber))
// check request structure
// check request structure
// FIXME: Request structure bug. Case-sensitive, likely bug at client
// uncomment the below code to view the bug // Body
var ch_map map[string]interface{}
// var change_request map[string]interface{} var changes models.Recipe01
// err = json.NewDecoder(r.Body).Decode(&change_request)
// if err != nil { err = json.NewDecoder(r.Body).Decode(&ch_map)
// Log.Error("Decode in request failed: ", zap.Error(err)) if err != nil {
// } Log.Error("Decode in request failed: ", zap.Error(err))
// Log.Debug("Request => ", zap.Any("request", change_request)) }
// Body // commit request
var changes models.Recipe01 editor := ch_map["edit_by"].(string)
err = json.NewDecoder(r.Body).Decode(&changes) Log.Debug("requester", zap.Any("editor", editor))
if err != nil { commit_msg := ch_map["commit_msg"].(string)
Log.Error("Decode in request failed: ", zap.Error(err)) 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 Log.Debug("Changes: ", zap.Any("changes", changes))
target_menu, err := rr.data.GetRecipe01ByProductCode(filename, countryID, changes.ProductCode) // 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)) if err != nil {
return Log.Error("Error when get recipe by product code", zap.Error(err))
} return
}
menu_map := target_menu.ToMap()
change_map := changes.ToMap() menu_map := target_menu.ToMap()
change_map := changes.ToMap()
// Find changes
for key, val := range menu_map { // Find changes
for key, val := range menu_map {
test_bool, err := helpers.DynamicCompare(val, change_map[key])
test_bool, err := helpers.DynamicCompare(val, change_map[key])
if err != nil {
Log.Error("DynamicCompare in request failed: ", zap.Error(err)) if err != nil {
} Log.Error("DynamicCompare in request failed: ", zap.Error(err))
}
if !test_bool {
menu_map[key] = change_map[key] if !test_bool {
} menu_map[key] = change_map[key]
} }
}
// Apply changes
tempRecipe := models.Recipe01{} // Apply changes
tempRecipe = tempRecipe.FromMap(menu_map) tempRecipe := models.Recipe01{}
rr.data.SetValuesToRecipe(tempRecipe) tempRecipe = tempRecipe.FromMap(menu_map)
Log.Debug("ApplyChange", zap.Any("status", "passed")) 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))) // 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 { // target saved filename
Log.Error("Error when tried to create file", zap.Error(err)) saved_filename := path.Join("./cofffeemachineConfig", countryID, filename)
return
} // store @ temporary file
temp_file_name := helpers.GetTempFile(saved_filename, editor, 0)
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ") // TODO: push this change, editor, commit_msg into db
err = encoder.Encode(rr.data.GetRecipe(countryID, filename))
// gen hash
if err != nil { commit_hash, err := data.HashCommit(8)
Log.Error("Error when write file", zap.Error(err))
} if err != nil {
Log.Error("Error when hash commit", zap.Error(err))
w.Header().Add("Content-Type", "application/json") return
json.NewEncoder(w).Encode(map[string]interface{}{ }
"status": "OK",
}) 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))
})
})
}

View file

@ -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))
}
}
}
}

View file

@ -1,192 +1,204 @@
package recipe package recipe
import ( import (
"fmt" "fmt"
"recipe-manager/contracts" "recipe-manager/contracts"
"recipe-manager/data" "recipe-manager/data"
"recipe-manager/models" "recipe-manager/models"
"sort" "recipe-manager/services/logger"
"strings" "sort"
) "strings"
type RecipeService interface { "go.uber.org/zap"
GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error) )
GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error)
var (
GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) Log = logger.GetInstance()
GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) )
}
type RecipeService interface {
type recipeService struct { GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error)
db *data.Data GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error)
}
GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error)
// GetRecipeDetail implements RecipeService. GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error)
func (rs *recipeService) GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) { }
recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode) type recipeService struct {
db *data.Data
if err != nil { }
return contracts.RecipeDetailResponse{}, err
} // GetRecipeDetail implements RecipeService.
func (rs *recipeService) GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) {
result := contracts.RecipeDetailResponse{
Name: recipe.Name, Log.Debug("GetRecipeDetail", zap.Any("request", request))
OtherName: recipe.OtherName,
Description: recipe.Description, recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode)
OtherDescription: recipe.OtherDescription,
LastUpdated: recipe.LastChange, if err != nil {
Picture: recipe.UriData[len("img="):], // remove "img=" prefix return contracts.RecipeDetailResponse{}, err
} }
return result, nil // DEBUG: picture
} Log.Debug("GetRecipeDetail", zap.String("picture", recipe.UriData))
// GetRecipeDetailMat implements RecipeService. result := contracts.RecipeDetailResponse{
func (rs *recipeService) GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) { Name: recipe.Name,
countryID, err := rs.db.GetCountryIDByName(request.Country) OtherName: recipe.OtherName,
Description: recipe.Description,
if err != nil { OtherDescription: recipe.OtherDescription,
return contracts.RecipeDetailMatListResponse{}, fmt.Errorf("country name: %s not found", request.Country) LastUpdated: recipe.LastChange,
} Picture: recipe.UriData[len("img="):], // remove "img=" prefix
}
recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode)
return result, nil
if err != nil { }
return contracts.RecipeDetailMatListResponse{}, err
} // GetRecipeDetailMat implements RecipeService.
func (rs *recipeService) GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) {
matIds := []uint64{} countryID, err := rs.db.GetCountryIDByName(request.Country)
for _, v := range recipe.Recipes {
if v.IsUse { if err != nil {
matIds = append(matIds, uint64(v.MaterialPathId)) return contracts.RecipeDetailMatListResponse{}, fmt.Errorf("country name: %s not found", request.Country)
} }
}
recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode)
matsCode := rs.db.GetMaterialCode(matIds, countryID, request.Filename)
if err != nil {
result := contracts.RecipeDetailMatListResponse{ return contracts.RecipeDetailMatListResponse{}, err
Result: []contracts.RecipeDetailMat{}, }
}
matIds := []uint64{}
for _, v := range recipe.Recipes { for _, v := range recipe.Recipes {
for _, mat := range matsCode { if v.IsUse {
if v.MaterialPathId == int(mat.MaterialID) { matIds = append(matIds, uint64(v.MaterialPathId))
result.Result = append(result.Result, contracts.RecipeDetailMat{ }
IsUse: v.IsUse, }
MaterialID: mat.MaterialID,
Name: mat.PackageDescription, matsCode := rs.db.GetMaterialCode(matIds, countryID, request.Filename)
MixOrder: v.MixOrder,
FeedParameter: v.FeedParameter, result := contracts.RecipeDetailMatListResponse{
FeedPattern: v.FeedPattern, Result: []contracts.RecipeDetailMat{},
MaterialPathId: v.MaterialPathId, }
PowderGram: v.PowderGram,
PowderTime: v.PowderTime, for _, v := range recipe.Recipes {
StirTime: v.StirTime, for _, mat := range matsCode {
SyrupGram: v.SyrupGram, if v.MaterialPathId == int(mat.MaterialID) {
SyrupTime: v.SyrupTime, result.Result = append(result.Result, contracts.RecipeDetailMat{
WaterCold: v.WaterCold, IsUse: v.IsUse,
WaterYield: v.WaterYield, MaterialID: mat.MaterialID,
}) Name: mat.PackageDescription,
break MixOrder: v.MixOrder,
} FeedParameter: v.FeedParameter,
} FeedPattern: v.FeedPattern,
} MaterialPathId: v.MaterialPathId,
PowderGram: v.PowderGram,
// sort by id PowderTime: v.PowderTime,
sort.Slice(result.Result, func(i, j int) bool { StirTime: v.StirTime,
return result.Result[i].MaterialID < result.Result[j].MaterialID SyrupGram: v.SyrupGram,
}) SyrupTime: v.SyrupTime,
WaterCold: v.WaterCold,
return result, nil WaterYield: v.WaterYield,
} })
break
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) // sort by id
} // sort.Slice(result.Result, func(i, j int) bool {
// return result.Result[i].MaterialID < result.Result[j].MaterialID
recipe := rs.db.GetRecipe(countryID, request.Filename) // })
result := contracts.RecipeDashboardResponse{ return result, nil
ConfigNumber: recipe.MachineSetting.ConfigNumber, }
LastUpdated: recipe.Timestamp,
Filename: request.Filename, func (rs *recipeService) GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error) {
} countryID, err := rs.db.GetCountryIDByName(request.Country)
return result, nil if err != nil {
} return contracts.RecipeDashboardResponse{}, fmt.Errorf("country name: %s not found", request.Country)
}
func (rs *recipeService) GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error) {
countryID, err := rs.db.GetCountryIDByName(request.Country) recipe := rs.db.GetRecipe(countryID, request.Filename)
if err != nil { result := contracts.RecipeDashboardResponse{
return contracts.RecipeOverviewResponse{}, fmt.Errorf("country name: %s not found", request.Country) ConfigNumber: recipe.MachineSetting.ConfigNumber,
} LastUpdated: recipe.Timestamp,
recipe := rs.db.GetRecipe(countryID, request.Filename) Filename: request.Filename,
recipeFilter := recipe.Recipe01 }
result := contracts.RecipeOverviewResponse{} return result, nil
}
if request.Search != "" {
searchResult := []models.Recipe01{} func (rs *recipeService) GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error) {
for _, v := range recipeFilter { countryID, err := rs.db.GetCountryIDByName(request.Country)
if strings.Contains(strings.ToLower(v.ProductCode), strings.ToLower(request.Search)) ||
strings.Contains(strings.ToLower(v.Name), strings.ToLower(request.Search)) || if err != nil {
strings.Contains(strings.ToLower(v.OtherName), strings.ToLower(request.Search)) { return contracts.RecipeOverviewResponse{}, fmt.Errorf("country name: %s not found", request.Country)
searchResult = append(searchResult, v) }
} recipe := rs.db.GetRecipe(countryID, request.Filename)
} recipeFilter := recipe.Recipe01
recipeFilter = searchResult
} result := contracts.RecipeOverviewResponse{}
if len(request.MatIds) > 0 { if request.Search != "" {
matIdsFiltered := []models.Recipe01{} searchResult := []models.Recipe01{}
for _, v := range recipeFilter { for _, v := range recipeFilter {
for _, matID := range request.MatIds { if strings.Contains(strings.ToLower(v.ProductCode), strings.ToLower(request.Search)) ||
for _, recipe := range v.Recipes { strings.Contains(strings.ToLower(v.Name), strings.ToLower(request.Search)) ||
if recipe.IsUse && recipe.MaterialPathId == matID { strings.Contains(strings.ToLower(v.OtherName), strings.ToLower(request.Search)) {
matIdsFiltered = append(matIdsFiltered, v) searchResult = append(searchResult, v)
} }
} }
} recipeFilter = searchResult
} }
recipeFilter = matIdsFiltered
} if len(request.MatIds) > 0 {
matIdsFiltered := []models.Recipe01{}
// Map to contracts.RecipeOverview for _, v := range recipeFilter {
for _, v := range recipeFilter { for _, matID := range request.MatIds {
result.Result = append(result.Result, contracts.RecipeOverview{ for _, recipe := range v.Recipes {
ID: v.ID, if recipe.IsUse && recipe.MaterialPathId == matID {
ProductCode: v.ProductCode, matIdsFiltered = append(matIdsFiltered, v)
Name: v.Name, }
OtherName: v.OtherName, }
Description: v.Description, }
LastUpdated: v.LastChange, }
}) recipeFilter = matIdsFiltered
} }
result.TotalCount = len(result.Result) // Map to contracts.RecipeOverview
for _, v := range recipeFilter {
result.HasMore = result.TotalCount >= request.Take+request.Skip result.Result = append(result.Result, contracts.RecipeOverview{
if result.HasMore { ID: v.ID,
result.Result = result.Result[request.Skip : request.Take+request.Skip] ProductCode: v.ProductCode,
sort.Slice(result.Result, func(i, j int) bool { Name: v.Name,
return result.Result[i].ID < result.Result[j].ID OtherName: v.OtherName,
}) Description: v.Description,
} else if result.TotalCount > request.Skip { LastUpdated: v.LastChange,
result.Result = result.Result[request.Skip:] })
} else { }
result.Result = []contracts.RecipeOverview{}
} result.TotalCount = len(result.Result)
return result, nil result.HasMore = result.TotalCount >= request.Take+request.Skip
} if result.HasMore {
result.Result = result.Result[request.Skip : request.Take+request.Skip]
func NewRecipeService(db *data.Data) RecipeService { sort.Slice(result.Result, func(i, j int) bool {
return &recipeService{ return result.Result[i].ID < result.Result[j].ID
db: db, })
} } 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,
}
}