update recipe detail and recipe detail list

This commit is contained in:
Kenta420 2023-11-24 17:47:44 +07:00
parent 8b45ed53ee
commit d52cad09fd
16 changed files with 947 additions and 458 deletions

View file

@ -1,3 +1,50 @@
export type RecipeOverview = {
id: string;
productCode: string;
name: string;
otherName: string;
description: string;
lastUpdated: Date;
};
export type RecipesDashboard = {
configNumber: number;
LastUpdated: Date;
filename: string;
};
export type RecipeOverviewList = {
result: RecipeOverview[];
hasMore: boolean;
totalCount: number;
};
export type RecipeDetail = {
name: string;
otherName: string;
description: string;
otherDescription: string;
lastUpdated: Date;
picture: string;
};
export type RecipeDetailMat = {
materialID: number;
name: string;
mixOrder: number;
feedParameter: number;
feedPattern: number;
isUse: boolean;
materialPathId: number;
powderGram: number;
powderTime: number;
stirTime: number;
syrupGram: number;
syrupTime: number;
waterCold: number;
waterYield: number;
};
export interface Recipe { export interface Recipe {
Timestamp: Date; Timestamp: Date;
MachineSetting: MachineSetting; MachineSetting: MachineSetting;

View file

@ -1,18 +1,31 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, tap } from 'rxjs'; import { Observable, tap } from 'rxjs';
import { Recipe, Recipe01 } from '../models/recipe.model'; import {
Recipe,
Recipe01,
RecipeDetail,
RecipeDetailMat,
RecipeOverview,
RecipeOverviewList,
RecipesDashboard,
} from '../models/recipe.model';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { RecipeMetaData } from 'src/app/shared/types/recipe'; import { RecipeMetaData } from 'src/app/shared/types/recipe';
interface RecipeParams { type RecipeOverviewParams = {
filename: string; filename: string;
country: string; country: string;
materialIds: number[]; materialIds: number[];
offset: number; offset: number;
take: number; take: number;
search: string; search: string;
} };
type RecipeDashboardParams = {
filename: string;
country: string;
};
interface RecipeFiles { interface RecipeFiles {
[key: string]: string[]; [key: string]: string[];
@ -25,36 +38,80 @@ export class RecipeService {
constructor(private _httpClient: HttpClient) {} constructor(private _httpClient: HttpClient) {}
getRecipes( getRecipesDashboard(
params: RecipeParams = { params: RecipeDashboardParams = {
take: 10, country: this.getCurrentCountry(),
offset: 0, filename: this.getCurrentFile(),
search: '', }
): Observable<RecipesDashboard> {
return this._httpClient.get<RecipesDashboard>(
environment.api + '/recipes/dashboard',
{
params: {
country: params.country,
filename: params.filename,
},
withCredentials: true,
responseType: 'json',
}
);
}
getRecipeOverview(
params: RecipeOverviewParams = {
country: this.getCurrentCountry(), country: this.getCurrentCountry(),
filename: this.getCurrentFile(), filename: this.getCurrentFile(),
materialIds: [], materialIds: [],
offset: 0,
take: 20,
search: '',
} }
): Observable<{ ): Observable<RecipeOverviewList> {
fileName: string; return this._httpClient.get<RecipeOverviewList>(
recipes: Recipe; environment.api + '/recipes/overview',
hasMore: boolean; {
}> { params: {
return this._httpClient.get<{ country: params.country,
fileName: string; filename: params.filename,
recipes: Recipe; materialIds: params.materialIds.join(','),
hasMore: boolean; offset: params.offset.toString(),
}>(environment.api + '/recipes', { take: params.take.toString(),
params: { search: params.search,
offset: params.offset, },
take: params.take, withCredentials: true,
search: params.search, responseType: 'json',
country: params.country, }
filename: params.filename, );
material_ids: params.materialIds.join(','), }
},
withCredentials: true, getRecipeDetail(productCode: string): Observable<RecipeDetail> {
responseType: 'json', return this._httpClient.get<RecipeDetail>(
}); environment.api + '/recipes/' + productCode,
{
params: {
filename: this.getCurrentFile(),
country: this.getCurrentCountry(),
},
withCredentials: true,
responseType: 'json',
}
);
}
getRecipeDetailMat(
productCode: string
): Observable<{ result: RecipeDetailMat[] }> {
return this._httpClient.get<{ result: RecipeDetailMat[] }>(
environment.api + '/recipes/' + productCode + '/mat',
{
params: {
filename: this.getCurrentFile(),
country: this.getCurrentCountry(),
},
withCredentials: true,
responseType: 'json',
}
);
} }
getCurrentFile(): string { getCurrentFile(): string {

View file

@ -1,16 +1,16 @@
<div class="p-4"> <div class="p-4">
<form class="grid grid-cols-3 gap-4 mb-4" [formGroup]="recipeDetail"> <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">
{{ recipeDetail.value.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">
{{ recipeDetail.value.otherName }} {{ recipeDetailForm.getRawValue().otherName }}
</h5> </h5>
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
@ -18,7 +18,8 @@
<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">
{{ {{
recipeDetail.value.lastModified | date : "dd/MM/yyyy HH:mm:ss" recipeDetailForm.getRawValue().lastModified
| date : "dd/MM/yyyy HH:mm:ss"
}} }}
</p> </p>
</div> </div>
@ -109,11 +110,13 @@
</div> </div>
</div> </div>
<div <div
class="col-span-3 min-h-[500px] max-h-[500px] 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
[matRecipeList]="materialListIds$" [parentForm]="recipeDetailForm"
[parentForm]="recipeDetail" [productCode]="productCode"
[actionRecord]="actionRecord"
[recipeDetailOriginal]="recipeOriginalDetail"
></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">

View file

@ -1,32 +1,25 @@
import { DatePipe, NgFor, NgIf } from '@angular/common'; import { CommonModule, DatePipe } from '@angular/common';
import { Component, EventEmitter, OnInit } from '@angular/core'; import { Component, EventEmitter, OnInit } from '@angular/core';
import { import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
FormArray,
FormControl,
FormGroup,
ReactiveFormsModule,
} from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { isEqual } from 'lodash'; import { Observable, first } from 'rxjs';
import { BehaviorSubject, Subject, finalize, map } 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 { MaterialService } from 'src/app/core/services/material.service'; import { RecipeListComponent } from './recipe-list/recipe-list.component';
import { RecipeMetaData, RecipeDetail } from 'src/app/shared/types/recipe';
import { import {
RecipeListComponent, RecipeDetail,
RecipeListDataFormGroup, RecipeDetailMat,
} from './recipe-list/recipe-list.component'; } from 'src/app/core/models/recipe.model';
import { MatRecipe } from 'src/app/core/models/recipe.model'; import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord';
import { isEqual } from 'lodash';
@Component({ @Component({
selector: 'app-recipe-details', selector: 'app-recipe-details',
templateUrl: './recipe-details.component.html', templateUrl: './recipe-details.component.html',
standalone: true, standalone: true,
imports: [ imports: [
NgIf, CommonModule,
NgFor,
RouterLink, RouterLink,
ReactiveFormsModule, ReactiveFormsModule,
ConfirmModal, ConfirmModal,
@ -44,88 +37,65 @@ import { MatRecipe } from 'src/app/core/models/recipe.model';
}) })
export class RecipeDetailsComponent implements OnInit { export class RecipeDetailsComponent implements OnInit {
title: string = 'Recipe Detail'; title: string = 'Recipe Detail';
recipeMetaData: RecipeMetaData | null = null;
originalRecipeDetail: BehaviorSubject<RecipeDetail | null> = recipeDetail$!: Observable<RecipeDetail>;
new BehaviorSubject<RecipeDetail | null>(null);
matForRecipeList = this.originalRecipeDetail.pipe(
map((x) => x?.recipe.recipes)
);
isLoaded: boolean = false; isLoaded: boolean = false;
isMatLoaded: boolean = false; isMatLoaded: boolean = false;
actionRecord: ActionRecord<RecipeDetail | RecipeDetailMat> =
new ActionRecord();
recipeOriginalDetail!: typeof this.recipeDetailForm.value;
constructor( constructor(
private _formBuilder: FormBuilder,
private _route: ActivatedRoute, private _route: ActivatedRoute,
private _router: Router, private _router: Router,
private _recipeService: RecipeService private _recipeService: RecipeService
) {} ) {}
recipeDetail = new FormGroup({ productCode!: string;
productCode: new FormControl<string>(''),
name: new FormControl<string>(''), recipeDetailForm = this._formBuilder.group({
otherName: new FormControl<string>(''), productCode: '',
description: new FormControl<string>(''), name: '',
otherDescription: new FormControl<string>(''), otherName: '',
lastModified: new FormControl<Date>(new Date()), description: '',
price: new FormControl<number>(0), otherDescription: '',
isUse: new FormControl<boolean>(false), lastModified: new Date(),
isShow: new FormControl<boolean>(false), price: 0,
disable: new FormControl<boolean>(false), isUse: false,
isShow: false,
disable: false,
recipeListData: this._formBuilder.array([]),
}); });
materialListIds$: Subject<{
ids: number[];
matRecipeList: MatRecipe[];
}> = new Subject<{
ids: number[];
matRecipeList: MatRecipe[];
}>();
ngOnInit() { ngOnInit() {
this._recipeService this.productCode = this._route.snapshot.params['productCode'];
.getRecipesById(this._route.snapshot.params['productCode'])
.pipe(finalize(() => {}))
.subscribe(({ recipe, recipeMetaData }) => {
this.title = recipe.name + ' | ' + recipe.productCode;
this.recipeDetail.patchValue({
productCode: recipe.productCode,
name: recipe.name,
otherName: recipe.otherName,
description: recipe.Description,
otherDescription: recipe.otherDescription,
lastModified: recipe.LastChange,
price: recipe.cashPrice,
isUse: recipe.isUse,
isShow: recipe.isShow,
disable: recipe.disable,
});
this.originalRecipeDetail.next({
recipe: {
lastModified: recipe.LastChange,
productCode: recipe.productCode,
name: recipe.name,
otherName: recipe.otherName,
description: recipe.Description,
otherDescription: recipe.otherDescription,
price: recipe.cashPrice,
isUse: recipe.isUse,
isShow: recipe.isShow,
disable: recipe.disable,
},
recipes: recipe.recipes,
});
const ids = recipe.recipes?.map((recipe) => recipe.materialPathId); this.recipeDetail$ = this._recipeService
this.materialListIds$.next({ .getRecipeDetail(this.productCode)
ids: ids || [], .pipe(first());
matRecipeList: recipe.recipes || [], this.recipeDetail$.subscribe((detail) => {
}); this.recipeDetailForm.patchValue(detail);
this.isLoaded = true;
this.recipeOriginalDetail = { ...this.recipeDetailForm.getRawValue() };
});
this.recipeMetaData = recipeMetaData; // snap recipe detail form value
this.isLoaded = true;
}); this.actionRecord.registerOnAddAction((currAction, allAction) => {
if (currAction.type === 'recipeListData') {
switch (currAction.action) {
case 'add':
break;
case 'delete':
break;
}
}
console.log('Action Record', allAction);
});
} }
showConfirmSaveModal: EventEmitter<boolean> = new EventEmitter<boolean>(); showConfirmSaveModal: EventEmitter<boolean> = new EventEmitter<boolean>();
@ -137,13 +107,13 @@ export class RecipeDetailsComponent implements OnInit {
confirmCallBack: () => { confirmCallBack: () => {
console.log('confirm save'); console.log('confirm save');
// TODO: update value in targeted recipe // TODO: update value in targeted recipe
this._recipeService.editChanges( // this._recipeService.editChanges(
this._recipeService.getCurrentCountry(), // this._recipeService.getCurrentCountry(),
this._recipeService.getCurrentFile(), // this._recipeService.getCurrentFile(),
{ // {
...this.recipeDetail, // ...this.recipeDetail,
} // }
); // );
console.log('Sending changes'); console.log('Sending changes');
this._router.navigate(['/recipes']); this._router.navigate(['/recipes']);
}, },
@ -176,8 +146,8 @@ export class RecipeDetailsComponent implements OnInit {
get isValueChanged() { get isValueChanged() {
return !isEqual( return !isEqual(
this.recipeDetail.value, this.recipeOriginalDetail,
this.originalRecipeDetail.getValue()?.recipe this.recipeDetailForm.getRawValue()
); );
} }
} }

View file

@ -1,7 +1,7 @@
<table class="table" [formGroup]="parentForm"> <table class="table" [formGroup]="parentForm">
<thead> <thead>
<tr class="bg-gray-200"> <tr class="bg-gray-200">
<th class="px-6 py-3">Enable</th> <th class="px-6 py-3">Action</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>
@ -11,55 +11,57 @@
<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 Hot</th> <th class="px-6 py-3">Water Yield</th>
</tr> </tr>
</thead> </thead>
<tbody formArrayName="recipes" *ngIf="isMatLoaded"> <tbody
<tr formArrayName="recipeListData"
*ngFor="let mat of recipeListData.controls; let i = index" *ngFor="let mat of recipeListData.controls; let i = index"
class="bg-white la border-b hover:bg-secondary" >
> <tr class="bg-white la border-b hover:bg-secondary" formGroupName="{{ i }}">
<div 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"> <button
<label> class="btn btn-primary"
<input (click)="deleteRecipeData(i)"
type="checkbox" type="button"
class="toggle toggle-sm" >
formControlName="enable" Delete
/> </button>
</label>
</td> <button class="btn btn-primary" (click)="addRecipeData()" type="button">
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"> Add
<input type="text" class="input" formControlName="id" /> </button>
</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="materialID" />
</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="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="stirTime" /> <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="waterHot" /> <input type="text" class="input" formControlName="waterYield" />
</td> </td>
</div> <td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
<input type="text" class="input" formControlName="stirTime" />
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -2,13 +2,18 @@ import { NgFor, NgIf } from '@angular/common';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { import {
FormArray, FormArray,
FormBuilder,
FormControl, FormControl,
FormGroup, FormGroup,
ReactiveFormsModule, ReactiveFormsModule,
} from '@angular/forms'; } from '@angular/forms';
import { Observable } from 'rxjs'; import { first } from 'rxjs';
import { MatRecipe } from 'src/app/core/models/recipe.model'; import {
import { MaterialService } from 'src/app/core/services/material.service'; RecipeDetail,
RecipeDetailMat,
} from 'src/app/core/models/recipe.model';
import { RecipeService } from 'src/app/core/services/recipe.service';
import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord';
export interface RecipeListDataFormGroup { export interface RecipeListDataFormGroup {
id: FormControl<number | null>; id: FormControl<number | null>;
@ -31,97 +36,86 @@ export interface RecipeListDataFormGroup {
imports: [NgIf, NgFor, ReactiveFormsModule], imports: [NgIf, NgFor, ReactiveFormsModule],
}) })
export class RecipeListComponent implements OnInit { export class RecipeListComponent implements OnInit {
@Input({ required: true }) matRecipeList!: Observable<{
ids: number[];
matRecipeList: MatRecipe[];
}>;
@Input({ required: true }) parentForm!: FormGroup; @Input({ required: true }) parentForm!: FormGroup;
@Input({ required: true }) actionRecord!: ActionRecord<
RecipeDetail | RecipeDetailMat
>;
recipeListData!: FormArray<FormGroup<RecipeListDataFormGroup>>; @Input({ required: true }) recipeDetailOriginal!: any;
@Input({ required: true }) productCode!: string;
isMatLoaded: boolean = false; isMatLoaded: boolean = false;
constructor(private _materialService: MaterialService) {} constructor(
private _recipeService: RecipeService,
private _formBuilder: FormBuilder
) {}
ngOnInit(): void { ngOnInit(): void {
this.matRecipeList.subscribe((x) => { this._recipeService
this._materialService.getMaterialCodes(x.ids).subscribe((data) => { .getRecipeDetailMat(this.productCode)
const matList = x.matRecipeList .pipe(first())
.map((item) => { .subscribe(({ result }) => {
for (let i = 0; i < data.length; i++) { if (this.recipeDetailOriginal)
if (item.materialPathId === 0) { this.recipeDetailOriginal.recipeListData = result;
return { else this.recipeDetailOriginal = { recipeListData: result };
id: 0, result.forEach((recipeDetailMat: RecipeDetailMat) => {
name: '', this.recipeListData.push(
enable: item.isUse, this._formBuilder.group({
mixOrder: item.MixOrder, materialID: recipeDetailMat.materialID,
stirTime: item.stirTime, name: recipeDetailMat.name,
powderGram: item.powderGram, enable: recipeDetailMat.isUse,
powderTime: item.powderTime, mixOrder: recipeDetailMat.mixOrder,
syrupGram: item.syrupGram, stirTime: recipeDetailMat.stirTime,
syrupTime: item.syrupTime, powderGram: recipeDetailMat.powderGram,
waterCold: item.waterCold, powderTime: recipeDetailMat.powderTime,
waterHot: item.waterYield, syrupGram: recipeDetailMat.syrupGram,
}; syrupTime: recipeDetailMat.syrupTime,
} waterCold: recipeDetailMat.waterCold,
waterYield: recipeDetailMat.waterYield,
if (item.materialPathId === data[i].materialID) { })
return { );
id: data[i].materialID, });
name: data[i].PackageDescription,
enable: item.isUse,
mixOrder: item.MixOrder,
stirTime: item.stirTime,
powderGram: item.powderGram,
powderTime: item.powderTime,
syrupGram: item.syrupGram,
syrupTime: item.syrupTime,
waterCold: item.waterCold,
waterHot: item.waterYield,
};
}
}
return {
id: item.materialPathId,
name: '',
enable: item.isUse,
mixOrder: item.MixOrder,
stirTime: item.stirTime,
powderGram: item.powderGram,
powderTime: item.powderTime,
syrupGram: item.syrupGram,
syrupTime: item.syrupTime,
waterCold: item.waterCold,
waterHot: item.waterYield,
};
})
.sort((a, b) => {
return a.id === 0 ? 1 : a.id > b.id ? 1 : -1;
});
this.recipeListData = new FormArray<FormGroup<RecipeListDataFormGroup>>(
matList.map((item) => {
return new FormGroup<RecipeListDataFormGroup>({
id: new FormControl<number>(item.id),
name: new FormControl<string>(item.name),
enable: new FormControl<boolean>(item.enable),
mixOrder: new FormControl<number>(item.mixOrder),
stirTime: new FormControl<number>(item.stirTime),
powderGram: new FormControl<number>(item.powderGram),
powderTime: new FormControl<number>(item.powderTime),
syrupGram: new FormControl<number>(item.syrupGram),
syrupTime: new FormControl<number>(item.syrupTime),
waterCold: new FormControl<number>(item.waterCold),
waterHot: new FormControl<number>(item.waterHot),
});
})
);
this.parentForm.addControl('recipes', this.recipeListData);
console.log(this.parentForm);
this.isMatLoaded = true; this.isMatLoaded = true;
}); });
}); }
get recipeListData(): FormArray {
return this.parentForm.get('recipeListData') as FormArray;
}
addRecipeData(): void {
const newRecipeDetailMat: RecipeDetailMat = {
materialID: 0,
name: '',
mixOrder: 0,
feedParameter: 0,
feedPattern: 0,
isUse: false,
materialPathId: 0,
powderGram: 0,
powderTime: 0,
stirTime: 0,
syrupGram: 0,
syrupTime: 0,
waterCold: 0,
waterYield: 0,
};
this.recipeListData.push(this._formBuilder.group(newRecipeDetailMat));
this.actionRecord.addAction(
new Action('add', newRecipeDetailMat, 'recipeListData')
);
}
deleteRecipeData(index: number): void {
const recipeDetailMat: RecipeDetailMat =
this.recipeListData.at(index).value;
this.recipeListData.removeAt(index);
this.actionRecord.addAction(
new Action('delete', recipeDetailMat, 'recipeListData')
);
} }
} }

View file

@ -2,14 +2,17 @@
class="relative overflow-auto max-h-[900px] shadow-md sm:rounded-lg" class="relative overflow-auto max-h-[900px] shadow-md sm:rounded-lg"
#table #table
> >
<table *ngIf="isLoaded" 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 class="divide-y divide-solid divide-gray-400"> <div
class="divide-y divide-solid divide-gray-400"
*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 {{ recipes?.MachineSetting?.configNumber }} | >Recipe Version {{ recipesDashboard.configNumber }} |
{{ currentFile }}</span {{ recipesDashboard.filename }}</span
> >
</div> </div>
<div class="flex flex-col ml-5"> <div class="flex flex-col ml-5">
@ -106,7 +109,9 @@
<div class="flex flex-col ml-auto"> <div class="flex flex-col ml-auto">
<span class="" <span class=""
>Last Updated: >Last Updated:
{{ recipes?.Timestamp | date : "dd-MMM-yyyy hh:mm:ss" }}</span {{
recipesDashboard.configNumber | date : "dd-MMM-yyyy hh:mm:ss"
}}</span
> >
</div> </div>
</div> </div>
@ -199,7 +204,7 @@
</thead> </thead>
<tbody> <tbody>
<tr <tr
*ngFor="let recipe of recipes01" *ngFor="let recipe of recipeOverviewList"
class="bg-white la border-b hover:bg-secondary" class="bg-white la border-b hover:bg-secondary"
> >
<th> <th>
@ -219,9 +224,9 @@
{{ recipe.name }} {{ recipe.name }}
</td> </td>
<td class="px-6 py-4">{{ recipe.otherName }}</td> <td class="px-6 py-4">{{ recipe.otherName }}</td>
<td class="px-6 py-4 flex-wrap max-w-xs">{{ recipe.Description }}</td> <td class="px-6 py-4 flex-wrap max-w-xs">{{ recipe.description }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{{ recipe.LastChange | date : "dd-MMM-yyyy hh:mm:ss" }} {{ recipe.lastUpdated | date : "dd-MMM-yyyy hh:mm:ss" }}
</td> </td>
<td class="px-4 py-4 flex"> <td class="px-4 py-4 flex">
<!-- <recipe-modal productCode="{{ recipe.productCode }}"></recipe-modal> --> <!-- <recipe-modal productCode="{{ recipe.productCode }}"></recipe-modal> -->
@ -249,7 +254,7 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<div *ngIf="!isLoaded"> <ng-template #loading>
<div <div
class="flex w-full items-center justify-center h-56 border border-gray-200 rounded-lg bg-gray-50" class="flex w-full items-center justify-center h-56 border border-gray-200 rounded-lg bg-gray-50"
> >
@ -272,7 +277,7 @@
</svg> </svg>
</div> </div>
</div> </div>
</div> </ng-template>
<button <button
class="btn btn-circle fixed z-100 bottom-5 right-1" class="btn btn-circle fixed z-100 bottom-5 right-1"

View file

@ -6,11 +6,23 @@ import {
ViewChild, ViewChild,
} from '@angular/core'; } from '@angular/core';
import { CommonModule, DatePipe } from '@angular/common'; import { CommonModule, DatePipe } from '@angular/common';
import { Recipe, Recipe01 } from 'src/app/core/models/recipe.model'; import {
Recipe,
Recipe01,
RecipeOverview,
RecipesDashboard,
} 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 { BehaviorSubject, Subscription, map } from 'rxjs'; import {
BehaviorSubject,
Observable,
Subscription,
finalize,
map,
tap,
} 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';
@ -31,9 +43,8 @@ import { MaterialService } from 'src/app/core/services/material.service';
templateUrl: './recipes.component.html', templateUrl: './recipes.component.html',
}) })
export class RecipesComponent implements OnInit, OnDestroy { export class RecipesComponent implements OnInit, OnDestroy {
recipes: Recipe | null = null; recipesDashboard$!: Observable<RecipesDashboard>;
recipes01: Recipe01[] | null = null; recipeOverviewList!: RecipeOverview[];
currentFile: string = '';
selectMaterialFilter: number[] | null = null; selectMaterialFilter: number[] | null = null;
materialList: { id: number; name: string | number }[] | null = null; materialList: { id: number; name: string | number }[] | null = null;
@ -47,8 +58,8 @@ export class RecipesComponent implements OnInit, OnDestroy {
private offset = 0; private offset = 0;
private take = 20; private take = 20;
isLoaded: boolean = false; // isLoaded: boolean = false;
isLoadMore: boolean = false; isLoadMore: boolean = true;
isHasMore: boolean = true; isHasMore: boolean = true;
private searchStr = ''; private searchStr = '';
@ -72,7 +83,7 @@ export class RecipesComponent implements OnInit, OnDestroy {
if (isBottom && !this.isLoadMore) { if (isBottom && !this.isLoadMore) {
this.isLoadMore = true; this.isLoadMore = true;
this._recipeService this._recipeService
.getRecipes({ .getRecipeOverview({
offset: this.offset, offset: this.offset,
take: this.take, take: this.take,
search: this.oldSearchStr, search: this.oldSearchStr,
@ -80,21 +91,16 @@ export class RecipesComponent implements OnInit, OnDestroy {
country: this._recipeService.getCurrentCountry(), country: this._recipeService.getCurrentCountry(),
materialIds: this.selectMaterialFilter || [], materialIds: this.selectMaterialFilter || [],
}) })
.subscribe(({ recipes, hasMore, fileName }) => { .subscribe(({ result, hasMore, totalCount }) => {
const { Recipe01, ...recipesWithoutRecipe01 } = recipes; if (this.recipeOverviewList) {
if (this.recipes01 && this.isHasMore) { this.recipeOverviewList =
this.recipes01 = [...this.recipes01, ...Recipe01]; this.recipeOverviewList.concat(result);
} else { } else {
this.recipes01 = Recipe01; this.recipeOverviewList = result;
} }
this.recipes = {
...recipesWithoutRecipe01,
Recipe01: [],
};
this.currentFile = fileName;
this.offset += 10; this.offset += 10;
this.isLoadMore = false;
this.isHasMore = hasMore; this.isHasMore = hasMore;
this.isLoadMore = false;
}); });
} }
}, },
@ -108,31 +114,30 @@ export class RecipesComponent implements OnInit, OnDestroy {
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this._recipeService this.recipesDashboard$ = this._recipeService
.getRecipes({ .getRecipesDashboard({
offset: this.offset,
take: this.take,
search: this.oldSearchStr,
filename: this._recipeService.getCurrentFile(), filename: this._recipeService.getCurrentFile(),
country: this._recipeService.getCurrentCountry(), country: this._recipeService.getCurrentCountry(),
materialIds: this.selectMaterialFilter || [],
}) })
.subscribe(({ recipes, hasMore, fileName }) => { .pipe(
const { Recipe01, ...recipesWithoutRecipe01 } = recipes; finalize(() => {
if (this.recipes01 && this.isHasMore) { this._recipeService
this.recipes01 = [...this.recipes01, ...Recipe01]; .getRecipeOverview({
} else { offset: this.offset,
this.recipes01 = Recipe01; take: this.take,
} search: this.oldSearchStr,
this.recipes = { filename: this._recipeService.getCurrentFile(),
...recipesWithoutRecipe01, country: this._recipeService.getCurrentCountry(),
Recipe01: [], materialIds: this.selectMaterialFilter || [],
}; })
this.currentFile = fileName; .subscribe(({ result, hasMore, totalCount }) => {
this.offset += 10; this.recipeOverviewList = result;
this.isLoaded = true; this.offset += 10;
this.isHasMore = hasMore; this.isHasMore = hasMore;
}); this.isLoadMore = false;
});
})
);
this._materialService this._materialService
.getMaterialCodes() .getMaterialCodes()
@ -157,28 +162,22 @@ export class RecipesComponent implements OnInit, OnDestroy {
search(event: Event) { search(event: Event) {
this.offset = 0; this.offset = 0;
this.isLoadMore = true;
this.oldSearchStr = this.searchStr; this.oldSearchStr = this.searchStr;
this._recipeService this._recipeService
.getRecipes({ .getRecipeOverview({
offset: this.offset, offset: this.offset,
take: this.take, take: this.take,
search: this.searchStr, search: this.oldSearchStr,
filename: this._recipeService.getCurrentFile(), filename: this._recipeService.getCurrentFile(),
country: this._recipeService.getCurrentCountry(), country: this._recipeService.getCurrentCountry(),
materialIds: this.selectMaterialFilter || [], materialIds: this.selectMaterialFilter || [],
}) })
.subscribe(({ recipes, hasMore, fileName }) => { .subscribe(({ result, hasMore, totalCount }) => {
const { Recipe01, ...recipesWithoutRecipe01 } = recipes; this.recipeOverviewList = result;
this.recipes01 = Recipe01;
this.recipes = {
...recipesWithoutRecipe01,
Recipe01: [],
};
this.currentFile = fileName;
this.offset += 10; this.offset += 10;
this.isLoaded = true;
this.isHasMore = hasMore; this.isHasMore = hasMore;
this.isLoadMore = false;
}); });
} }
@ -282,17 +281,19 @@ export class RecipesComponent implements OnInit, OnDestroy {
loadRecipe(recipeFileName: string) { loadRecipe(recipeFileName: string) {
// clear all recipes // clear all recipes
this.recipes = null;
this.recipes01 = null;
this.offset = 0; this.offset = 0;
this.isLoaded = false;
this.isHasMore = true; this.isHasMore = true;
this.isLoadMore = false; this.isLoadMore = true;
this.oldSearchStr = ''; this.oldSearchStr = '';
localStorage.setItem('currentRecipeFile', recipeFileName); localStorage.setItem('currentRecipeFile', recipeFileName);
this.recipesDashboard$ = this._recipeService.getRecipesDashboard({
filename: recipeFileName,
country: this.selectedCountry!,
});
this._recipeService this._recipeService
.getRecipes({ .getRecipeOverview({
offset: this.offset, offset: this.offset,
take: this.take, take: this.take,
search: this.oldSearchStr, search: this.oldSearchStr,
@ -300,21 +301,11 @@ export class RecipesComponent implements OnInit, OnDestroy {
country: this.selectedCountry!, country: this.selectedCountry!,
materialIds: this.selectMaterialFilter || [], materialIds: this.selectMaterialFilter || [],
}) })
.subscribe(({ recipes, hasMore, fileName }) => { .subscribe(({ result, hasMore, totalCount }) => {
const { Recipe01, ...recipesWithoutRecipe01 } = recipes; this.recipeOverviewList = result;
if (this.recipes01 && this.isHasMore) {
this.recipes01 = [...this.recipes01, ...Recipe01];
} else {
this.recipes01 = Recipe01;
}
this.recipes = {
...recipesWithoutRecipe01,
Recipe01: [],
};
this.currentFile = fileName;
this.offset += 10; this.offset += 10;
this.isLoaded = true;
this.isHasMore = hasMore; this.isHasMore = hasMore;
this.isLoadMore = false;
}); });
} }

View file

@ -0,0 +1,59 @@
export class Action<T> {
private _action: string;
private _data: T;
private _type: string;
constructor(action: string, data: T, type: string) {
this._action = action;
this._data = data;
this._type = type;
}
get action(): string {
return this._action;
}
get data(): T {
return this._data;
}
get type(): string {
return this._type;
}
}
export class ActionRecord<T> {
private _actionRecord: Action<T>[];
private _onAddActionCallback: (
currentAction: Action<T>,
actionRecord: Action<T>[]
) => void = () => {};
constructor() {
this._actionRecord = [];
}
getRecord(): Action<T>[] {
return this._actionRecord;
}
addAction(action: Action<T>): void {
this._actionRecord.push(action);
this._onAddActionCallback(action, this._actionRecord);
}
removeAction(action: Action<T>): void {
let index = this._actionRecord.indexOf(action);
this._actionRecord.splice(index, 1);
}
clearAction(): void {
this._actionRecord = [];
}
registerOnAddAction(
fn: (currentAction: Action<T>, actionRecord: Action<T>[]) => void
): void {
this._onAddActionCallback = fn;
}
}

View file

@ -24,22 +24,22 @@ export interface MaterialData {
waterHot: number; waterHot: number;
} }
export interface RecipeDetail { // export interface RecipeDetail {
recipe: { // recipe: {
lastModified: Date; // lastModified: Date;
productCode: string; // productCode: string;
name: string; // name: string;
otherName: string; // otherName: string;
description: string; // description: string;
otherDescription: string; // otherDescription: string;
price: number; // price: number;
isUse: boolean; // isUse: boolean;
isShow: boolean; // isShow: boolean;
disable: boolean; // disable: boolean;
recipes?: MaterialData[]; // recipes?: MaterialData[];
}; // };
recipes?: MatRecipe[]; // recipes?: MatRecipe[];
} // }
export interface RecipeDetailEditable { export interface RecipeDetailEditable {
name: string; name: string;

15
server/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceRoot}"
}
]
}

View file

@ -0,0 +1,82 @@
package contracts
// ================================== Recipes Dashboard and Overview ==================================
type RecipeOverview struct {
ID int `json:"id"`
ProductCode string `json:"productCode"`
Name string `json:"name"`
OtherName string `json:"otherName"`
Description string `json:"description"`
LastUpdated string `json:"lastUpdated"`
}
type RecipeDashboardRequest struct {
Country string `json:"country"`
Filename string `json:"filename"`
}
type RecipeDashboardResponse struct {
ConfigNumber int `json:"configNumber"`
LastUpdated string `json:"lastUpdated"`
Filename string `json:"filename"`
}
type RecipeOverviewRequest struct {
Take int `json:"take"`
Skip int `json:"skip"`
Search string `json:"search"`
Country string `json:"country"`
Filename string `json:"filename"`
MatIds []int `json:"matIds"`
}
type RecipeOverviewResponse struct {
Result []RecipeOverview `json:"result"`
HasMore bool `json:"hasMore"`
TotalCount int `json:"totalCount"`
}
// ================================== Recipe Detail ==================================
type RecipeDetailRequest struct {
Filename string `json:"filename"`
Country string `json:"country"`
ProductCode string `json:"productCode"`
}
type RecipeDetailResponse struct {
Name string `json:"name"`
OtherName string `json:"otherName"`
Description string `json:"description"`
OtherDescription string `json:"otherDescription"`
LastUpdated string `json:"lastUpdated"`
Picture string `json:"picture"`
}
type RecipeDetailMat struct {
MaterialID uint64 `json:"materialID"`
Name string `json:"name"`
MixOrder int `json:"mixOrder"`
FeedParameter int `json:"feedParameter"`
FeedPattern int `json:"feedPattern"`
IsUse bool `json:"isUse"`
MaterialPathId int `json:"materialPathId"`
PowderGram int `json:"powderGram"`
PowderTime int `json:"powderTime"`
StirTime int `json:"stirTime"`
SyrupGram int `json:"syrupGram"`
SyrupTime int `json:"syrupTime"`
WaterCold int `json:"waterCold"`
WaterYield int `json:"waterYield"`
}
type RecipeDetailMatListRequest struct {
Filename string `json:"filename"`
Country string `json:"country"`
ProductCode string `json:"productCode"`
}
type RecipeDetailMatListResponse struct {
Result []RecipeDetailMat `json:"result"`
}

View file

@ -69,20 +69,20 @@ func NewData() *Data {
} }
} }
func (d *Data) GetRecipe(countryID, filename string) models.Recipe { func (d *Data) GetRecipe(countryID, filename string) *models.Recipe {
if countryID == "" { if countryID == "" {
return *d.currentRecipe return d.currentRecipe
} }
if filename == "" || filename == d.CurrentFile { if filename == "" || filename == d.CurrentFile {
return *d.currentRecipe return d.currentRecipe
} }
if recipe, ok := d.recipeMap[filename]; ok { if recipe, ok := d.recipeMap[filename]; ok {
d.CurrentFile = filename d.CurrentFile = filename
d.CurrentCountryID = countryID d.CurrentCountryID = countryID
return recipe.Recipe return &recipe.Recipe
} }
// change current version and read new recipe // change current version and read new recipe
@ -92,7 +92,7 @@ func (d *Data) GetRecipe(countryID, filename string) models.Recipe {
if err != nil { if err != nil {
logger.GetInstance().Error("Error when read recipe file", zap.Error(err)) logger.GetInstance().Error("Error when read recipe file", zap.Error(err))
return *d.currentRecipe return d.currentRecipe
} }
d.currentRecipe = recipe d.currentRecipe = recipe
@ -116,23 +116,70 @@ func (d *Data) GetRecipe(countryID, filename string) models.Recipe {
TimeStamps: time.Now().Unix(), TimeStamps: time.Now().Unix(),
} }
return *d.currentRecipe return d.currentRecipe
} }
func (d *Data) GetRecipe01() []models.Recipe01 { func (d *Data) GetRecipe01() []models.Recipe01 {
return d.currentRecipe.Recipe01 return d.currentRecipe.Recipe01
} }
func (d *Data) GetRecipe01ByProductCode(code string) models.Recipe01 { func (d *Data) GetRecipe01ByProductCode(filename, countryID, productCode string) (models.Recipe01, error) {
result := make([]models.Recipe01, 0)
for _, v := range d.currentRecipe.Recipe01 { if filename == "" || filename == d.CurrentFile {
if v.ProductCode == code { for _, v := range d.currentRecipe.Recipe01 {
result = append(result, v) if v.ProductCode == productCode {
return v, nil
}
}
} else if recipe, ok := d.recipeMap[filename]; ok {
for _, v := range recipe.Recipe.Recipe01 {
if v.ProductCode == productCode {
return v, nil
}
} }
} }
return result[0] d.CurrentFile = filename
d.CurrentCountryID = countryID
recipe, err := helpers.ReadRecipeFile(countryID, filename)
if err != nil {
logger.GetInstance().Error("Error when read recipe file", zap.Error(err))
for _, v := range d.currentRecipe.Recipe01 {
if v.ProductCode == productCode {
return v, nil
}
}
}
d.currentRecipe = recipe
// save to map
if len(d.recipeMap) > 5 { // limit keep in memory 5 version
// remove oldest version
var oldestVersion string
var oldestTime int64
for k, v := range d.recipeMap {
if oldestTime == 0 || v.TimeStamps < oldestTime {
oldestTime = v.TimeStamps
oldestVersion = k
}
}
delete(d.recipeMap, oldestVersion)
}
d.recipeMap[filename] = RecipeWithTimeStamps{
Recipe: *d.currentRecipe,
TimeStamps: time.Now().Unix(),
}
for _, v := range d.currentRecipe.Recipe01 {
if v.ProductCode == productCode {
return v, nil
}
}
return models.Recipe01{}, fmt.Errorf("product code: %s not found", productCode)
} }
func (d *Data) SetValuesToRecipe(recipe models.Recipe01) { func (d *Data) SetValuesToRecipe(recipe models.Recipe01) {

View file

@ -7,11 +7,12 @@ import (
"net/http" "net/http"
"os" "os"
"path" "path"
"recipe-manager/contracts"
"recipe-manager/data" "recipe-manager/data"
"recipe-manager/models" "recipe-manager/models"
"recipe-manager/services/logger" "recipe-manager/services/logger"
"recipe-manager/services/recipe"
"recipe-manager/services/sheet" "recipe-manager/services/sheet"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -20,24 +21,45 @@ import (
) )
type RecipeRouter struct { type RecipeRouter struct {
data *data.Data data *data.Data
sheetService sheet.SheetService sheetService sheet.SheetService
recipeService recipe.RecipeService
} }
var ( var (
Log = logger.GetInstance() Log = logger.GetInstance()
) )
func NewRecipeRouter(data *data.Data, sheetService sheet.SheetService) *RecipeRouter { func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter {
return &RecipeRouter{ return &RecipeRouter{
data: data, data: data,
sheetService: sheetService, recipeService: recipeService,
sheetService: sheetService,
} }
} }
func (rr *RecipeRouter) Route(r chi.Router) { func (rr *RecipeRouter) Route(r chi.Router) {
r.Route("/recipes", func(r chi.Router) { r.Route("/recipes", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) { r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
country := r.URL.Query().Get("country")
filename := r.URL.Query().Get("filename")
result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{
Country: country,
Filename: filename,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(result)
})
r.Get("/overview", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
var take, offset uint64 = 10, 0 var take, offset uint64 = 10, 0
if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil { if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil {
@ -50,111 +72,105 @@ func (rr *RecipeRouter) Route(r chi.Router) {
country := r.URL.Query().Get("country") country := r.URL.Query().Get("country")
filename := r.URL.Query().Get("filename") filename := r.URL.Query().Get("filename")
materialIds := r.URL.Query().Get("material_ids") materialIds := r.URL.Query().Get("materialIds")
var materialIdsUint []uint64 var materialIdsUint []int
for _, v := range strings.Split(materialIds, ",") { for _, v := range strings.Split(materialIds, ",") {
materialIdUint, err := strconv.ParseUint(v, 10, 64) materialIdUint, err := strconv.ParseUint(v, 10, 64)
if err != nil || materialIdUint == 0 { if err != nil || materialIdUint == 0 {
continue continue
} }
materialIdsUint = append(materialIdsUint, materialIdUint) materialIdsUint = append(materialIdsUint, int(materialIdUint))
} }
countryID, err := rr.data.GetCountryIDByName(country) result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{
Take: int(take),
Skip: int(offset),
Search: r.URL.Query().Get("search"),
Country: country,
Filename: filename,
MatIds: materialIdsUint,
})
if err != nil { if err != nil {
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound) http.Error(w, err.Error(), http.StatusNotFound)
return return
} }
recipe := rr.data.GetRecipe(countryID, filename) json.NewEncoder(w).Encode(result)
searchQuery := r.URL.Query().Get("search")
if searchQuery != "" {
recipe.Recipe01 = []models.Recipe01{}
for _, v := range rr.data.GetRecipe01() {
if strings.Contains(strings.ToLower(v.ProductCode), strings.ToLower(searchQuery)) ||
strings.Contains(strings.ToLower(v.Name), strings.ToLower(searchQuery)) ||
strings.Contains(strings.ToLower(v.OtherName), strings.ToLower(searchQuery)) {
recipe.Recipe01 = append(recipe.Recipe01, v)
}
}
}
if len(materialIdsUint) > 0 {
resultFilter := []models.Recipe01{}
for _, v := range recipe.Recipe01 {
for _, matID := range materialIdsUint {
for _, recipe := range v.Recipes {
if recipe.IsUse && uint64(recipe.MaterialPathId) == matID {
resultFilter = append(resultFilter, v)
}
}
}
}
recipe.Recipe01 = resultFilter
}
isHasMore := len(recipe.Recipe01) >= int(take+offset)
if isHasMore {
recipe.Recipe01 = recipe.Recipe01[offset : take+offset]
sort.Slice(recipe.Recipe01, func(i, j int) bool {
return recipe.Recipe01[i].ID < recipe.Recipe01[j].ID
})
} else if len(recipe.Recipe01) > int(offset) {
recipe.Recipe01 = recipe.Recipe01[offset:]
} else {
recipe.Recipe01 = []models.Recipe01{}
}
json.NewEncoder(w).Encode(map[string]interface{}{
"fileName": rr.data.CurrentFile,
"recipes": recipe,
"hasMore": isHasMore,
})
}) })
r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) { r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json") w.Header().Add("Content-Type", "application/json")
productCode := chi.URLParam(r, "product_code") productCode := chi.URLParam(r, "product_code")
recipe := rr.data.GetRecipe01() // recipe := rr.data.GetRecipe01()
recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE") // recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
var recipeResult *models.Recipe01 // var recipeResult *models.Recipe01
recipeMetaDataResult := map[string]string{} // recipeMetaDataResult := map[string]string{}
for _, v := range recipe { // for _, v := range recipe {
if v.ProductCode == productCode { // if v.ProductCode == productCode {
recipeResult = &v // recipeResult = &v
break // break
} // }
} // }
for _, v := range recipeMetaData { // for _, v := range recipeMetaData {
if v[0].(string) == productCode { // if v[0].(string) == productCode {
recipeMetaDataResult = map[string]string{ // recipeMetaDataResult = map[string]string{
"productCode": v[0].(string), // "productCode": v[0].(string),
"name": v[1].(string), // "name": v[1].(string),
"otherName": v[2].(string), // "otherName": v[2].(string),
"description": v[3].(string), // "description": v[3].(string),
"otherDescription": v[4].(string), // "otherDescription": v[4].(string),
"picture": v[5].(string), // "picture": v[5].(string),
} // }
break // break
} // }
} // }
if recipeResult == nil { // if recipeResult == nil {
http.Error(w, "Not Found", http.StatusNotFound) // http.Error(w, "Not Found", http.StatusNotFound)
// return
// }
// json.NewEncoder(w).Encode(map[string]interface{}{
// "recipe": recipeResult,
// "recipeMetaData": recipeMetaDataResult,
// })
result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{
Filename: r.URL.Query().Get("filename"),
Country: r.URL.Query().Get("country"),
ProductCode: productCode,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return return
} }
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(result)
"recipe": recipeResult, })
"recipeMetaData": recipeMetaDataResult,
r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
productCode := chi.URLParam(r, "product_code")
result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{
Filename: r.URL.Query().Get("filename"),
Country: r.URL.Query().Get("country"),
ProductCode: productCode,
}) })
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(result)
}) })
r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) { r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) {
@ -242,7 +258,12 @@ func (rr *RecipeRouter) Route(r chi.Router) {
Log.Debug("Changes: ", zap.Any("changes", changes)) Log.Debug("Changes: ", zap.Any("changes", changes))
// TODO: find the matched pd // TODO: find the matched pd
target_menu := rr.data.GetRecipe01ByProductCode(changes.ProductCode) target_menu, err := rr.data.GetRecipe01ByProductCode(filename, countryID, changes.ProductCode)
if err != nil {
Log.Error("Error when get recipe by product code", zap.Error(err))
return
}
menu_map := target_menu.ToMap() menu_map := target_menu.ToMap()
change_map := changes.ToMap() change_map := changes.ToMap()

View file

@ -16,6 +16,7 @@ import (
"recipe-manager/routers" "recipe-manager/routers"
"recipe-manager/services/logger" "recipe-manager/services/logger"
"recipe-manager/services/oauth" "recipe-manager/services/oauth"
"recipe-manager/services/recipe"
"recipe-manager/services/sheet" "recipe-manager/services/sheet"
"strings" "strings"
"sync" "sync"
@ -429,8 +430,11 @@ func (s *Server) createHandler() {
return return
} }
// Recipe Service
rs := recipe.NewRecipeService(database)
// Recipe Router // Recipe Router
rr := routers.NewRecipeRouter(database, sheetService) rr := routers.NewRecipeRouter(database, rs, sheetService)
rr.Route(r) rr.Route(r)
// Material Router // Material Router

View file

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