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

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 == "" {
return *d.currentRecipe
return d.currentRecipe
}
if filename == "" || filename == d.CurrentFile {
return *d.currentRecipe
return d.currentRecipe
}
if recipe, ok := d.recipeMap[filename]; ok {
d.CurrentFile = filename
d.CurrentCountryID = countryID
return recipe.Recipe
return &recipe.Recipe
}
// change current version and read new recipe
@ -92,7 +92,7 @@ func (d *Data) GetRecipe(countryID, filename string) models.Recipe {
if err != nil {
logger.GetInstance().Error("Error when read recipe file", zap.Error(err))
return *d.currentRecipe
return d.currentRecipe
}
d.currentRecipe = recipe
@ -116,23 +116,70 @@ func (d *Data) GetRecipe(countryID, filename string) models.Recipe {
TimeStamps: time.Now().Unix(),
}
return *d.currentRecipe
return d.currentRecipe
}
func (d *Data) GetRecipe01() []models.Recipe01 {
return d.currentRecipe.Recipe01
}
func (d *Data) GetRecipe01ByProductCode(code string) models.Recipe01 {
result := make([]models.Recipe01, 0)
func (d *Data) GetRecipe01ByProductCode(filename, countryID, productCode string) (models.Recipe01, error) {
for _, v := range d.currentRecipe.Recipe01 {
if v.ProductCode == code {
result = append(result, v)
if filename == "" || filename == d.CurrentFile {
for _, v := range d.currentRecipe.Recipe01 {
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) {

View file

@ -7,11 +7,12 @@ import (
"net/http"
"os"
"path"
"recipe-manager/contracts"
"recipe-manager/data"
"recipe-manager/models"
"recipe-manager/services/logger"
"recipe-manager/services/recipe"
"recipe-manager/services/sheet"
"sort"
"strconv"
"strings"
@ -20,24 +21,45 @@ import (
)
type RecipeRouter struct {
data *data.Data
sheetService sheet.SheetService
data *data.Data
sheetService sheet.SheetService
recipeService recipe.RecipeService
}
var (
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{
data: data,
sheetService: sheetService,
data: data,
recipeService: recipeService,
sheetService: sheetService,
}
}
func (rr *RecipeRouter) Route(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")
var take, offset uint64 = 10, 0
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")
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, ",") {
materialIdUint, err := strconv.ParseUint(v, 10, 64)
if err != nil || materialIdUint == 0 {
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 {
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
http.Error(w, err.Error(), http.StatusNotFound)
return
}
recipe := rr.data.GetRecipe(countryID, filename)
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,
})
json.NewEncoder(w).Encode(result)
})
r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
productCode := chi.URLParam(r, "product_code")
recipe := rr.data.GetRecipe01()
recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
// 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 {
recipeResult = &v
break
}
}
// for _, v := range recipe {
// if v.ProductCode == productCode {
// recipeResult = &v
// break
// }
// }
for _, v := range recipeMetaData {
if v[0].(string) == productCode {
recipeMetaDataResult = map[string]string{
"productCode": v[0].(string),
"name": v[1].(string),
"otherName": v[2].(string),
"description": v[3].(string),
"otherDescription": v[4].(string),
"picture": v[5].(string),
}
break
}
}
// for _, v := range recipeMetaData {
// if v[0].(string) == productCode {
// recipeMetaDataResult = map[string]string{
// "productCode": v[0].(string),
// "name": v[1].(string),
// "otherName": v[2].(string),
// "description": v[3].(string),
// "otherDescription": v[4].(string),
// "picture": v[5].(string),
// }
// break
// }
// }
if recipeResult == nil {
http.Error(w, "Not Found", http.StatusNotFound)
// if recipeResult == nil {
// http.Error(w, "Not Found", http.StatusNotFound)
// return
// }
// json.NewEncoder(w).Encode(map[string]interface{}{
// "recipe": recipeResult,
// "recipeMetaData": recipeMetaDataResult,
// })
result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{
Filename: r.URL.Query().Get("filename"),
Country: r.URL.Query().Get("country"),
ProductCode: productCode,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(map[string]interface{}{
"recipe": recipeResult,
"recipeMetaData": recipeMetaDataResult,
json.NewEncoder(w).Encode(result)
})
r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
productCode := chi.URLParam(r, "product_code")
result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{
Filename: r.URL.Query().Get("filename"),
Country: r.URL.Query().Get("country"),
ProductCode: productCode,
})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
json.NewEncoder(w).Encode(result)
})
r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) {
@ -242,7 +258,12 @@ func (rr *RecipeRouter) Route(r chi.Router) {
Log.Debug("Changes: ", zap.Any("changes", changes))
// 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()
change_map := changes.ToMap()

View file

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