change save file method, add file changes history
This commit is contained in:
parent
519749fd3a
commit
820557a268
12 changed files with 2388 additions and 2036 deletions
|
|
@ -1,329 +1,332 @@
|
|||
<div class="p-4">
|
||||
<form class="grid grid-cols-3 gap-4 mb-4" [formGroup]="recipeDetailForm">
|
||||
<div
|
||||
class="block col-span-1 p-6 bg-white border border-gray-200 rounded-lg shadow"
|
||||
>
|
||||
<div *ngIf="isLoaded; else indicator" [@inOutAnimation]>
|
||||
<div class="flex flex-wrap">
|
||||
<h5 class="mb-2 text-xl font-bold text-gray-900">
|
||||
{{ recipeDetailForm.getRawValue().name }}
|
||||
</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">
|
||||
{{ recipeDetailForm.getRawValue().otherName }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm text-gray-500">Last Modify</p>
|
||||
<p class="ml-2 text-sm text-gray-900">
|
||||
{{
|
||||
recipeDetailForm.getRawValue().lastModified
|
||||
| date : "dd/MM/yyyy HH:mm:ss"
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex p-3 items-center justify-center w-full">
|
||||
<img
|
||||
class="rounded-lg shadow"
|
||||
src="/assets/{{ 'bn_hot_america_no' }}.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="block col-span-2 p-6 bg-white border border-gray-200 rounded-lg shadow min-h-[300px]"
|
||||
>
|
||||
<div
|
||||
*ngIf="isLoaded; else indicator"
|
||||
[@inOutAnimation]
|
||||
class="grid-cols-2 w-full flex flex-col gap-2"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<label class="label text-base mr-3 w-max"
|
||||
><span class="label-text text-base mr-3 w-max">Name:</span></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered input-ghost w-full"
|
||||
formControlName="name"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label">
|
||||
<span class="label-text text-base mr-3 w-max"
|
||||
>Other Name:</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered input-ghost w-full"
|
||||
formControlName="otherName"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label"
|
||||
><span class="lable-text text-base mr-3 w-max"
|
||||
>Description:</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered input-ghost w-full"
|
||||
formControlName="description"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label"
|
||||
><span class="label-text text-base mr-3 w-max"
|
||||
>Other Description:</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered input-ghost w-full"
|
||||
formControlName="otherDescription"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-3 overflow-auto mb-4 rounded bg-white border border-gray-200 shadow"
|
||||
>
|
||||
<app-recipe-list
|
||||
[productCode]="productCode"
|
||||
(recipeListFormChange)="onRecipeListFormChange($event)"
|
||||
></app-recipe-list>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sticky bottom-0 col-span-3 flex justify-end bg-white rounded-full drop-shadow-2xl p-3"
|
||||
>
|
||||
<button
|
||||
(click)="onPressConfirmClose()"
|
||||
class="btn btn-error w-36 text-white mr-2"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
(click)="onPressConfirmSave()"
|
||||
class="btn btn-success w-36 text-white"
|
||||
[disabled]="!isValueChanged"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<ng-template #indicator>
|
||||
<div class="flex w-full h-full justify-center items-center">
|
||||
<span class="loading loading-spinner w-20"></span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<confirm-modal
|
||||
[title]="confirmSave.title"
|
||||
[message]="confirmSave.message"
|
||||
[confirmCallBack]="confirmSave.confirmCallBack"
|
||||
[show]="showConfirmSaveModal"
|
||||
></confirm-modal>
|
||||
|
||||
<confirm-modal
|
||||
[title]="confirmClose.title"
|
||||
[message]="confirmClose.message"
|
||||
[confirmCallBack]="confirmClose.confirmCallBack"
|
||||
[show]="showConfirmCloseModal"
|
||||
></confirm-modal>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<form class="grid grid-cols-3 gap-4 mb-4" [formGroup]="recipeDetailForm">
|
||||
<div
|
||||
class="block col-span-1 p-6 bg-white border border-gray-200 rounded-lg shadow"
|
||||
>
|
||||
<div *ngIf="isLoaded; else indicator" [@inOutAnimation]>
|
||||
<div class="flex flex-wrap">
|
||||
<h5 class="mb-2 text-xl font-bold text-gray-900">
|
||||
{{ recipeDetailForm.getRawValue().name }}
|
||||
</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">
|
||||
{{ recipeDetailForm.getRawValue().otherName }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm text-gray-500">Last Modify</p>
|
||||
<p class="ml-2 text-sm text-gray-900">
|
||||
{{
|
||||
recipeDetailForm.getRawValue().lastModified
|
||||
| date : "dd/MM/yyyy HH:mm:ss"
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex p-3 items-center justify-center w-full">
|
||||
<img
|
||||
class="rounded-lg shadow"
|
||||
src="/assets/{{ 'bn_hot_america_no' }}.png"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="block col-span-2 p-6 bg-white border border-gray-200 rounded-lg shadow min-h-[300px]"
|
||||
>
|
||||
<div
|
||||
*ngIf="isLoaded; else indicator"
|
||||
[@inOutAnimation]
|
||||
class="grid-cols-2 w-full flex flex-col gap-2"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<label class="label text-base mr-3 w-max"
|
||||
><span class="label-text text-base mr-3 w-max">Name:</span></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered input-ghost w-full"
|
||||
formControlName="name"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label">
|
||||
<span class="label-text text-base mr-3 w-max"
|
||||
>Other Name:</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered input-ghost w-full"
|
||||
formControlName="otherName"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label"
|
||||
><span class="lable-text text-base mr-3 w-max"
|
||||
>Description:</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered input-ghost w-full"
|
||||
formControlName="Description"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<label class="label"
|
||||
><span class="label-text text-base mr-3 w-max"
|
||||
>Other Description:</span
|
||||
></label
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm input-bordered input-ghost w-full"
|
||||
formControlName="otherDescription"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-3 overflow-auto mb-4 rounded bg-white border border-gray-200 shadow"
|
||||
>
|
||||
<app-recipe-list
|
||||
[productCode]="productCode"
|
||||
(recipeListFormChange)="onRecipeListFormChange($event)"
|
||||
></app-recipe-list>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
class="w-3.5 h-3.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 18"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 1v16M1 9h16"
|
||||
/>
|
||||
</svg>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sticky bottom-0 col-span-3 flex justify-end bg-white rounded-full drop-shadow-2xl p-3"
|
||||
>
|
||||
<!-- <div> Commit Message </div> -->
|
||||
<!-- <p class="text-2xl mr-8 text-gray-400 dark:text-gray-500">Commit Message</p> -->
|
||||
<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
|
||||
(click)="onPressConfirmClose()"
|
||||
class="btn btn-error w-36 text-white mr-2"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
(click)="onPressConfirmSave()"
|
||||
class="btn btn-success w-36 text-white"
|
||||
[disabled]="!isValueChanged"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<ng-template #indicator>
|
||||
<div class="flex w-full h-full justify-center items-center">
|
||||
<span class="loading loading-spinner w-20"></span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<confirm-modal
|
||||
[title]="confirmSave.title"
|
||||
[message]="confirmSave.message"
|
||||
[confirmCallBack]="confirmSave.confirmCallBack"
|
||||
[show]="showConfirmSaveModal"
|
||||
></confirm-modal>
|
||||
|
||||
<confirm-modal
|
||||
[title]="confirmClose.title"
|
||||
[message]="confirmClose.message"
|
||||
[confirmCallBack]="confirmClose.confirmCallBack"
|
||||
[show]="showConfirmCloseModal"
|
||||
></confirm-modal>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,158 +1,203 @@
|
|||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { Component, EventEmitter, OnInit } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { Observable, first } from 'rxjs';
|
||||
import { RecipeService } from 'src/app/core/services/recipe.service';
|
||||
import { ConfirmModal } from 'src/app/shared/modal/confirm/confirm-modal.component';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { RecipeListComponent } from './recipe-list/recipe-list.component';
|
||||
import {
|
||||
RecipeDetail,
|
||||
RecipeDetailMat,
|
||||
} from 'src/app/core/models/recipe.model';
|
||||
import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipe-details',
|
||||
templateUrl: './recipe-details.component.html',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
ReactiveFormsModule,
|
||||
ConfirmModal,
|
||||
DatePipe,
|
||||
RecipeListComponent,
|
||||
],
|
||||
animations: [
|
||||
trigger('inOutAnimation', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate('1s ease-out', style({ opacity: 1 })),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class RecipeDetailsComponent implements OnInit {
|
||||
title: string = 'Recipe Detail';
|
||||
|
||||
recipeDetail$!: Observable<RecipeDetail>;
|
||||
|
||||
isLoaded: boolean = false;
|
||||
isMatLoaded: boolean = false;
|
||||
|
||||
actionRecord: ActionRecord<RecipeDetail | RecipeDetailMat> =
|
||||
new ActionRecord();
|
||||
|
||||
recipeOriginalDetail!: typeof this.recipeDetailForm.value;
|
||||
|
||||
constructor(
|
||||
private _formBuilder: FormBuilder,
|
||||
private _route: ActivatedRoute,
|
||||
private _router: Router,
|
||||
private _recipeService: RecipeService
|
||||
) {}
|
||||
|
||||
productCode!: string;
|
||||
|
||||
recipeDetailForm = this._formBuilder.group({
|
||||
productCode: '',
|
||||
name: '',
|
||||
otherName: '',
|
||||
description: '',
|
||||
otherDescription: '',
|
||||
lastModified: new Date(),
|
||||
price: 0,
|
||||
isUse: false,
|
||||
isShow: false,
|
||||
disable: false,
|
||||
});
|
||||
|
||||
ngOnInit() {
|
||||
this.productCode = this._route.snapshot.params['productCode'];
|
||||
|
||||
this.recipeDetail$ = this._recipeService
|
||||
.getRecipeDetail(this.productCode)
|
||||
.pipe(first());
|
||||
this.recipeDetail$.subscribe((detail) => {
|
||||
this.recipeDetailForm.patchValue(detail);
|
||||
this.isLoaded = true;
|
||||
this.recipeOriginalDetail = { ...this.recipeDetailForm.getRawValue() };
|
||||
});
|
||||
|
||||
this.recipeDetailForm.valueChanges.subscribe(this.onRecipeDetailFormChange);
|
||||
|
||||
// snap recipe detail form value
|
||||
|
||||
this.actionRecord.registerOnAddAction((currAction, allAction) => {
|
||||
if (currAction.type === 'recipeListData') {
|
||||
switch (currAction.action) {
|
||||
case 'add':
|
||||
break;
|
||||
case 'delete':
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log('Action Record', allAction);
|
||||
});
|
||||
}
|
||||
|
||||
showConfirmSaveModal: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
showConfirmCloseModal: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
|
||||
confirmSave = {
|
||||
title: 'The changes detected!',
|
||||
message: 'Do you want to save changes?',
|
||||
confirmCallBack: () => {
|
||||
console.log('confirm save');
|
||||
// TODO: update value in targeted recipe
|
||||
// this._recipeService.editChanges(
|
||||
// this._recipeService.getCurrentCountry(),
|
||||
// this._recipeService.getCurrentFile(),
|
||||
// {
|
||||
// ...this.recipeDetail,
|
||||
// }
|
||||
// );
|
||||
console.log('Sending changes');
|
||||
this._router.navigate(['/recipes']);
|
||||
},
|
||||
};
|
||||
|
||||
confirmClose = {
|
||||
title: 'The changes will be lost!',
|
||||
message: 'Do you want to close without saving?',
|
||||
confirmCallBack: () => {
|
||||
console.log('confirm close');
|
||||
this._router.navigate(['/recipes']);
|
||||
},
|
||||
};
|
||||
|
||||
onPressConfirmSave() {
|
||||
if (this.isValueChanged) {
|
||||
this.showConfirmSaveModal.emit(true);
|
||||
} else {
|
||||
this._router.navigate(['/recipes']);
|
||||
}
|
||||
}
|
||||
|
||||
onPressConfirmClose() {
|
||||
if (this.isValueChanged) {
|
||||
this.showConfirmCloseModal.emit(true);
|
||||
} else {
|
||||
this._router.navigate(['/recipes']);
|
||||
}
|
||||
}
|
||||
|
||||
isValueChanged: boolean = false;
|
||||
|
||||
onRecipeDetailFormChange(recipeDetail: typeof this.recipeDetailForm.value) {
|
||||
console.log('Recipe Detail Form Changed', recipeDetail);
|
||||
}
|
||||
|
||||
onRecipeListFormChange(isValueChanged: boolean) {
|
||||
console.log('Recipe List Form Changed', isValueChanged);
|
||||
this.isValueChanged ||= isValueChanged;
|
||||
}
|
||||
}
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import { Component, EventEmitter, OnInit } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { Observable, first } from 'rxjs';
|
||||
import { RecipeService } from 'src/app/core/services/recipe.service';
|
||||
import { ConfirmModal } from 'src/app/shared/modal/confirm/confirm-modal.component';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { RecipeListComponent } from './recipe-list/recipe-list.component';
|
||||
import {
|
||||
RecipeDetail,
|
||||
RecipeDetailMat,
|
||||
} from 'src/app/core/models/recipe.model';
|
||||
import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord';
|
||||
import { isEqual } from 'lodash';
|
||||
import { UserService } from 'src/app/core/services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipe-details',
|
||||
templateUrl: './recipe-details.component.html',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
ReactiveFormsModule,
|
||||
ConfirmModal,
|
||||
DatePipe,
|
||||
RecipeListComponent,
|
||||
],
|
||||
animations: [
|
||||
trigger('inOutAnimation', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate('1s ease-out', style({ opacity: 1 })),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class RecipeDetailsComponent implements OnInit {
|
||||
title: string = 'Recipe Detail';
|
||||
|
||||
recipeDetail$!: Observable<RecipeDetail>;
|
||||
|
||||
isLoaded: boolean = false;
|
||||
isMatLoaded: boolean = false;
|
||||
|
||||
actionRecord: ActionRecord<RecipeDetail | RecipeDetailMat> =
|
||||
new ActionRecord();
|
||||
|
||||
recipeOriginalDetail!: typeof this.recipeDetailForm.value;
|
||||
|
||||
commit_msg :string = "";
|
||||
|
||||
constructor(
|
||||
private _formBuilder: FormBuilder,
|
||||
private _route: ActivatedRoute,
|
||||
private _router: Router,
|
||||
private _recipeService: RecipeService,
|
||||
private _userService: UserService
|
||||
) {}
|
||||
|
||||
productCode!: string;
|
||||
|
||||
recipeDetailForm = this._formBuilder.group({
|
||||
productCode: '',
|
||||
name: '',
|
||||
otherName: '',
|
||||
Description: '',
|
||||
otherDescription: '',
|
||||
lastModified: new Date(),
|
||||
price: 0,
|
||||
isUse: false,
|
||||
isShow: false,
|
||||
disable: false,
|
||||
});
|
||||
|
||||
repl = []
|
||||
|
||||
ngOnInit() {
|
||||
this.productCode = this._route.snapshot.params['productCode'];
|
||||
|
||||
this.recipeDetail$ = this._recipeService
|
||||
.getRecipeDetail(this.productCode)
|
||||
.pipe(first());
|
||||
this.recipeDetail$.subscribe((detail) => {
|
||||
|
||||
console.log('Recipe Detail', detail);
|
||||
|
||||
this.recipeDetailForm.patchValue(detail);
|
||||
this.isLoaded = true;
|
||||
this.recipeOriginalDetail = { ...this.recipeDetailForm.getRawValue() };
|
||||
});
|
||||
|
||||
this.recipeDetailForm.valueChanges.subscribe(this.onRecipeDetailFormChange);
|
||||
|
||||
// snap recipe detail form value
|
||||
|
||||
this.actionRecord.registerOnAddAction((currAction, allAction) => {
|
||||
if (currAction.type === 'recipeListData') {
|
||||
switch (currAction.action) {
|
||||
case 'add':
|
||||
break;
|
||||
case 'delete':
|
||||
break;
|
||||
}
|
||||
}
|
||||
console.log('Action Record', allAction);
|
||||
});
|
||||
}
|
||||
|
||||
showConfirmSaveModal: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
showConfirmCloseModal: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
|
||||
confirmSave = {
|
||||
title: 'The changes detected!',
|
||||
message: 'Do you want to save changes?',
|
||||
confirmCallBack: () => {
|
||||
console.log('confirm save');
|
||||
|
||||
// get username
|
||||
let username:string = ""
|
||||
this._userService.getCurrentUser().subscribe((user) => {
|
||||
username = user.user.name;
|
||||
|
||||
|
||||
|
||||
let to_send = {
|
||||
edit_by: username,
|
||||
commit_msg: this.commit_msg,
|
||||
productCode: this.productCode,
|
||||
name: this.recipeDetailForm.getRawValue().name,
|
||||
otherName: this.recipeDetailForm.getRawValue().otherName,
|
||||
Description: this.recipeDetailForm.getRawValue().Description,
|
||||
otherDescription: this.recipeDetailForm.getRawValue().otherDescription,
|
||||
LastChange: this.recipeDetailForm.getRawValue().lastModified,
|
||||
price: this.recipeDetailForm.getRawValue().price,
|
||||
// isUse: this,
|
||||
// isShow: null,
|
||||
// disable: null,
|
||||
recipes: [
|
||||
...this.repl
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
// TODO: update value in targeted recipe
|
||||
this._recipeService.editChanges(
|
||||
this._recipeService.getCurrentCountry(),
|
||||
this._recipeService.getCurrentFile(),
|
||||
{
|
||||
...to_send,
|
||||
}
|
||||
);
|
||||
console.log('Sending changes');
|
||||
this._router.navigate(['/recipes']);
|
||||
})
|
||||
|
||||
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
onKeyUpCommitMsg(e: any){
|
||||
this.commit_msg = e.target.value;
|
||||
}
|
||||
|
||||
confirmClose = {
|
||||
title: 'The changes will be lost!',
|
||||
message: 'Do you want to close without saving?',
|
||||
confirmCallBack: () => {
|
||||
console.log('confirm close');
|
||||
this._router.navigate(['/recipes']);
|
||||
},
|
||||
};
|
||||
|
||||
onPressConfirmSave() {
|
||||
if (this.isValueChanged) {
|
||||
this.showConfirmSaveModal.emit(true);
|
||||
} else {
|
||||
this._router.navigate(['/recipes']);
|
||||
}
|
||||
}
|
||||
|
||||
onPressConfirmClose() {
|
||||
if (this.isValueChanged) {
|
||||
this.showConfirmCloseModal.emit(true);
|
||||
} else {
|
||||
this._router.navigate(['/recipes']);
|
||||
}
|
||||
}
|
||||
|
||||
isValueChanged: boolean = false;
|
||||
|
||||
onRecipeDetailFormChange(recipeDetail: typeof this.recipeDetailForm.value) {
|
||||
console.log('Recipe Detail Form Changed', recipeDetail);
|
||||
}
|
||||
|
||||
onRecipeListFormChange(repl: unknown[]) {
|
||||
console.log('Recipe List Form Changed', repl);
|
||||
this.repl = repl as never[];
|
||||
this.isValueChanged ||= repl != undefined ? true : false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,57 +1,57 @@
|
|||
<table class="table" [formGroup]="recipeListForm">
|
||||
<thead>
|
||||
<tr class="bg-gray-200">
|
||||
<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 Name</th>
|
||||
<th class="px-6 py-3">MixOrder</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 Time</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">Water Cold</th>
|
||||
<th class="px-6 py-3">Water Yield</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
formArrayName="recipeListData"
|
||||
*ngFor="let mat of recipeListData.controls; let i = index"
|
||||
>
|
||||
<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">
|
||||
<input type="checkbox" class="toggle" formControlName="isUse" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="materialID" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="name" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="mixOrder" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="powderGram" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="powderTime" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="syrupGram" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="syrupTime" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="waterCold" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="waterYield" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="stirTime" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table" [formGroup]="recipeListForm">
|
||||
<thead>
|
||||
<tr class="bg-gray-200">
|
||||
<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 Name</th>
|
||||
<th class="px-6 py-3">MixOrder</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 Time</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">Water Cold</th>
|
||||
<th class="px-6 py-3">Water Yield</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
formArrayName="recipeListData"
|
||||
*ngFor="let mat of recipeListData.controls; let i = index"
|
||||
>
|
||||
<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">
|
||||
<input type="checkbox" class="toggle" formControlName="isUse" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="materialPathId" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="name" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="mixOrder" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="powderGram" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="powderTime" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="syrupGram" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="syrupTime" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="waterCold" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="waterYield" />
|
||||
</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap">
|
||||
<input type="text" class="input" formControlName="stirTime" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,90 +1,102 @@
|
|||
import { NgFor, NgIf } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import {
|
||||
FormArray,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms';
|
||||
import { isEqual, sortBy } from 'lodash';
|
||||
import { first } from 'rxjs';
|
||||
import {
|
||||
RecipeDetail,
|
||||
RecipeDetailMat,
|
||||
} from 'src/app/core/models/recipe.model';
|
||||
import { RecipeService } from 'src/app/core/services/recipe.service';
|
||||
import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipe-list',
|
||||
templateUrl: './recipe-list.component.html',
|
||||
standalone: true,
|
||||
imports: [NgIf, NgFor, ReactiveFormsModule],
|
||||
})
|
||||
export class RecipeListComponent implements OnInit {
|
||||
@Input({ required: true }) productCode!: string;
|
||||
@Output() recipeListFormChange = new EventEmitter<boolean>();
|
||||
|
||||
isMatLoaded: boolean = false;
|
||||
|
||||
constructor(
|
||||
private _recipeService: RecipeService,
|
||||
private _formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
recipeListForm = this._formBuilder.group(
|
||||
{
|
||||
recipeListData: this._formBuilder.array([]),
|
||||
},
|
||||
{ updateOn: 'blur' }
|
||||
);
|
||||
|
||||
private _recipeListOriginalArray!: RecipeDetailMat[];
|
||||
|
||||
ngOnInit(): void {
|
||||
this._recipeService
|
||||
.getRecipeDetailMat(this.productCode)
|
||||
.pipe(first())
|
||||
.subscribe(({ result }) => {
|
||||
this._recipeListOriginalArray = result;
|
||||
result.forEach((recipeDetailMat: RecipeDetailMat) => {
|
||||
this.recipeListData.push(
|
||||
this._formBuilder.group({
|
||||
isUse: recipeDetailMat.isUse,
|
||||
materialID: recipeDetailMat.materialID,
|
||||
name: [{ value: recipeDetailMat.name, disabled: true }],
|
||||
mixOrder: recipeDetailMat.mixOrder,
|
||||
stirTime: recipeDetailMat.stirTime,
|
||||
powderGram: recipeDetailMat.powderGram,
|
||||
powderTime: recipeDetailMat.powderTime,
|
||||
syrupGram: recipeDetailMat.syrupGram,
|
||||
syrupTime: recipeDetailMat.syrupTime,
|
||||
waterCold: recipeDetailMat.waterCold,
|
||||
waterYield: recipeDetailMat.waterYield,
|
||||
})
|
||||
);
|
||||
});
|
||||
this.isMatLoaded = true;
|
||||
});
|
||||
|
||||
this.recipeListForm.valueChanges.subscribe((value) => {
|
||||
console.log(value.recipeListData);
|
||||
console.log(this._recipeListOriginalArray);
|
||||
if (
|
||||
!isEqual(
|
||||
sortBy(value, 'materialID'),
|
||||
sortBy(this._recipeListOriginalArray, 'materialID')
|
||||
)
|
||||
) {
|
||||
this.recipeListFormChange.emit(true);
|
||||
} else {
|
||||
this.recipeListFormChange.emit(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get recipeListData(): FormArray {
|
||||
return this.recipeListForm.get('recipeListData') as FormArray;
|
||||
}
|
||||
}
|
||||
import { NgFor, NgIf } from '@angular/common';
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import {
|
||||
FormArray,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms';
|
||||
import { forEach, isEqual, sortBy } from 'lodash';
|
||||
import { first } from 'rxjs';
|
||||
import {
|
||||
RecipeDetail,
|
||||
RecipeDetailMat,
|
||||
} from 'src/app/core/models/recipe.model';
|
||||
import { RecipeService } from 'src/app/core/services/recipe.service';
|
||||
import { Action, ActionRecord } from 'src/app/shared/actionRecord/actionRecord';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipe-list',
|
||||
templateUrl: './recipe-list.component.html',
|
||||
standalone: true,
|
||||
imports: [NgIf, NgFor, ReactiveFormsModule],
|
||||
})
|
||||
export class RecipeListComponent implements OnInit {
|
||||
@Input({ required: true }) productCode!: string;
|
||||
@Output() recipeListFormChange = new EventEmitter<unknown[]>();
|
||||
|
||||
isMatLoaded: boolean = false;
|
||||
|
||||
constructor(
|
||||
private _recipeService: RecipeService,
|
||||
private _formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
recipeListForm = this._formBuilder.group(
|
||||
{
|
||||
recipeListData: this._formBuilder.array([]),
|
||||
},
|
||||
{ updateOn: 'blur' }
|
||||
);
|
||||
|
||||
private _recipeListOriginalArray!: RecipeDetailMat[];
|
||||
|
||||
ngOnInit(): void {
|
||||
this._recipeService
|
||||
.getRecipeDetailMat(this.productCode)
|
||||
.pipe(first())
|
||||
.subscribe(({ result }) => {
|
||||
this._recipeListOriginalArray = result;
|
||||
result.forEach((recipeDetailMat: RecipeDetailMat) => {
|
||||
this.recipeListData.push(
|
||||
this._formBuilder.group({
|
||||
isUse: recipeDetailMat.isUse,
|
||||
materialPathId: recipeDetailMat.materialPathId,
|
||||
name: [{ value: recipeDetailMat.name, disabled: true }],
|
||||
mixOrder: recipeDetailMat.mixOrder,
|
||||
stirTime: recipeDetailMat.stirTime,
|
||||
powderGram: recipeDetailMat.powderGram,
|
||||
powderTime: recipeDetailMat.powderTime,
|
||||
syrupGram: recipeDetailMat.syrupGram,
|
||||
syrupTime: recipeDetailMat.syrupTime,
|
||||
waterCold: recipeDetailMat.waterCold,
|
||||
waterYield: recipeDetailMat.waterYield,
|
||||
})
|
||||
);
|
||||
});
|
||||
this.isMatLoaded = true;
|
||||
});
|
||||
|
||||
this.recipeListForm.valueChanges.subscribe((value) => {
|
||||
console.log(value.recipeListData);
|
||||
console.log(this._recipeListOriginalArray);
|
||||
if (
|
||||
!isEqual(
|
||||
sortBy(value, 'materialID'),
|
||||
sortBy(this._recipeListOriginalArray, 'materialID')
|
||||
)
|
||||
) {
|
||||
|
||||
let emitted_res: any[] = []
|
||||
|
||||
// force type change. temporary solution
|
||||
forEach(value.recipeListData!, (recipeDetailMat: any) => {
|
||||
recipeDetailMat.materialPathId = parseInt(recipeDetailMat.materialPathId!)
|
||||
emitted_res.push(recipeDetailMat)
|
||||
})
|
||||
|
||||
this.recipeListFormChange.emit(emitted_res as unknown[]);
|
||||
} else {
|
||||
this.recipeListFormChange.emit([]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
get recipeListData(): FormArray {
|
||||
return this.recipeListForm.get('recipeListData') as FormArray;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,288 +1,312 @@
|
|||
<main
|
||||
class="relative overflow-auto max-h-[80%] h-[88vh] shadow-md sm:rounded-lg"
|
||||
#table
|
||||
>
|
||||
<table class="table">
|
||||
<caption class="p-5 text-lg font-semibold text-left text-gray-900">
|
||||
<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-col">
|
||||
<span
|
||||
>Recipe Version {{ recipesDashboard.configNumber }} |
|
||||
{{ recipesDashboard.filename }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col ml-5">
|
||||
<button
|
||||
class="btn bg-primary btn-md"
|
||||
onclick="select_file_modal.showModal()"
|
||||
>
|
||||
<span class="text-base text-gray-700">เลือก Recipe ไฟล์</span>
|
||||
</button>
|
||||
<dialog id="select_file_modal" class="modal">
|
||||
<div
|
||||
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>
|
||||
<div class="flex flex-row gap-5">
|
||||
<div class="dropdown dropdown-end">
|
||||
<input
|
||||
#countryInput
|
||||
type="search"
|
||||
tabindex="1"
|
||||
placeholder="Select Country"
|
||||
class="input input-bordered input-sm w-full max-w-xs"
|
||||
[value]="selectedCountry"
|
||||
(input)="setCountryFilter($event)"
|
||||
(focus)="getRecipeCountries()"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-content z-[1000] min-w-[200px] max-h-[500px] overflow-y-auto"
|
||||
>
|
||||
<ul
|
||||
tabindex="1"
|
||||
class="menu p-2 shadow bg-base-100 rounded-box w-auto"
|
||||
>
|
||||
<li *ngFor="let country of recipeCountryFiltered">
|
||||
<a (click)="countrySelected(country)">{{
|
||||
country
|
||||
}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="isCountrySelected" class="dropdown dropdown-end">
|
||||
<input
|
||||
#fileInput
|
||||
type="search"
|
||||
tabindex="1"
|
||||
placeholder="เลือก Recipe File"
|
||||
class="input input-bordered input-sm w-full max-w-xs"
|
||||
(input)="setFileFilter($event)"
|
||||
(focus)="getRecipeFiles()"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-content z-[1000] min-w-[200px] max-h-[500px] overflow-y-auto"
|
||||
>
|
||||
<ul
|
||||
tabindex="1"
|
||||
class="menu p-2 shadow bg-base-100 rounded-box w-auto"
|
||||
>
|
||||
<li *ngFor="let file of recipeFileCountries">
|
||||
<a (click)="loadRecipe(file)">{{ file }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<!-- <div class="dropdown dropdown-end">
|
||||
<input
|
||||
type="text"
|
||||
tabindex="0"
|
||||
placeholder="เลือก Recipe File"
|
||||
class="input input-bordered input-sm w-full max-w-xs"
|
||||
(input)="setRecipeVersion($event)"
|
||||
(focus)="getRecipeVersions()"
|
||||
/>
|
||||
<div class="dropdown-content z-[1] max-h-[500px] overflow-y-auto">
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu p-2 shadow bg-base-100 rounded-box w-auto"
|
||||
>
|
||||
<li *ngFor="let recipeVersion of recipeVersions">
|
||||
<a (click)="loadRecipe(recipeVersion)">{{
|
||||
recipeVersion
|
||||
}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<div class="flex flex-col ml-auto">
|
||||
<span class=""
|
||||
>Last Updated:
|
||||
{{
|
||||
recipesDashboard.configNumber | date : "dd-MMM-yyyy hh:mm:ss"
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="relativ sm:rounded-lg">
|
||||
<div
|
||||
class="flex flex-col items-center justify-between p-4 space-y-3 md:flex-row md:space-y-0 md:space-x-4"
|
||||
>
|
||||
<div class="join">
|
||||
<input
|
||||
class="input input-bordered join-item w-[300px] bg-white"
|
||||
placeholder="Product Code, Name or Other Name"
|
||||
(input)="setSearch($event)"
|
||||
(keydown.enter)="search($event)"
|
||||
/>
|
||||
<ng-select
|
||||
[items]="materialList"
|
||||
class="join-item text-base"
|
||||
bindLabel="name"
|
||||
bindValue="id"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="false"
|
||||
[(ngModel)]="selectMaterialFilter"
|
||||
>
|
||||
<ng-template ng-option-tmp let-item="item">
|
||||
<p class="text-xs">{{ item.name }}</p>
|
||||
<small class="text-xs text-gray-500">{{ item.id }}</small>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
<button class="btn join-item" (click)="search($event)">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button class="btn rounded-lg" (click)="openJsonTab()">
|
||||
View JSON
|
||||
</button>
|
||||
<button class="btn rounded-lg">
|
||||
<svg
|
||||
class="w-6 h-6 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 20"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
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>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</caption>
|
||||
<thead class="text-xs sticky top-0 text-gray-700 uppercase bg-secondary">
|
||||
<tr>
|
||||
<th>
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox" />
|
||||
</label>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3" *ngFor="let header of tableHeads">
|
||||
<div class="flex items-center uppercase">
|
||||
{{ header }}
|
||||
<a href="#"
|
||||
><svg
|
||||
class="w-3 h-3 ml-1.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
*ngFor="let recipe of recipeOverviewList"
|
||||
class="bg-white la border-b hover:bg-secondary"
|
||||
>
|
||||
<th>
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox" />
|
||||
</label>
|
||||
</th>
|
||||
<td
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"
|
||||
>
|
||||
{{ recipe.productCode }}
|
||||
</td>
|
||||
<td
|
||||
class="px-6 max-w-fit py-4 font-medium text-gray-900 whitespace-nowrap"
|
||||
>
|
||||
{{ recipe.name }}
|
||||
</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">
|
||||
{{ recipe.lastUpdated | date : "dd-MMM-yyyy hh:mm:ss" }}
|
||||
</td>
|
||||
<td class="px-4 py-4 flex">
|
||||
<!-- <recipe-modal productCode="{{ recipe.productCode }}"></recipe-modal> -->
|
||||
<a
|
||||
routerLink="/recipe/{{ recipe.productCode }}"
|
||||
class="btn btn-ghost"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
|
||||
<button
|
||||
class="btn btn-circle fixed z-100 bottom-5 right-1"
|
||||
(click)="scrollToTop()"
|
||||
>
|
||||
^
|
||||
</button>
|
||||
</main>
|
||||
<main
|
||||
class="relative overflow-auto max-h-[80%] h-[88vh] shadow-md sm:rounded-lg"
|
||||
#table
|
||||
>
|
||||
<table class="table">
|
||||
<caption class="p-5 text-lg font-semibold text-left text-gray-900">
|
||||
<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-col">
|
||||
<span
|
||||
>Recipe Version {{ recipesDashboard.configNumber }} |
|
||||
{{ recipesDashboard.filename }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col ml-5">
|
||||
<button
|
||||
class="btn bg-primary btn-md"
|
||||
onclick="select_file_modal.showModal()"
|
||||
>
|
||||
<span class="text-base text-gray-700">เลือก Recipe ไฟล์</span>
|
||||
</button>
|
||||
<dialog id="select_file_modal" class="modal">
|
||||
<div
|
||||
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>
|
||||
<div class="flex flex-row gap-5">
|
||||
<div class="dropdown dropdown-end">
|
||||
<input
|
||||
#countryInput
|
||||
type="search"
|
||||
tabindex="1"
|
||||
placeholder="Select Country"
|
||||
class="input input-bordered input-sm w-full max-w-xs"
|
||||
[value]="selectedCountry"
|
||||
(input)="setCountryFilter($event)"
|
||||
(focus)="getRecipeCountries()"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-content z-[1000] min-w-[200px] max-h-[500px] overflow-y-auto"
|
||||
>
|
||||
<ul
|
||||
tabindex="1"
|
||||
class="menu p-2 shadow bg-base-100 rounded-box w-auto"
|
||||
>
|
||||
<li *ngFor="let country of recipeCountryFiltered">
|
||||
<a (click)="countrySelected(country)">{{
|
||||
country
|
||||
}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="isCountrySelected" class="dropdown dropdown-end">
|
||||
<input
|
||||
#fileInput
|
||||
type="search"
|
||||
tabindex="1"
|
||||
placeholder="เลือก Recipe File"
|
||||
class="input input-bordered input-sm w-full max-w-xs"
|
||||
(input)="setFileFilter($event)"
|
||||
(focus)="getRecipeFiles()"
|
||||
/>
|
||||
<div
|
||||
class="dropdown-content z-[1000] min-w-[200px] max-h-[500px] overflow-y-auto"
|
||||
>
|
||||
<ul
|
||||
tabindex="1"
|
||||
class="menu p-2 shadow bg-base-100 rounded-box w-auto"
|
||||
>
|
||||
<li *ngFor="let file of recipeFileCountries">
|
||||
<a (click)="loadRecipe(file)">{{ file }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<!-- <div class="dropdown dropdown-end">
|
||||
<input
|
||||
type="text"
|
||||
tabindex="0"
|
||||
placeholder="เลือก Recipe File"
|
||||
class="input input-bordered input-sm w-full max-w-xs"
|
||||
(input)="setRecipeVersion($event)"
|
||||
(focus)="getRecipeVersions()"
|
||||
/>
|
||||
<div class="dropdown-content z-[1] max-h-[500px] overflow-y-auto">
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu p-2 shadow bg-base-100 rounded-box w-auto"
|
||||
>
|
||||
<li *ngFor="let recipeVersion of recipeVersions">
|
||||
<a (click)="loadRecipe(recipeVersion)">{{
|
||||
recipeVersion
|
||||
}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<button class="btn bg-primary btn-md border-2" *ngIf="savedTmpfiles.length > 0">
|
||||
<span class="text-base text-gray-700" onclick="select_savefile_modal.showModal()">โหลดเซฟ</span>
|
||||
</button>
|
||||
<!-- todo: add modal -->
|
||||
<dialog id="select_savefile_modal" class="modal">
|
||||
<div class="modal-box max-w-[600px] overflow-visible">
|
||||
<p class="font-bold text-lg m-2">Saved Files</p>
|
||||
<table class="table">
|
||||
<tr class="bg-primary ">
|
||||
<th>Name</th>
|
||||
<th>Comment</th>
|
||||
<!-- <th></th> -->
|
||||
</tr>
|
||||
<tr class="row hover:bg-secondary" *ngFor="let file of savedTmpfiles">
|
||||
<td>{{ file }}</td>
|
||||
<td>"-"</td>
|
||||
<button class="btn bg-blue-400">Select</button>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
|
||||
<div class="flex flex-col ml-auto">
|
||||
<span class=""
|
||||
>Last Updated:
|
||||
{{
|
||||
recipesDashboard.configNumber | date : "dd-MMM-yyyy hh:mm:ss"
|
||||
}}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="relativ sm:rounded-lg">
|
||||
<div
|
||||
class="flex flex-col items-center justify-between p-4 space-y-3 md:flex-row md:space-y-0 md:space-x-4"
|
||||
>
|
||||
<div class="join">
|
||||
<input
|
||||
class="input input-bordered join-item w-[300px] bg-white"
|
||||
placeholder="Product Code, Name or Other Name"
|
||||
(input)="setSearch($event)"
|
||||
(keydown.enter)="search($event)"
|
||||
/>
|
||||
<ng-select
|
||||
[items]="materialList"
|
||||
class="join-item text-base"
|
||||
bindLabel="name"
|
||||
bindValue="id"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="false"
|
||||
[(ngModel)]="selectMaterialFilter"
|
||||
>
|
||||
<ng-template ng-option-tmp let-item="item">
|
||||
<p class="text-xs">{{ item.name }}</p>
|
||||
<small class="text-xs text-gray-500">{{ item.id }}</small>
|
||||
</ng-template>
|
||||
</ng-select>
|
||||
<button class="btn join-item" (click)="search($event)">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button class="btn rounded-lg" (click)="openJsonTab()">
|
||||
View JSON
|
||||
</button>
|
||||
<button class="btn rounded-lg">
|
||||
<svg
|
||||
class="w-6 h-6 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 18 20"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
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>
|
||||
Export
|
||||
</button>
|
||||
<button class="btn rounded-lg"> Upgrade to {{ recipesDashboard.configNumber + 1 }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</caption>
|
||||
<thead class="text-xs sticky top-0 text-gray-700 uppercase bg-secondary">
|
||||
<tr>
|
||||
<th>
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox" />
|
||||
</label>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3" *ngFor="let header of tableHeads">
|
||||
<div class="flex items-center uppercase">
|
||||
{{ header }}
|
||||
<a href="#"
|
||||
><svg
|
||||
class="w-3 h-3 ml-1.5"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
*ngFor="let recipe of recipeOverviewList"
|
||||
class="bg-white la border-b hover:bg-secondary"
|
||||
>
|
||||
<th>
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox" />
|
||||
</label>
|
||||
</th>
|
||||
<td
|
||||
scope="row"
|
||||
class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap"
|
||||
>
|
||||
{{ recipe.productCode }}
|
||||
</td>
|
||||
<td
|
||||
class="px-6 max-w-fit py-4 font-medium text-gray-900 whitespace-nowrap"
|
||||
>
|
||||
{{ recipe.name }}
|
||||
</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">
|
||||
{{ recipe.lastUpdated | date : "dd-MMM-yyyy hh:mm:ss" }}
|
||||
</td>
|
||||
<td class="px-4 py-4 flex">
|
||||
<!-- <recipe-modal productCode="{{ recipe.productCode }}"></recipe-modal> -->
|
||||
<a
|
||||
routerLink="/recipe/{{ recipe.productCode }}"
|
||||
class="btn btn-ghost"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-gray-800 dark:text-white"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1"
|
||||
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"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,335 +1,356 @@
|
|||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import {
|
||||
Recipe,
|
||||
Recipe01,
|
||||
RecipeOverview,
|
||||
RecipesDashboard,
|
||||
} from 'src/app/core/models/recipe.model';
|
||||
import { RecipeService } from 'src/app/core/services/recipe.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { RecipeModalComponent } from 'src/app/shared/modal/recipe-details/recipe-modal.component';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
Subscription,
|
||||
finalize,
|
||||
map,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import * as lodash from 'lodash';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MaterialService } from 'src/app/core/services/material.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipes',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
DatePipe,
|
||||
RecipeModalComponent,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
],
|
||||
templateUrl: './recipes.component.html',
|
||||
})
|
||||
export class RecipesComponent implements OnInit, OnDestroy {
|
||||
recipesDashboard$!: Observable<RecipesDashboard>;
|
||||
recipeOverviewList!: RecipeOverview[];
|
||||
selectMaterialFilter: number[] | null = null;
|
||||
materialList: { id: number; name: string | number }[] | null = null;
|
||||
|
||||
tableHeads: string[] = [
|
||||
'Product Code',
|
||||
'Name',
|
||||
'Other Name',
|
||||
'Description',
|
||||
'Last Updated',
|
||||
];
|
||||
private offset = 0;
|
||||
private take = 20;
|
||||
|
||||
// isLoaded: boolean = false;
|
||||
isLoadMore: boolean = true;
|
||||
isHasMore: boolean = true;
|
||||
|
||||
private searchStr = '';
|
||||
private oldSearchStr = '';
|
||||
|
||||
tableCtx?: ElementRef;
|
||||
|
||||
@ViewChild('table', { static: false }) set content(table: ElementRef) {
|
||||
// expose element ref for other fn
|
||||
this.tableCtx = table;
|
||||
|
||||
table.nativeElement.addEventListener(
|
||||
'scroll',
|
||||
() => {
|
||||
if (this.isHasMore === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = table.nativeElement;
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - 10;
|
||||
if (isBottom && !this.isLoadMore) {
|
||||
this.isLoadMore = true;
|
||||
this._recipeService
|
||||
.getRecipeOverview({
|
||||
offset: this.offset,
|
||||
take: this.take,
|
||||
search: this.oldSearchStr,
|
||||
filename: this._recipeService.getCurrentFile(),
|
||||
country: this._recipeService.getCurrentCountry(),
|
||||
materialIds: this.selectMaterialFilter || [],
|
||||
})
|
||||
.subscribe(({ result, hasMore, totalCount }) => {
|
||||
if (this.recipeOverviewList) {
|
||||
this.recipeOverviewList =
|
||||
this.recipeOverviewList.concat(result);
|
||||
} else {
|
||||
this.recipeOverviewList = result;
|
||||
}
|
||||
this.offset += 10;
|
||||
this.isHasMore = hasMore;
|
||||
this.isLoadMore = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _recipeService: RecipeService,
|
||||
private _materialService: MaterialService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.recipesDashboard$ = this._recipeService
|
||||
.getRecipesDashboard({
|
||||
filename: this._recipeService.getCurrentFile(),
|
||||
country: this._recipeService.getCurrentCountry(),
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this._recipeService
|
||||
.getRecipeOverview({
|
||||
offset: this.offset,
|
||||
take: this.take,
|
||||
search: this.oldSearchStr,
|
||||
filename: this._recipeService.getCurrentFile(),
|
||||
country: this._recipeService.getCurrentCountry(),
|
||||
materialIds: this.selectMaterialFilter || [],
|
||||
})
|
||||
.subscribe(({ result, hasMore, totalCount }) => {
|
||||
this.recipeOverviewList = result;
|
||||
this.offset += 10;
|
||||
this.isHasMore = hasMore;
|
||||
this.isLoadMore = false;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this._materialService
|
||||
.getMaterialCodes()
|
||||
.pipe(
|
||||
map((mat) =>
|
||||
mat.map((m) => ({
|
||||
id: m.materialID,
|
||||
name: m.PackageDescription || m.materialID,
|
||||
}))
|
||||
)
|
||||
)
|
||||
.subscribe((materials) => {
|
||||
this.materialList = materials;
|
||||
});
|
||||
|
||||
this.initRecipeSelection();
|
||||
}
|
||||
|
||||
setSearch(event: Event) {
|
||||
this.searchStr = (event.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
search(event: Event) {
|
||||
this.offset = 0;
|
||||
this.isLoadMore = true;
|
||||
this.oldSearchStr = this.searchStr;
|
||||
this._recipeService
|
||||
.getRecipeOverview({
|
||||
offset: this.offset,
|
||||
take: this.take,
|
||||
search: this.oldSearchStr,
|
||||
filename: this._recipeService.getCurrentFile(),
|
||||
country: this._recipeService.getCurrentCountry(),
|
||||
materialIds: this.selectMaterialFilter || [],
|
||||
})
|
||||
.subscribe(({ result, hasMore, totalCount }) => {
|
||||
this.recipeOverviewList = result;
|
||||
this.offset += 10;
|
||||
this.isHasMore = hasMore;
|
||||
this.isLoadMore = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Recipe Version selection
|
||||
currentCountryFilter: BehaviorSubject<string> = new BehaviorSubject<string>(
|
||||
''
|
||||
);
|
||||
currentFileFilter: BehaviorSubject<string> = new BehaviorSubject<string>('');
|
||||
recipeCountryFiltered: string[] = [];
|
||||
recipeFileCountries: string[] = [];
|
||||
|
||||
currentCountryFilterSubScription: Subscription | null = null;
|
||||
currentFileFilterSubScription: Subscription | null = null;
|
||||
|
||||
selectedCountry: string | null = null;
|
||||
isCountrySelected: boolean = false;
|
||||
|
||||
private countryInputEl?: ElementRef<HTMLInputElement>;
|
||||
private fileInputEl?: ElementRef<HTMLInputElement>;
|
||||
|
||||
@ViewChild('countryInput', { static: false }) set countryInput(
|
||||
countryInput: ElementRef<HTMLInputElement>
|
||||
) {
|
||||
this.countryInputEl = countryInput;
|
||||
}
|
||||
|
||||
@ViewChild('fileInput', { static: false }) set fileInput(
|
||||
fileInput: ElementRef<HTMLInputElement>
|
||||
) {
|
||||
this.fileInputEl = fileInput;
|
||||
}
|
||||
|
||||
private firstTimeOpenModal = true;
|
||||
|
||||
initRecipeSelection() {
|
||||
if (this._recipeService.getRecipeFileCountries().length == 0) {
|
||||
this._recipeService.getRecipeCountries().subscribe((countries) => {
|
||||
this.recipeCountryFiltered = countries;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setCountryFilter(event: Event) {
|
||||
this.currentCountryFilter.next((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
setFileFilter(event: Event) {
|
||||
this.currentFileFilter.next((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
getRecipeCountries() {
|
||||
if (this.firstTimeOpenModal) {
|
||||
this.countryInputEl!.nativeElement.blur();
|
||||
this.firstTimeOpenModal = false;
|
||||
}
|
||||
this.currentCountryFilterSubScription = this.currentCountryFilter.subscribe(
|
||||
(c) => {
|
||||
const countries = this._recipeService.getRecipeFileCountries();
|
||||
if (countries.length > 0) {
|
||||
this.recipeCountryFiltered = lodash.filter(countries, (country) =>
|
||||
country.toLowerCase().includes(c.toLowerCase())
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getRecipeFiles() {
|
||||
this.currentFileFilterSubScription = this.currentFileFilter.subscribe(
|
||||
(c) => {
|
||||
if (this.selectedCountry === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileNames = this._recipeService.getRecipeFileNames(
|
||||
this.selectedCountry
|
||||
);
|
||||
if (fileNames && fileNames.length > 0) {
|
||||
this.recipeFileCountries = lodash.filter(fileNames, (file) =>
|
||||
file.toLowerCase().includes(c.toLowerCase())
|
||||
);
|
||||
} else {
|
||||
this._recipeService
|
||||
.getRecipeFiles(this.selectedCountry)
|
||||
.subscribe((files) => {
|
||||
this.recipeFileCountries = lodash.filter(files, (file) =>
|
||||
file.toLowerCase().includes(c.toLowerCase())
|
||||
);
|
||||
});
|
||||
}
|
||||
console.log(this.recipeFileCountries);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
countrySelected(country: string) {
|
||||
this.selectedCountry = country;
|
||||
this.isCountrySelected = true;
|
||||
localStorage.setItem('currentRecipeCountry', country);
|
||||
}
|
||||
|
||||
loadRecipe(recipeFileName: string) {
|
||||
// clear all recipes
|
||||
this.offset = 0;
|
||||
this.isHasMore = true;
|
||||
this.isLoadMore = true;
|
||||
this.oldSearchStr = '';
|
||||
localStorage.setItem('currentRecipeFile', recipeFileName);
|
||||
|
||||
this.recipesDashboard$ = this._recipeService.getRecipesDashboard({
|
||||
filename: recipeFileName,
|
||||
country: this.selectedCountry!,
|
||||
});
|
||||
|
||||
this._recipeService
|
||||
.getRecipeOverview({
|
||||
offset: this.offset,
|
||||
take: this.take,
|
||||
search: this.oldSearchStr,
|
||||
filename: recipeFileName,
|
||||
country: this.selectedCountry!,
|
||||
materialIds: this.selectMaterialFilter || [],
|
||||
})
|
||||
.subscribe(({ result, hasMore, totalCount }) => {
|
||||
this.recipeOverviewList = result;
|
||||
this.offset += 10;
|
||||
this.isHasMore = hasMore;
|
||||
this.isLoadMore = false;
|
||||
});
|
||||
}
|
||||
|
||||
// end of Recipe Version selection
|
||||
|
||||
openJsonTab() {
|
||||
window.open(
|
||||
environment.api +
|
||||
`/recipes/${this._recipeService.getCurrentCountry()}/${this._recipeService.getCurrentFile()}/json`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
const table = this.tableCtx!.nativeElement;
|
||||
table.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.currentCountryFilterSubScription) {
|
||||
this.currentCountryFilterSubScription.unsubscribe();
|
||||
}
|
||||
if (this.currentFileFilterSubScription) {
|
||||
this.currentFileFilterSubScription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { CommonModule, DatePipe } from '@angular/common';
|
||||
import {
|
||||
Recipe,
|
||||
Recipe01,
|
||||
RecipeOverview,
|
||||
RecipesDashboard,
|
||||
} from 'src/app/core/models/recipe.model';
|
||||
import { RecipeService } from 'src/app/core/services/recipe.service';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { RecipeModalComponent } from 'src/app/shared/modal/recipe-details/recipe-modal.component';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
Subscription,
|
||||
finalize,
|
||||
map,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
import * as lodash from 'lodash';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NgSelectModule } from '@ng-select/ng-select';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MaterialService } from 'src/app/core/services/material.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recipes',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
DatePipe,
|
||||
RecipeModalComponent,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
],
|
||||
templateUrl: './recipes.component.html',
|
||||
})
|
||||
export class RecipesComponent implements OnInit, OnDestroy {
|
||||
recipesDashboard$!: Observable<RecipesDashboard>;
|
||||
recipeOverviewList!: RecipeOverview[];
|
||||
selectMaterialFilter: number[] | null = null;
|
||||
materialList: { id: number; name: string | number }[] | null = null;
|
||||
|
||||
tableHeads: string[] = [
|
||||
'Product Code',
|
||||
'Name',
|
||||
'Other Name',
|
||||
'Description',
|
||||
'Last Updated',
|
||||
];
|
||||
private offset = 0;
|
||||
private take = 20;
|
||||
|
||||
// isLoaded: boolean = false;
|
||||
isLoadMore: boolean = true;
|
||||
isHasMore: boolean = true;
|
||||
|
||||
private searchStr = '';
|
||||
private oldSearchStr = '';
|
||||
|
||||
savedTmpfiles: string[] = [];
|
||||
|
||||
tableCtx?: ElementRef;
|
||||
|
||||
@ViewChild('table', { static: false }) set content(table: ElementRef) {
|
||||
// expose element ref for other fn
|
||||
this.tableCtx = table;
|
||||
|
||||
table.nativeElement.addEventListener(
|
||||
'scroll',
|
||||
() => {
|
||||
if (this.isHasMore === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, clientHeight } = table.nativeElement;
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight - 10;
|
||||
if (isBottom && !this.isLoadMore) {
|
||||
this.isLoadMore = true;
|
||||
this._recipeService
|
||||
.getRecipeOverview({
|
||||
offset: this.offset,
|
||||
take: this.take,
|
||||
search: this.oldSearchStr,
|
||||
filename: this._recipeService.getCurrentFile(),
|
||||
country: this._recipeService.getCurrentCountry(),
|
||||
materialIds: this.selectMaterialFilter || [],
|
||||
})
|
||||
.subscribe(({ result, hasMore, totalCount }) => {
|
||||
if (this.recipeOverviewList) {
|
||||
this.recipeOverviewList =
|
||||
this.recipeOverviewList.concat(result);
|
||||
} else {
|
||||
this.recipeOverviewList = result;
|
||||
}
|
||||
this.offset += 10;
|
||||
this.isHasMore = hasMore;
|
||||
this.isLoadMore = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _recipeService: RecipeService,
|
||||
private _materialService: MaterialService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.recipesDashboard$ = this._recipeService
|
||||
.getRecipesDashboard({
|
||||
filename: this._recipeService.getCurrentFile(),
|
||||
country: this._recipeService.getCurrentCountry(),
|
||||
})
|
||||
.pipe(
|
||||
finalize(() => {
|
||||
this._recipeService
|
||||
.getRecipeOverview({
|
||||
offset: this.offset,
|
||||
take: this.take,
|
||||
search: this.oldSearchStr,
|
||||
filename: this._recipeService.getCurrentFile(),
|
||||
country: this._recipeService.getCurrentCountry(),
|
||||
materialIds: this.selectMaterialFilter || [],
|
||||
})
|
||||
.subscribe(({ result, hasMore, totalCount }) => {
|
||||
this.recipeOverviewList = result;
|
||||
this.offset += 10;
|
||||
this.isHasMore = hasMore;
|
||||
this.isLoadMore = false;
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
this._recipeService.getSavedTmp(
|
||||
this._recipeService.getCurrentCountry(),
|
||||
this._recipeService.getCurrentFile()
|
||||
).subscribe({
|
||||
next: (files:any) => {
|
||||
console.log("Obtain saves: ", typeof files);
|
||||
if(files != undefined && typeof files === 'object'){
|
||||
// console.log("Obtain saves object: ", files.files[0], typeof files);
|
||||
this.savedTmpfiles = files.files;
|
||||
}
|
||||
|
||||
},
|
||||
})
|
||||
|
||||
this._materialService
|
||||
.getMaterialCodes()
|
||||
.pipe(
|
||||
map((mat) =>
|
||||
mat.map((m) => ({
|
||||
id: m.materialID,
|
||||
name: m.PackageDescription || m.materialID,
|
||||
}))
|
||||
)
|
||||
)
|
||||
.subscribe((materials) => {
|
||||
this.materialList = materials;
|
||||
});
|
||||
|
||||
this.initRecipeSelection();
|
||||
}
|
||||
|
||||
setSearch(event: Event) {
|
||||
this.searchStr = (event.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
search(event: Event) {
|
||||
this.offset = 0;
|
||||
this.isLoadMore = true;
|
||||
this.oldSearchStr = this.searchStr;
|
||||
this._recipeService
|
||||
.getRecipeOverview({
|
||||
offset: this.offset,
|
||||
take: this.take,
|
||||
search: this.oldSearchStr,
|
||||
filename: this._recipeService.getCurrentFile(),
|
||||
country: this._recipeService.getCurrentCountry(),
|
||||
materialIds: this.selectMaterialFilter || [],
|
||||
})
|
||||
.subscribe(({ result, hasMore, totalCount }) => {
|
||||
this.recipeOverviewList = result;
|
||||
this.offset += 10;
|
||||
this.isHasMore = hasMore;
|
||||
this.isLoadMore = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Recipe Version selection
|
||||
currentCountryFilter: BehaviorSubject<string> = new BehaviorSubject<string>(
|
||||
''
|
||||
);
|
||||
currentFileFilter: BehaviorSubject<string> = new BehaviorSubject<string>('');
|
||||
recipeCountryFiltered: string[] = [];
|
||||
recipeFileCountries: string[] = [];
|
||||
|
||||
currentCountryFilterSubScription: Subscription | null = null;
|
||||
currentFileFilterSubScription: Subscription | null = null;
|
||||
|
||||
selectedCountry: string | null = null;
|
||||
isCountrySelected: boolean = false;
|
||||
|
||||
private countryInputEl?: ElementRef<HTMLInputElement>;
|
||||
private fileInputEl?: ElementRef<HTMLInputElement>;
|
||||
|
||||
@ViewChild('countryInput', { static: false }) set countryInput(
|
||||
countryInput: ElementRef<HTMLInputElement>
|
||||
) {
|
||||
this.countryInputEl = countryInput;
|
||||
}
|
||||
|
||||
@ViewChild('fileInput', { static: false }) set fileInput(
|
||||
fileInput: ElementRef<HTMLInputElement>
|
||||
) {
|
||||
this.fileInputEl = fileInput;
|
||||
}
|
||||
|
||||
private firstTimeOpenModal = true;
|
||||
|
||||
initRecipeSelection() {
|
||||
if (this._recipeService.getRecipeFileCountries().length == 0) {
|
||||
this._recipeService.getRecipeCountries().subscribe((countries) => {
|
||||
this.recipeCountryFiltered = countries;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setCountryFilter(event: Event) {
|
||||
this.currentCountryFilter.next((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
setFileFilter(event: Event) {
|
||||
this.currentFileFilter.next((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
getRecipeCountries() {
|
||||
if (this.firstTimeOpenModal) {
|
||||
this.countryInputEl!.nativeElement.blur();
|
||||
this.firstTimeOpenModal = false;
|
||||
}
|
||||
this.currentCountryFilterSubScription = this.currentCountryFilter.subscribe(
|
||||
(c) => {
|
||||
const countries = this._recipeService.getRecipeFileCountries();
|
||||
if (countries.length > 0) {
|
||||
this.recipeCountryFiltered = lodash.filter(countries, (country) =>
|
||||
country.toLowerCase().includes(c.toLowerCase())
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getRecipeFiles() {
|
||||
this.currentFileFilterSubScription = this.currentFileFilter.subscribe(
|
||||
(c) => {
|
||||
if (this.selectedCountry === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileNames = this._recipeService.getRecipeFileNames(
|
||||
this.selectedCountry
|
||||
);
|
||||
if (fileNames && fileNames.length > 0) {
|
||||
this.recipeFileCountries = lodash.filter(fileNames, (file) =>
|
||||
file.toLowerCase().includes(c.toLowerCase())
|
||||
);
|
||||
} else {
|
||||
this._recipeService
|
||||
.getRecipeFiles(this.selectedCountry)
|
||||
.subscribe((files) => {
|
||||
this.recipeFileCountries = lodash.filter(files, (file) =>
|
||||
file.toLowerCase().includes(c.toLowerCase())
|
||||
);
|
||||
});
|
||||
}
|
||||
console.log(this.recipeFileCountries);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
countrySelected(country: string) {
|
||||
this.selectedCountry = country;
|
||||
this.isCountrySelected = true;
|
||||
localStorage.setItem('currentRecipeCountry', country);
|
||||
}
|
||||
|
||||
loadRecipe(recipeFileName: string) {
|
||||
// clear all recipes
|
||||
this.offset = 0;
|
||||
this.isHasMore = true;
|
||||
this.isLoadMore = true;
|
||||
this.oldSearchStr = '';
|
||||
localStorage.setItem('currentRecipeFile', recipeFileName);
|
||||
|
||||
this.recipesDashboard$ = this._recipeService.getRecipesDashboard({
|
||||
filename: recipeFileName,
|
||||
country: this.selectedCountry!,
|
||||
});
|
||||
|
||||
this._recipeService
|
||||
.getRecipeOverview({
|
||||
offset: this.offset,
|
||||
take: this.take,
|
||||
search: this.oldSearchStr,
|
||||
filename: recipeFileName,
|
||||
country: this.selectedCountry!,
|
||||
materialIds: this.selectMaterialFilter || [],
|
||||
})
|
||||
.subscribe(({ result, hasMore, totalCount }) => {
|
||||
this.recipeOverviewList = result;
|
||||
this.offset += 10;
|
||||
this.isHasMore = hasMore;
|
||||
this.isLoadMore = false;
|
||||
});
|
||||
}
|
||||
|
||||
// end of Recipe Version selection
|
||||
|
||||
openJsonTab() {
|
||||
window.open(
|
||||
environment.api +
|
||||
`/recipes/${this._recipeService.getCurrentCountry()}/${this._recipeService.getCurrentFile()}/json`,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
|
||||
// get tmp files
|
||||
openTmpFilesList(){
|
||||
// TODO: get tmp files to display or
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
const table = this.tableCtx!.nativeElement;
|
||||
table.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.currentCountryFilterSubScription) {
|
||||
this.currentCountryFilterSubScription.unsubscribe();
|
||||
}
|
||||
if (this.currentFileFilterSubScription) {
|
||||
this.currentFileFilterSubScription.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
82
server/data/commit.go
Normal file
82
server/data/commit.go
Normal 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
|
||||
}
|
||||
|
|
@ -1,58 +1,180 @@
|
|||
package helpers
|
||||
|
||||
import "fmt"
|
||||
|
||||
// DynamicCompare compares two values dynamically and returns true if they are equal.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - s: The first value to compare.
|
||||
//
|
||||
// - u: The second value to compare.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// - bool: True if the values are equal, false otherwise.
|
||||
//
|
||||
// - error: An error if the values cannot be compared.
|
||||
func DynamicCompare(s interface{}, u interface{}) (bool, error) {
|
||||
switch t := s.(type) {
|
||||
case bool:
|
||||
u, ok := u.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("[bool] cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return t == u, nil
|
||||
case string:
|
||||
u, ok := u.(string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("[string] cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return t == u, nil
|
||||
case int:
|
||||
u, ok := u.(int)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("[int] cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return t == u, nil
|
||||
case float64:
|
||||
u, ok := u.(float64)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("[float64] cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return t == u, nil
|
||||
case nil:
|
||||
return t == u, nil
|
||||
case []interface{}:
|
||||
for i := range t {
|
||||
if ok, err := DynamicCompare(t[i], u); err != nil {
|
||||
return false, err
|
||||
} else if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false, fmt.Errorf("[unknown] not in case. Cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return false, fmt.Errorf("[unknown] unexpected error")
|
||||
}
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"recipe-manager/services/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
Log = logger.GetInstance()
|
||||
)
|
||||
|
||||
// DynamicCompare compares two values dynamically and returns true if they are equal.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// - s: The first value to compare.
|
||||
//
|
||||
// - u: The second value to compare.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// - bool: True if the values are equal, false otherwise.
|
||||
//
|
||||
// - error: An error if the values cannot be compared.
|
||||
func DynamicCompare(s interface{}, u interface{}) (bool, error) {
|
||||
switch t := s.(type) {
|
||||
case bool:
|
||||
u, ok := u.(bool)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("[bool] cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return t == u, nil
|
||||
case string:
|
||||
u, ok := u.(string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("[string] cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return t == u, nil
|
||||
case int:
|
||||
u, ok := u.(int)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("[int] cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return t == u, nil
|
||||
case float64:
|
||||
u, ok := u.(float64)
|
||||
// Log.Debug("[helpers] DynamicCompare", zap.Any("u", u), zap.Any("ok", ok), zap.Any("test_compare*(t==u)", t == u))
|
||||
|
||||
if t == u {
|
||||
return t == u, nil
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return false, fmt.Errorf("[float64] cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
return t == u, nil
|
||||
case nil:
|
||||
return t == u, nil
|
||||
case []interface{}:
|
||||
for i := range t {
|
||||
if ok, err := DynamicCompare(t[i], u); err != nil {
|
||||
return false, err
|
||||
} else if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
break
|
||||
case map[string]interface{}:
|
||||
for _, v := range t {
|
||||
if ok, err := DynamicCompare(v, u); err != nil {
|
||||
return false, err
|
||||
} else if !ok {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
break
|
||||
default:
|
||||
return false, fmt.Errorf("[unknown] not in case. Cannot compare %T and %T, %v and %v", t, u, s, u)
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
return false, fmt.Errorf("[empty] the compared value is nil")
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("[unknown] unexpected error. [old] %v and [new] %v", s, u)
|
||||
}
|
||||
|
||||
func GetTempFile(filename string, user string, suffix int) string {
|
||||
|
||||
// Check if the temp file exist
|
||||
_, err := os.Stat(filename)
|
||||
|
||||
// Log.Debug("[helpers] GetTempFile", zap.Any("filename", filename), zap.Any("suffix", suffix), zap.Any("err", err))
|
||||
|
||||
// file not exists
|
||||
if os.IsNotExist(err) {
|
||||
// Create temp file
|
||||
if suffix == 0 {
|
||||
Log.Debug("[helpers] Suffix 0 GetTempFile", zap.Any("filename", filename))
|
||||
return strings.Replace(filename, ".json", "_"+user+".tmp"+strconv.Itoa(suffix), 1)
|
||||
}
|
||||
|
||||
// change extension from json to tmp
|
||||
filename = strings.Replace(filename, ".json", "_"+user+".tmp"+strconv.Itoa(suffix), 1)
|
||||
Log.Debug("[helpers] GetTempFile", zap.Any("filename", filename))
|
||||
return filename
|
||||
} else {
|
||||
|
||||
if strings.Contains(filename, ".tmp") {
|
||||
return GetTempFile(strings.Replace(filename, "_"+user+".tmp"+strconv.Itoa(suffix-1), "_"+user+".tmp"+strconv.Itoa(suffix), 1), user, suffix+1)
|
||||
}
|
||||
|
||||
// recursive call
|
||||
return GetTempFile(strings.Replace(filename, ".json", "_"+user+".tmp"+strconv.Itoa(suffix), 1), user, suffix+1)
|
||||
}
|
||||
}
|
||||
|
||||
// func PackTempToRealFile(data *data.Data, countryID string, filename string) {
|
||||
|
||||
// // list file that end with .tmp*
|
||||
// files, err := filepath.Glob(filename + ".tmp*")
|
||||
|
||||
// // for all files, read and get configNumber
|
||||
|
||||
// if err != nil {
|
||||
// Log.Error("[helpers] PackTempToRealFile", zap.Error(err))
|
||||
// }
|
||||
|
||||
// // get configNumber from actual filename.json
|
||||
|
||||
// //
|
||||
// base_recipe := data.GetRecipe(countryID, filename)
|
||||
|
||||
// // read file and apply tmp file from 0 to tmpX.
|
||||
// // - if there is more than 1 user that access this file at the same time,
|
||||
// // pack in order, and if conflict, stop
|
||||
|
||||
// if len(files) == 0 {
|
||||
// return
|
||||
// }
|
||||
|
||||
// // TODO: must check the changes
|
||||
|
||||
// for _, file := range files {
|
||||
// var tmpdata models.Recipe
|
||||
// tmpfile, err := os.Open(file)
|
||||
// if err != nil {
|
||||
// return
|
||||
// }
|
||||
// _ = json.NewDecoder(tmpfile).Decode(&tmpdata)
|
||||
// // apply change
|
||||
|
||||
// // = tmpdata.Recipe01
|
||||
|
||||
// for key, val := range tmpdata.Recipe01 {
|
||||
|
||||
// test_bol, err := DynamicCompare(base_recipe.Recipe01[key], val)
|
||||
|
||||
// if err != nil {
|
||||
// Log.Error("[helpers] PackTempToRealFile", zap.Error(err))
|
||||
// }
|
||||
|
||||
// if !test_bol {
|
||||
// base_recipe.Recipe01[key] = val
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// // verify changes between tmpX and actual filename.json
|
||||
|
||||
// // if changes, rename tmpX to filename (version +1) .json
|
||||
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,159 +1,160 @@
|
|||
package models
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Recipe struct {
|
||||
Timestamp string `json:"Timestamp"`
|
||||
MachineSetting MachineSetting `json:"MachineSetting"`
|
||||
Recipe01 []Recipe01 `json:"Recipe01"`
|
||||
Topping Topping `json:"Topping"`
|
||||
MaterialCode []MaterialCode `json:"MaterialCode"`
|
||||
MaterialSetting []MaterialSetting `json:"MaterialSetting"`
|
||||
}
|
||||
|
||||
type MachineSetting struct {
|
||||
RecipeTag string `json:"RecipeTag"`
|
||||
StrTextShowError []string `json:"strTextShowError"`
|
||||
ConfigNumber int `json:"configNumber"`
|
||||
TemperatureMax int `json:"temperatureMax"`
|
||||
TemperatureMin int `json:"temperatureMin"`
|
||||
}
|
||||
|
||||
type MaterialCode struct {
|
||||
PackageDescription string `json:"PackageDescription"`
|
||||
RefillValuePerStep int `json:"RefillValuePerStep"`
|
||||
MaterialID uint64 `json:"materialID"`
|
||||
MaterialCode string `json:"materialCode"`
|
||||
}
|
||||
|
||||
type MaterialSetting struct {
|
||||
AlarmIDWhenOffline int `json:"AlarmIDWhenOffline"`
|
||||
BeanChannel bool `json:"BeanChannel"`
|
||||
CanisterType string `json:"CanisterType"`
|
||||
DrainTimer int `json:"DrainTimer"`
|
||||
IsEquipment bool `json:"IsEquipment"`
|
||||
LeavesChannel bool `json:"LeavesChannel"`
|
||||
LowToOffline int `json:"LowToOffline"`
|
||||
MaterialStatus int `json:"MaterialStatus"`
|
||||
PowderChannel bool `json:"PowderChannel"`
|
||||
RefillUnitGram bool `json:"RefillUnitGram"`
|
||||
RefillUnitMilliliters bool `json:"RefillUnitMilliliters"`
|
||||
RefillUnitPCS bool `json:"RefillUnitPCS"`
|
||||
ScheduleDrainType int `json:"ScheduleDrainType"`
|
||||
SodaChannel bool `json:"SodaChannel"`
|
||||
StockAdjust int `json:"StockAdjust"`
|
||||
SyrupChannel bool `json:"SyrupChannel"`
|
||||
ID uint64 `json:"id"`
|
||||
IDAlternate int `json:"idAlternate"`
|
||||
IsUse bool `json:"isUse"`
|
||||
PayRettryMaxCount int `json:"pay_rettry_max_count"`
|
||||
FeedMode string `json:"feed_mode"`
|
||||
MaterialParameter string `json:"MaterialParameter"`
|
||||
}
|
||||
|
||||
type Recipe01 struct {
|
||||
Description string `json:"Description"`
|
||||
ExtendID int `json:"ExtendID"`
|
||||
OnTOP bool `json:"OnTOP"`
|
||||
LastChange string `json:"LastChange"`
|
||||
MenuStatus int `json:"MenuStatus"`
|
||||
RemainingCups json.Number `json:"RemainingCups"`
|
||||
StringParam string `json:"StringParam"`
|
||||
TextForWarningBeforePay []string `json:"TextForWarningBeforePay"`
|
||||
CashPrice int `json:"cashPrice"`
|
||||
Changerecipe string `json:"changerecipe"`
|
||||
Disable bool `json:"disable"`
|
||||
Disable_by_cup bool `json:"disable_by_cup"`
|
||||
Disable_by_ice bool `json:"disable_by_ice"`
|
||||
EncoderCount int `json:"EncoderCount"`
|
||||
ID int `json:"id"`
|
||||
IsUse bool `json:"isUse"`
|
||||
IsShow bool `json:"isShow"`
|
||||
Name string `json:"name"`
|
||||
NonCashPrice int `json:"nonCashPrice"`
|
||||
OtherDescription string `json:"otherDescription"`
|
||||
OtherName string `json:"otherName"`
|
||||
ProductCode string `json:"productCode"`
|
||||
Recipes []MatRecipe `json:"recipes"`
|
||||
SubMenu []Recipe01 `json:"SubMenu"`
|
||||
ToppingSet []ToppingSet `json:"ToppingSet"`
|
||||
Total_time int `json:"total_time"`
|
||||
Total_weight int `json:"total_weight"`
|
||||
UriData string `json:"uriData"`
|
||||
UseGram bool `json:"useGram"`
|
||||
Weight_float int `json:"weight_float"`
|
||||
}
|
||||
|
||||
func (r *Recipe01) ToMap() map[string]interface{} {
|
||||
var m map[string]interface{}
|
||||
recipeRecord, _ := json.Marshal(r)
|
||||
json.Unmarshal(recipeRecord, &m)
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *Recipe01) FromMap(m map[string]interface{}) Recipe01 {
|
||||
recipeRecord, _ := json.Marshal(m)
|
||||
json.Unmarshal(recipeRecord, &r)
|
||||
return *r
|
||||
}
|
||||
|
||||
type MatRecipe struct {
|
||||
MixOrder int `json:"MixOrder"`
|
||||
FeedParameter int `json:"FeedParameter"`
|
||||
FeedPattern int `json:"FeedPattern"`
|
||||
IsUse bool `json:"isUse"`
|
||||
MaterialPathId int `json:"materialPathId"`
|
||||
PowderGram int `json:"powderGram"`
|
||||
PowderTime int `json:"powderTime"`
|
||||
StirTime int `json:"stirTime"`
|
||||
SyrupGram int `json:"syrupGram"`
|
||||
SyrupTime int `json:"syrupTime"`
|
||||
WaterCold int `json:"waterCold"`
|
||||
WaterYield int `json:"waterYield"`
|
||||
}
|
||||
|
||||
type ToppingSet struct {
|
||||
ListGroupID []int `json:"ListGroupID"`
|
||||
DefaultIDSelect int `json:"defaultIDSelect"`
|
||||
GroupID string `json:"groupID"`
|
||||
IsUse bool `json:"isUse"`
|
||||
}
|
||||
|
||||
type Topping struct {
|
||||
ToppingGroup []ToppingGroup `json:"ToppingGroup"`
|
||||
ToppingList []ToppingList `json:"ToppingList"`
|
||||
}
|
||||
|
||||
type ToppingGroup struct {
|
||||
Desc string `json:"Desc"`
|
||||
GroupID int `json:"groupID"`
|
||||
IDDefault int `json:"idDefault"`
|
||||
IDInGroup string `json:"idInGroup"`
|
||||
InUse bool `json:"inUse"`
|
||||
Name string `json:"name"`
|
||||
OtherName string `json:"otherName"`
|
||||
}
|
||||
|
||||
type ToppingList struct {
|
||||
ExtendID int `json:"ExtendID"`
|
||||
OnTOP bool `json:"OnTOP"`
|
||||
MenuStatus int `json:"MenuStatus"`
|
||||
CashPrice int `json:"cashPrice"`
|
||||
Disable bool `json:"disable"`
|
||||
Disable_by_cup bool `json:"disable_by_cup"`
|
||||
Disable_by_ice bool `json:"disable_by_ice"`
|
||||
EncoderCount int `json:"EncoderCount"`
|
||||
ID int `json:"id"`
|
||||
IsUse bool `json:"isUse"`
|
||||
IsShow bool `json:"isShow"`
|
||||
StringParam string `json:"stringParam"`
|
||||
Name string `json:"name"`
|
||||
NonCashPrice int `json:"nonCashPrice"`
|
||||
OtherName string `json:"otherName"`
|
||||
ProductCode string `json:"productCode"`
|
||||
Recipes []MatRecipe `json:"recipes"`
|
||||
Total_time int `json:"total_time"`
|
||||
Total_weight int `json:"total_weight"`
|
||||
UseGram bool `json:"useGram"`
|
||||
Weight_float int `json:"weight_float"`
|
||||
}
|
||||
package models
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Recipe struct {
|
||||
Timestamp string `json:"Timestamp"`
|
||||
MachineSetting MachineSetting `json:"MachineSetting"`
|
||||
Recipe01 []Recipe01 `json:"Recipe01"`
|
||||
Topping Topping `json:"Topping"`
|
||||
MaterialCode []MaterialCode `json:"MaterialCode"`
|
||||
MaterialSetting []MaterialSetting `json:"MaterialSetting"`
|
||||
}
|
||||
|
||||
type MachineSetting struct {
|
||||
RecipeTag string `json:"RecipeTag"`
|
||||
StrTextShowError []string `json:"strTextShowError"`
|
||||
ConfigNumber int `json:"configNumber"`
|
||||
Comment []string `json:"Comment"`
|
||||
TemperatureMax int `json:"temperatureMax"`
|
||||
TemperatureMin int `json:"temperatureMin"`
|
||||
}
|
||||
|
||||
type MaterialCode struct {
|
||||
PackageDescription string `json:"PackageDescription"`
|
||||
RefillValuePerStep int `json:"RefillValuePerStep"`
|
||||
MaterialID uint64 `json:"materialID"`
|
||||
MaterialCode string `json:"materialCode"`
|
||||
}
|
||||
|
||||
type MaterialSetting struct {
|
||||
AlarmIDWhenOffline int `json:"AlarmIDWhenOffline"`
|
||||
BeanChannel bool `json:"BeanChannel"`
|
||||
CanisterType string `json:"CanisterType"`
|
||||
DrainTimer int `json:"DrainTimer"`
|
||||
IsEquipment bool `json:"IsEquipment"`
|
||||
LeavesChannel bool `json:"LeavesChannel"`
|
||||
LowToOffline int `json:"LowToOffline"`
|
||||
MaterialStatus int `json:"MaterialStatus"`
|
||||
PowderChannel bool `json:"PowderChannel"`
|
||||
RefillUnitGram bool `json:"RefillUnitGram"`
|
||||
RefillUnitMilliliters bool `json:"RefillUnitMilliliters"`
|
||||
RefillUnitPCS bool `json:"RefillUnitPCS"`
|
||||
ScheduleDrainType int `json:"ScheduleDrainType"`
|
||||
SodaChannel bool `json:"SodaChannel"`
|
||||
StockAdjust int `json:"StockAdjust"`
|
||||
SyrupChannel bool `json:"SyrupChannel"`
|
||||
ID uint64 `json:"id"`
|
||||
IDAlternate int `json:"idAlternate"`
|
||||
IsUse bool `json:"isUse"`
|
||||
PayRettryMaxCount int `json:"pay_rettry_max_count"`
|
||||
FeedMode string `json:"feed_mode"`
|
||||
MaterialParameter string `json:"MaterialParameter"`
|
||||
}
|
||||
|
||||
type Recipe01 struct {
|
||||
Description string `json:"Description"`
|
||||
ExtendID int `json:"ExtendID"`
|
||||
OnTOP bool `json:"OnTOP"`
|
||||
LastChange string `json:"LastChange"`
|
||||
MenuStatus int `json:"MenuStatus"`
|
||||
RemainingCups json.Number `json:"RemainingCups"`
|
||||
StringParam string `json:"StringParam"`
|
||||
TextForWarningBeforePay []string `json:"TextForWarningBeforePay"`
|
||||
CashPrice int `json:"cashPrice"`
|
||||
Changerecipe string `json:"changerecipe"`
|
||||
Disable bool `json:"disable"`
|
||||
Disable_by_cup bool `json:"disable_by_cup"`
|
||||
Disable_by_ice bool `json:"disable_by_ice"`
|
||||
EncoderCount int `json:"EncoderCount"`
|
||||
ID int `json:"id"`
|
||||
IsUse bool `json:"isUse"`
|
||||
IsShow bool `json:"isShow"`
|
||||
Name string `json:"name"`
|
||||
NonCashPrice int `json:"nonCashPrice"`
|
||||
OtherDescription string `json:"otherDescription"`
|
||||
OtherName string `json:"otherName"`
|
||||
ProductCode string `json:"productCode"`
|
||||
Recipes []MatRecipe `json:"recipes"`
|
||||
SubMenu []Recipe01 `json:"SubMenu"`
|
||||
ToppingSet []ToppingSet `json:"ToppingSet"`
|
||||
Total_time int `json:"total_time"`
|
||||
Total_weight int `json:"total_weight"`
|
||||
UriData string `json:"uriData"`
|
||||
UseGram bool `json:"useGram"`
|
||||
Weight_float int `json:"weight_float"`
|
||||
}
|
||||
|
||||
func (r *Recipe01) ToMap() map[string]interface{} {
|
||||
var m map[string]interface{}
|
||||
recipeRecord, _ := json.Marshal(r)
|
||||
json.Unmarshal(recipeRecord, &m)
|
||||
return m
|
||||
}
|
||||
|
||||
func (r *Recipe01) FromMap(m map[string]interface{}) Recipe01 {
|
||||
recipeRecord, _ := json.Marshal(m)
|
||||
json.Unmarshal(recipeRecord, &r)
|
||||
return *r
|
||||
}
|
||||
|
||||
type MatRecipe struct {
|
||||
MixOrder int `json:"MixOrder"`
|
||||
FeedParameter int `json:"FeedParameter"`
|
||||
FeedPattern int `json:"FeedPattern"`
|
||||
IsUse bool `json:"isUse"`
|
||||
MaterialPathId int `json:"materialPathId"`
|
||||
PowderGram int `json:"powderGram"`
|
||||
PowderTime int `json:"powderTime"`
|
||||
StirTime int `json:"stirTime"`
|
||||
SyrupGram int `json:"syrupGram"`
|
||||
SyrupTime int `json:"syrupTime"`
|
||||
WaterCold int `json:"waterCold"`
|
||||
WaterYield int `json:"waterYield"`
|
||||
}
|
||||
|
||||
type ToppingSet struct {
|
||||
ListGroupID []int `json:"ListGroupID"`
|
||||
DefaultIDSelect int `json:"defaultIDSelect"`
|
||||
GroupID string `json:"groupID"`
|
||||
IsUse bool `json:"isUse"`
|
||||
}
|
||||
|
||||
type Topping struct {
|
||||
ToppingGroup []ToppingGroup `json:"ToppingGroup"`
|
||||
ToppingList []ToppingList `json:"ToppingList"`
|
||||
}
|
||||
|
||||
type ToppingGroup struct {
|
||||
Desc string `json:"Desc"`
|
||||
GroupID int `json:"groupID"`
|
||||
IDDefault int `json:"idDefault"`
|
||||
IDInGroup string `json:"idInGroup"`
|
||||
InUse bool `json:"inUse"`
|
||||
Name string `json:"name"`
|
||||
OtherName string `json:"otherName"`
|
||||
}
|
||||
|
||||
type ToppingList struct {
|
||||
ExtendID int `json:"ExtendID"`
|
||||
OnTOP bool `json:"OnTOP"`
|
||||
MenuStatus int `json:"MenuStatus"`
|
||||
CashPrice int `json:"cashPrice"`
|
||||
Disable bool `json:"disable"`
|
||||
Disable_by_cup bool `json:"disable_by_cup"`
|
||||
Disable_by_ice bool `json:"disable_by_ice"`
|
||||
EncoderCount int `json:"EncoderCount"`
|
||||
ID int `json:"id"`
|
||||
IsUse bool `json:"isUse"`
|
||||
IsShow bool `json:"isShow"`
|
||||
StringParam string `json:"stringParam"`
|
||||
Name string `json:"name"`
|
||||
NonCashPrice int `json:"nonCashPrice"`
|
||||
OtherName string `json:"otherName"`
|
||||
ProductCode string `json:"productCode"`
|
||||
Recipes []MatRecipe `json:"recipes"`
|
||||
Total_time int `json:"total_time"`
|
||||
Total_weight int `json:"total_weight"`
|
||||
UseGram bool `json:"useGram"`
|
||||
Weight_float int `json:"weight_float"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,326 +1,400 @@
|
|||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"recipe-manager/contracts"
|
||||
"recipe-manager/data"
|
||||
"recipe-manager/helpers"
|
||||
"recipe-manager/models"
|
||||
"recipe-manager/services/logger"
|
||||
"recipe-manager/services/recipe"
|
||||
"recipe-manager/services/sheet"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type RecipeRouter struct {
|
||||
data *data.Data
|
||||
sheetService sheet.SheetService
|
||||
recipeService recipe.RecipeService
|
||||
}
|
||||
|
||||
var (
|
||||
Log = logger.GetInstance()
|
||||
)
|
||||
|
||||
func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter {
|
||||
return &RecipeRouter{
|
||||
data: data,
|
||||
recipeService: recipeService,
|
||||
sheetService: sheetService,
|
||||
}
|
||||
}
|
||||
|
||||
func (rr *RecipeRouter) Route(r chi.Router) {
|
||||
r.Route("/recipes", func(r chi.Router) {
|
||||
r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
country := r.URL.Query().Get("country")
|
||||
filename := r.URL.Query().Get("filename")
|
||||
|
||||
result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{
|
||||
Country: country,
|
||||
Filename: filename,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
r.Get("/overview", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
var take, offset uint64 = 10, 0
|
||||
if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil {
|
||||
offset = newOffset
|
||||
}
|
||||
|
||||
if newTake, err := strconv.ParseUint(r.URL.Query().Get("take"), 10, 64); err == nil {
|
||||
take = newTake
|
||||
}
|
||||
|
||||
country := r.URL.Query().Get("country")
|
||||
filename := r.URL.Query().Get("filename")
|
||||
materialIds := r.URL.Query().Get("materialIds")
|
||||
|
||||
var materialIdsUint []int
|
||||
for _, v := range strings.Split(materialIds, ",") {
|
||||
materialIdUint, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil || materialIdUint == 0 {
|
||||
continue
|
||||
}
|
||||
materialIdsUint = append(materialIdsUint, int(materialIdUint))
|
||||
}
|
||||
|
||||
result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{
|
||||
Take: int(take),
|
||||
Skip: int(offset),
|
||||
Search: r.URL.Query().Get("search"),
|
||||
Country: country,
|
||||
Filename: filename,
|
||||
MatIds: materialIdsUint,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
productCode := chi.URLParam(r, "product_code")
|
||||
|
||||
// recipe := rr.data.GetRecipe01()
|
||||
// recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
|
||||
|
||||
// var recipeResult *models.Recipe01
|
||||
// recipeMetaDataResult := map[string]string{}
|
||||
|
||||
// for _, v := range recipe {
|
||||
// if v.ProductCode == productCode {
|
||||
// recipeResult = &v
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// for _, v := range recipeMetaData {
|
||||
// if v[0].(string) == productCode {
|
||||
// recipeMetaDataResult = map[string]string{
|
||||
// "productCode": v[0].(string),
|
||||
// "name": v[1].(string),
|
||||
// "otherName": v[2].(string),
|
||||
// "description": v[3].(string),
|
||||
// "otherDescription": v[4].(string),
|
||||
// "picture": v[5].(string),
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// if recipeResult == nil {
|
||||
// http.Error(w, "Not Found", http.StatusNotFound)
|
||||
// return
|
||||
// }
|
||||
|
||||
// json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
// "recipe": recipeResult,
|
||||
// "recipeMetaData": recipeMetaDataResult,
|
||||
// })
|
||||
|
||||
result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{
|
||||
Filename: r.URL.Query().Get("filename"),
|
||||
Country: r.URL.Query().Get("country"),
|
||||
ProductCode: productCode,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
productCode := chi.URLParam(r, "product_code")
|
||||
|
||||
result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{
|
||||
Filename: r.URL.Query().Get("filename"),
|
||||
Country: r.URL.Query().Get("country"),
|
||||
ProductCode: productCode,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) {
|
||||
country := chi.URLParam(r, "country")
|
||||
filename := chi.URLParam(r, "filename")
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
countryID, err := rr.data.GetCountryIDByName(country)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(rr.data.GetRecipe(countryID, filename))
|
||||
})
|
||||
|
||||
r.Get("/versions", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
// get key from map
|
||||
keys := []string{}
|
||||
for k := range rr.data.AllRecipeFiles {
|
||||
countryName, err := rr.data.GetCountryNameByID(k)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, countryName)
|
||||
}
|
||||
json.NewEncoder(w).Encode(keys)
|
||||
})
|
||||
|
||||
r.Get("/versions/{country}", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
countryName := chi.URLParam(r, "country")
|
||||
countryID, err := rr.data.GetCountryIDByName(countryName)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", countryName), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
files := []string{}
|
||||
for _, v := range rr.data.AllRecipeFiles[countryID] {
|
||||
files = append(files, v.Name)
|
||||
}
|
||||
json.NewEncoder(w).Encode(files)
|
||||
})
|
||||
|
||||
r.Get("/test/sheet", func(w http.ResponseWriter, r *http.Request) {
|
||||
result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
|
||||
|
||||
mapResult := []map[string]string{}
|
||||
|
||||
for _, v := range result {
|
||||
mapResult = append(mapResult, map[string]string{
|
||||
"productCode": v[0].(string),
|
||||
"name": v[1].(string),
|
||||
"otherName": v[2].(string),
|
||||
"description": v[3].(string),
|
||||
"otherDescription": v[4].(string),
|
||||
"picture": v[5].(string),
|
||||
})
|
||||
}
|
||||
json.NewEncoder(w).Encode(mapResult)
|
||||
})
|
||||
|
||||
r.Post("/edit/{country}/{filename}", func(w http.ResponseWriter, r *http.Request) {
|
||||
Log.Debug("Edit: ", zap.String("path", r.RequestURI))
|
||||
filename := chi.URLParam(r, "filename")
|
||||
country := chi.URLParam(r, "country")
|
||||
|
||||
countryID, err := rr.data.GetCountryIDByName(country)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
target_recipe := rr.data.GetRecipe(countryID, filename)
|
||||
|
||||
Log.Debug("Target => ", zap.Any("target", target_recipe.MachineSetting.ConfigNumber))
|
||||
|
||||
// check request structure
|
||||
|
||||
// FIXME: Request structure bug. Case-sensitive, likely bug at client
|
||||
// uncomment the below code to view the bug
|
||||
|
||||
// var change_request map[string]interface{}
|
||||
// err = json.NewDecoder(r.Body).Decode(&change_request)
|
||||
// if err != nil {
|
||||
// Log.Error("Decode in request failed: ", zap.Error(err))
|
||||
// }
|
||||
// Log.Debug("Request => ", zap.Any("request", change_request))
|
||||
|
||||
// Body
|
||||
var changes models.Recipe01
|
||||
err = json.NewDecoder(r.Body).Decode(&changes)
|
||||
if err != nil {
|
||||
Log.Error("Decode in request failed: ", zap.Error(err))
|
||||
}
|
||||
|
||||
Log.Debug("Changes: ", zap.Any("changes", changes))
|
||||
// TODO: find the matched pd
|
||||
target_menu, err := rr.data.GetRecipe01ByProductCode(filename, countryID, changes.ProductCode)
|
||||
|
||||
if err != nil {
|
||||
Log.Error("Error when get recipe by product code", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
menu_map := target_menu.ToMap()
|
||||
change_map := changes.ToMap()
|
||||
|
||||
// Find changes
|
||||
for key, val := range menu_map {
|
||||
|
||||
test_bool, err := helpers.DynamicCompare(val, change_map[key])
|
||||
|
||||
if err != nil {
|
||||
Log.Error("DynamicCompare in request failed: ", zap.Error(err))
|
||||
}
|
||||
|
||||
if !test_bool {
|
||||
menu_map[key] = change_map[key]
|
||||
}
|
||||
}
|
||||
|
||||
// Apply changes
|
||||
tempRecipe := models.Recipe01{}
|
||||
tempRecipe = tempRecipe.FromMap(menu_map)
|
||||
rr.data.SetValuesToRecipe(tempRecipe)
|
||||
Log.Debug("ApplyChange", zap.Any("status", "passed"))
|
||||
|
||||
// check if changed
|
||||
// Log.Debug("Check if changed", zap.Any("result", rr.data.GetRecipe01ByProductCode(changes.ProductCode)))
|
||||
|
||||
file, _ := os.Create(path.Join("./cofffeemachineConfig", countryID, filename))
|
||||
if err != nil {
|
||||
Log.Error("Error when tried to create file", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(rr.data.GetRecipe(countryID, filename))
|
||||
|
||||
if err != nil {
|
||||
Log.Error("Error when write file", zap.Error(err))
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
package routers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"recipe-manager/contracts"
|
||||
"recipe-manager/data"
|
||||
"recipe-manager/helpers"
|
||||
"recipe-manager/models"
|
||||
"recipe-manager/services/logger"
|
||||
"recipe-manager/services/recipe"
|
||||
"recipe-manager/services/sheet"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type RecipeRouter struct {
|
||||
data *data.Data
|
||||
sheetService sheet.SheetService
|
||||
recipeService recipe.RecipeService
|
||||
}
|
||||
|
||||
var (
|
||||
Log = logger.GetInstance()
|
||||
)
|
||||
|
||||
func NewRecipeRouter(data *data.Data, recipeService recipe.RecipeService, sheetService sheet.SheetService) *RecipeRouter {
|
||||
return &RecipeRouter{
|
||||
data: data,
|
||||
recipeService: recipeService,
|
||||
sheetService: sheetService,
|
||||
}
|
||||
}
|
||||
|
||||
func (rr *RecipeRouter) Route(r chi.Router) {
|
||||
r.Route("/recipes", func(r chi.Router) {
|
||||
r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
country := r.URL.Query().Get("country")
|
||||
filename := r.URL.Query().Get("filename")
|
||||
|
||||
result, err := rr.recipeService.GetRecipeDashboard(&contracts.RecipeDashboardRequest{
|
||||
Country: country,
|
||||
Filename: filename,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
r.Get("/overview", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
var take, offset uint64 = 10, 0
|
||||
if newOffset, err := strconv.ParseUint(r.URL.Query().Get("offset"), 10, 64); err == nil {
|
||||
offset = newOffset
|
||||
}
|
||||
|
||||
if newTake, err := strconv.ParseUint(r.URL.Query().Get("take"), 10, 64); err == nil {
|
||||
take = newTake
|
||||
}
|
||||
|
||||
country := r.URL.Query().Get("country")
|
||||
filename := r.URL.Query().Get("filename")
|
||||
materialIds := r.URL.Query().Get("materialIds")
|
||||
|
||||
var materialIdsUint []int
|
||||
for _, v := range strings.Split(materialIds, ",") {
|
||||
materialIdUint, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil || materialIdUint == 0 {
|
||||
continue
|
||||
}
|
||||
materialIdsUint = append(materialIdsUint, int(materialIdUint))
|
||||
}
|
||||
|
||||
result, err := rr.recipeService.GetRecipeOverview(&contracts.RecipeOverviewRequest{
|
||||
Take: int(take),
|
||||
Skip: int(offset),
|
||||
Search: r.URL.Query().Get("search"),
|
||||
Country: country,
|
||||
Filename: filename,
|
||||
MatIds: materialIdsUint,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
r.Get("/{product_code}", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
productCode := chi.URLParam(r, "product_code")
|
||||
|
||||
// recipe := rr.data.GetRecipe01()
|
||||
// recipeMetaData := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
|
||||
|
||||
// var recipeResult *models.Recipe01
|
||||
// recipeMetaDataResult := map[string]string{}
|
||||
|
||||
// for _, v := range recipe {
|
||||
// if v.ProductCode == productCode {
|
||||
// recipeResult = &v
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// for _, v := range recipeMetaData {
|
||||
// if v[0].(string) == productCode {
|
||||
// recipeMetaDataResult = map[string]string{
|
||||
// "productCode": v[0].(string),
|
||||
// "name": v[1].(string),
|
||||
// "otherName": v[2].(string),
|
||||
// "description": v[3].(string),
|
||||
// "otherDescription": v[4].(string),
|
||||
// "picture": v[5].(string),
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// if recipeResult == nil {
|
||||
// http.Error(w, "Not Found", http.StatusNotFound)
|
||||
// return
|
||||
// }
|
||||
|
||||
// json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
// "recipe": recipeResult,
|
||||
// "recipeMetaData": recipeMetaDataResult,
|
||||
// })
|
||||
|
||||
result, err := rr.recipeService.GetRecipeDetail(&contracts.RecipeDetailRequest{
|
||||
Filename: r.URL.Query().Get("filename"),
|
||||
Country: r.URL.Query().Get("country"),
|
||||
ProductCode: productCode,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
r.Get("/{product_code}/mat", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
productCode := chi.URLParam(r, "product_code")
|
||||
|
||||
result, err := rr.recipeService.GetRecipeDetailMat(&contracts.RecipeDetailRequest{
|
||||
Filename: r.URL.Query().Get("filename"),
|
||||
Country: r.URL.Query().Get("country"),
|
||||
ProductCode: productCode,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(result)
|
||||
})
|
||||
|
||||
r.Get("/{country}/{filename}/json", func(w http.ResponseWriter, r *http.Request) {
|
||||
country := chi.URLParam(r, "country")
|
||||
filename := chi.URLParam(r, "filename")
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
countryID, err := rr.data.GetCountryIDByName(country)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(rr.data.GetRecipe(countryID, filename))
|
||||
})
|
||||
|
||||
r.Get("/versions", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
// get key from map
|
||||
keys := []string{}
|
||||
for k := range rr.data.AllRecipeFiles {
|
||||
countryName, err := rr.data.GetCountryNameByID(k)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
keys = append(keys, countryName)
|
||||
}
|
||||
json.NewEncoder(w).Encode(keys)
|
||||
})
|
||||
|
||||
r.Get("/versions/{country}", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
countryName := chi.URLParam(r, "country")
|
||||
countryID, err := rr.data.GetCountryIDByName(countryName)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", countryName), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
files := []string{}
|
||||
for _, v := range rr.data.AllRecipeFiles[countryID] {
|
||||
files = append(files, v.Name)
|
||||
}
|
||||
json.NewEncoder(w).Encode(files)
|
||||
})
|
||||
|
||||
r.Get("/test/sheet", func(w http.ResponseWriter, r *http.Request) {
|
||||
result := rr.sheetService.GetSheet(r.Context(), "1rSUKcc5POR1KeZFGoeAZIoVoI7LPGztBhPw5Z_ConDE")
|
||||
|
||||
mapResult := []map[string]string{}
|
||||
|
||||
for _, v := range result {
|
||||
mapResult = append(mapResult, map[string]string{
|
||||
"productCode": v[0].(string),
|
||||
"name": v[1].(string),
|
||||
"otherName": v[2].(string),
|
||||
"Description": v[3].(string),
|
||||
"otherDescription": v[4].(string),
|
||||
"picture": v[5].(string),
|
||||
})
|
||||
}
|
||||
json.NewEncoder(w).Encode(mapResult)
|
||||
})
|
||||
|
||||
r.Post("/edit/{country}/{filename}", func(w http.ResponseWriter, r *http.Request) {
|
||||
Log.Debug("Edit: ", zap.String("path", r.RequestURI))
|
||||
filename := chi.URLParam(r, "filename")
|
||||
country := chi.URLParam(r, "country")
|
||||
|
||||
countryID, err := rr.data.GetCountryIDByName(country)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
target_recipe := rr.data.GetRecipe(countryID, filename)
|
||||
|
||||
Log.Debug("Target => ", zap.Any("target", target_recipe.MachineSetting.ConfigNumber))
|
||||
|
||||
// check request structure
|
||||
|
||||
// Body
|
||||
var ch_map map[string]interface{}
|
||||
var changes models.Recipe01
|
||||
|
||||
err = json.NewDecoder(r.Body).Decode(&ch_map)
|
||||
if err != nil {
|
||||
Log.Error("Decode in request failed: ", zap.Error(err))
|
||||
}
|
||||
|
||||
// commit request
|
||||
editor := ch_map["edit_by"].(string)
|
||||
Log.Debug("requester", zap.Any("editor", editor))
|
||||
commit_msg := ch_map["commit_msg"].(string)
|
||||
Log.Debug("commit_msg", zap.Any("commit_msg", commit_msg))
|
||||
|
||||
changes = changes.FromMap(ch_map)
|
||||
|
||||
Log.Debug("Changes: ", zap.Any("changes", changes))
|
||||
// TODO: find the matched pd
|
||||
target_menu, err := rr.data.GetRecipe01ByProductCode(filename, countryID, changes.ProductCode)
|
||||
|
||||
if err != nil {
|
||||
Log.Error("Error when get recipe by product code", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
menu_map := target_menu.ToMap()
|
||||
change_map := changes.ToMap()
|
||||
|
||||
// Find changes
|
||||
for key, val := range menu_map {
|
||||
|
||||
test_bool, err := helpers.DynamicCompare(val, change_map[key])
|
||||
|
||||
if err != nil {
|
||||
Log.Error("DynamicCompare in request failed: ", zap.Error(err))
|
||||
}
|
||||
|
||||
if !test_bool {
|
||||
menu_map[key] = change_map[key]
|
||||
}
|
||||
}
|
||||
|
||||
// Apply changes
|
||||
tempRecipe := models.Recipe01{}
|
||||
tempRecipe = tempRecipe.FromMap(menu_map)
|
||||
rr.data.SetValuesToRecipe(tempRecipe)
|
||||
Log.Debug("ApplyChange", zap.Any("status", "passed"))
|
||||
|
||||
// check if changed
|
||||
// Log.Debug("Check if changed", zap.Any("result", rr.data.GetRecipe01ByProductCode(changes.ProductCode)))
|
||||
|
||||
// target saved filename
|
||||
saved_filename := path.Join("./cofffeemachineConfig", countryID, filename)
|
||||
|
||||
// store @ temporary file
|
||||
temp_file_name := helpers.GetTempFile(saved_filename, editor, 0)
|
||||
|
||||
// TODO: push this change, editor, commit_msg into db
|
||||
|
||||
// gen hash
|
||||
commit_hash, err := data.HashCommit(8)
|
||||
|
||||
if err != nil {
|
||||
Log.Error("Error when hash commit", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
commit := data.CommitLog{
|
||||
|
||||
Id: commit_hash,
|
||||
Msg: commit_msg,
|
||||
Created_at: time.Now().Format("2006-01-02 15:04:05"),
|
||||
Editor: editor,
|
||||
Change_file: temp_file_name,
|
||||
}
|
||||
|
||||
err = data.Insert(&commit)
|
||||
Log.Debug("Commit", zap.Any("attr", commit))
|
||||
|
||||
if err != nil {
|
||||
Log.Error("Error when insert commit log", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
file, _ := os.Create(temp_file_name)
|
||||
if err != nil {
|
||||
Log.Error("Error when tried to create file", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
// err = encoder.Encode(rr.data.GetRecipe(countryID, temp_file_name))
|
||||
err = encoder.Encode(rr.data.GetRecipe(countryID, filename))
|
||||
|
||||
if err != nil {
|
||||
Log.Error("Error when write file", zap.Error(err))
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"commit_id": commit_hash,
|
||||
})
|
||||
})
|
||||
|
||||
// get saved files
|
||||
r.Get("/saved/{country}/{filename_version_only}", func(w http.ResponseWriter, r *http.Request) {
|
||||
file_version := chi.URLParam(r, "filename_version_only")
|
||||
country := chi.URLParam(r, "country")
|
||||
|
||||
countryID, err := rr.data.GetCountryIDByName(country)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("Country Name: %s not found!!!", country), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
recipe_root_path := "./cofffeemachineConfig/"
|
||||
|
||||
// structure
|
||||
full_file_name_targets := []string{}
|
||||
|
||||
files, err := os.ReadDir(recipe_root_path + countryID)
|
||||
|
||||
if err != nil {
|
||||
Log.Error("Error when read directory", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
Log.Debug("File: ", zap.Any("file", file.Name()))
|
||||
if strings.Contains(file.Name(), file_version) && strings.Contains(file.Name(), ".tmp") {
|
||||
full_file_name_targets = append(full_file_name_targets, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"files": full_file_name_targets})
|
||||
|
||||
Log.Debug("Saved Files: ", zap.Any("files", full_file_name_targets))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,192 +1,204 @@
|
|||
package recipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"recipe-manager/contracts"
|
||||
"recipe-manager/data"
|
||||
"recipe-manager/models"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type RecipeService interface {
|
||||
GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error)
|
||||
GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error)
|
||||
|
||||
GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error)
|
||||
GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error)
|
||||
}
|
||||
|
||||
type recipeService struct {
|
||||
db *data.Data
|
||||
}
|
||||
|
||||
// GetRecipeDetail implements RecipeService.
|
||||
func (rs *recipeService) GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) {
|
||||
|
||||
recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeDetailResponse{}, err
|
||||
}
|
||||
|
||||
result := contracts.RecipeDetailResponse{
|
||||
Name: recipe.Name,
|
||||
OtherName: recipe.OtherName,
|
||||
Description: recipe.Description,
|
||||
OtherDescription: recipe.OtherDescription,
|
||||
LastUpdated: recipe.LastChange,
|
||||
Picture: recipe.UriData[len("img="):], // remove "img=" prefix
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetRecipeDetailMat implements RecipeService.
|
||||
func (rs *recipeService) GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) {
|
||||
countryID, err := rs.db.GetCountryIDByName(request.Country)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeDetailMatListResponse{}, fmt.Errorf("country name: %s not found", request.Country)
|
||||
}
|
||||
|
||||
recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeDetailMatListResponse{}, err
|
||||
}
|
||||
|
||||
matIds := []uint64{}
|
||||
for _, v := range recipe.Recipes {
|
||||
if v.IsUse {
|
||||
matIds = append(matIds, uint64(v.MaterialPathId))
|
||||
}
|
||||
}
|
||||
|
||||
matsCode := rs.db.GetMaterialCode(matIds, countryID, request.Filename)
|
||||
|
||||
result := contracts.RecipeDetailMatListResponse{
|
||||
Result: []contracts.RecipeDetailMat{},
|
||||
}
|
||||
|
||||
for _, v := range recipe.Recipes {
|
||||
for _, mat := range matsCode {
|
||||
if v.MaterialPathId == int(mat.MaterialID) {
|
||||
result.Result = append(result.Result, contracts.RecipeDetailMat{
|
||||
IsUse: v.IsUse,
|
||||
MaterialID: mat.MaterialID,
|
||||
Name: mat.PackageDescription,
|
||||
MixOrder: v.MixOrder,
|
||||
FeedParameter: v.FeedParameter,
|
||||
FeedPattern: v.FeedPattern,
|
||||
MaterialPathId: v.MaterialPathId,
|
||||
PowderGram: v.PowderGram,
|
||||
PowderTime: v.PowderTime,
|
||||
StirTime: v.StirTime,
|
||||
SyrupGram: v.SyrupGram,
|
||||
SyrupTime: v.SyrupTime,
|
||||
WaterCold: v.WaterCold,
|
||||
WaterYield: v.WaterYield,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort by id
|
||||
sort.Slice(result.Result, func(i, j int) bool {
|
||||
return result.Result[i].MaterialID < result.Result[j].MaterialID
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (rs *recipeService) GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error) {
|
||||
countryID, err := rs.db.GetCountryIDByName(request.Country)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeDashboardResponse{}, fmt.Errorf("country name: %s not found", request.Country)
|
||||
}
|
||||
|
||||
recipe := rs.db.GetRecipe(countryID, request.Filename)
|
||||
|
||||
result := contracts.RecipeDashboardResponse{
|
||||
ConfigNumber: recipe.MachineSetting.ConfigNumber,
|
||||
LastUpdated: recipe.Timestamp,
|
||||
Filename: request.Filename,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (rs *recipeService) GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error) {
|
||||
countryID, err := rs.db.GetCountryIDByName(request.Country)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeOverviewResponse{}, fmt.Errorf("country name: %s not found", request.Country)
|
||||
}
|
||||
recipe := rs.db.GetRecipe(countryID, request.Filename)
|
||||
recipeFilter := recipe.Recipe01
|
||||
|
||||
result := contracts.RecipeOverviewResponse{}
|
||||
|
||||
if request.Search != "" {
|
||||
searchResult := []models.Recipe01{}
|
||||
for _, v := range recipeFilter {
|
||||
if strings.Contains(strings.ToLower(v.ProductCode), strings.ToLower(request.Search)) ||
|
||||
strings.Contains(strings.ToLower(v.Name), strings.ToLower(request.Search)) ||
|
||||
strings.Contains(strings.ToLower(v.OtherName), strings.ToLower(request.Search)) {
|
||||
searchResult = append(searchResult, v)
|
||||
}
|
||||
}
|
||||
recipeFilter = searchResult
|
||||
}
|
||||
|
||||
if len(request.MatIds) > 0 {
|
||||
matIdsFiltered := []models.Recipe01{}
|
||||
for _, v := range recipeFilter {
|
||||
for _, matID := range request.MatIds {
|
||||
for _, recipe := range v.Recipes {
|
||||
if recipe.IsUse && recipe.MaterialPathId == matID {
|
||||
matIdsFiltered = append(matIdsFiltered, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recipeFilter = matIdsFiltered
|
||||
}
|
||||
|
||||
// Map to contracts.RecipeOverview
|
||||
for _, v := range recipeFilter {
|
||||
result.Result = append(result.Result, contracts.RecipeOverview{
|
||||
ID: v.ID,
|
||||
ProductCode: v.ProductCode,
|
||||
Name: v.Name,
|
||||
OtherName: v.OtherName,
|
||||
Description: v.Description,
|
||||
LastUpdated: v.LastChange,
|
||||
})
|
||||
}
|
||||
|
||||
result.TotalCount = len(result.Result)
|
||||
|
||||
result.HasMore = result.TotalCount >= request.Take+request.Skip
|
||||
if result.HasMore {
|
||||
result.Result = result.Result[request.Skip : request.Take+request.Skip]
|
||||
sort.Slice(result.Result, func(i, j int) bool {
|
||||
return result.Result[i].ID < result.Result[j].ID
|
||||
})
|
||||
} else if result.TotalCount > request.Skip {
|
||||
result.Result = result.Result[request.Skip:]
|
||||
} else {
|
||||
result.Result = []contracts.RecipeOverview{}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NewRecipeService(db *data.Data) RecipeService {
|
||||
return &recipeService{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"recipe-manager/contracts"
|
||||
"recipe-manager/data"
|
||||
"recipe-manager/models"
|
||||
"recipe-manager/services/logger"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
Log = logger.GetInstance()
|
||||
)
|
||||
|
||||
type RecipeService interface {
|
||||
GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error)
|
||||
GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error)
|
||||
|
||||
GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error)
|
||||
GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error)
|
||||
}
|
||||
|
||||
type recipeService struct {
|
||||
db *data.Data
|
||||
}
|
||||
|
||||
// GetRecipeDetail implements RecipeService.
|
||||
func (rs *recipeService) GetRecipeDetail(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailResponse, error) {
|
||||
|
||||
Log.Debug("GetRecipeDetail", zap.Any("request", request))
|
||||
|
||||
recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeDetailResponse{}, err
|
||||
}
|
||||
|
||||
// DEBUG: picture
|
||||
Log.Debug("GetRecipeDetail", zap.String("picture", recipe.UriData))
|
||||
|
||||
result := contracts.RecipeDetailResponse{
|
||||
Name: recipe.Name,
|
||||
OtherName: recipe.OtherName,
|
||||
Description: recipe.Description,
|
||||
OtherDescription: recipe.OtherDescription,
|
||||
LastUpdated: recipe.LastChange,
|
||||
Picture: recipe.UriData[len("img="):], // remove "img=" prefix
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetRecipeDetailMat implements RecipeService.
|
||||
func (rs *recipeService) GetRecipeDetailMat(request *contracts.RecipeDetailRequest) (contracts.RecipeDetailMatListResponse, error) {
|
||||
countryID, err := rs.db.GetCountryIDByName(request.Country)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeDetailMatListResponse{}, fmt.Errorf("country name: %s not found", request.Country)
|
||||
}
|
||||
|
||||
recipe, err := rs.db.GetRecipe01ByProductCode(request.Filename, request.Country, request.ProductCode)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeDetailMatListResponse{}, err
|
||||
}
|
||||
|
||||
matIds := []uint64{}
|
||||
for _, v := range recipe.Recipes {
|
||||
if v.IsUse {
|
||||
matIds = append(matIds, uint64(v.MaterialPathId))
|
||||
}
|
||||
}
|
||||
|
||||
matsCode := rs.db.GetMaterialCode(matIds, countryID, request.Filename)
|
||||
|
||||
result := contracts.RecipeDetailMatListResponse{
|
||||
Result: []contracts.RecipeDetailMat{},
|
||||
}
|
||||
|
||||
for _, v := range recipe.Recipes {
|
||||
for _, mat := range matsCode {
|
||||
if v.MaterialPathId == int(mat.MaterialID) {
|
||||
result.Result = append(result.Result, contracts.RecipeDetailMat{
|
||||
IsUse: v.IsUse,
|
||||
MaterialID: mat.MaterialID,
|
||||
Name: mat.PackageDescription,
|
||||
MixOrder: v.MixOrder,
|
||||
FeedParameter: v.FeedParameter,
|
||||
FeedPattern: v.FeedPattern,
|
||||
MaterialPathId: v.MaterialPathId,
|
||||
PowderGram: v.PowderGram,
|
||||
PowderTime: v.PowderTime,
|
||||
StirTime: v.StirTime,
|
||||
SyrupGram: v.SyrupGram,
|
||||
SyrupTime: v.SyrupTime,
|
||||
WaterCold: v.WaterCold,
|
||||
WaterYield: v.WaterYield,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sort by id
|
||||
// sort.Slice(result.Result, func(i, j int) bool {
|
||||
// return result.Result[i].MaterialID < result.Result[j].MaterialID
|
||||
// })
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (rs *recipeService) GetRecipeDashboard(request *contracts.RecipeDashboardRequest) (contracts.RecipeDashboardResponse, error) {
|
||||
countryID, err := rs.db.GetCountryIDByName(request.Country)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeDashboardResponse{}, fmt.Errorf("country name: %s not found", request.Country)
|
||||
}
|
||||
|
||||
recipe := rs.db.GetRecipe(countryID, request.Filename)
|
||||
|
||||
result := contracts.RecipeDashboardResponse{
|
||||
ConfigNumber: recipe.MachineSetting.ConfigNumber,
|
||||
LastUpdated: recipe.Timestamp,
|
||||
Filename: request.Filename,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (rs *recipeService) GetRecipeOverview(request *contracts.RecipeOverviewRequest) (contracts.RecipeOverviewResponse, error) {
|
||||
countryID, err := rs.db.GetCountryIDByName(request.Country)
|
||||
|
||||
if err != nil {
|
||||
return contracts.RecipeOverviewResponse{}, fmt.Errorf("country name: %s not found", request.Country)
|
||||
}
|
||||
recipe := rs.db.GetRecipe(countryID, request.Filename)
|
||||
recipeFilter := recipe.Recipe01
|
||||
|
||||
result := contracts.RecipeOverviewResponse{}
|
||||
|
||||
if request.Search != "" {
|
||||
searchResult := []models.Recipe01{}
|
||||
for _, v := range recipeFilter {
|
||||
if strings.Contains(strings.ToLower(v.ProductCode), strings.ToLower(request.Search)) ||
|
||||
strings.Contains(strings.ToLower(v.Name), strings.ToLower(request.Search)) ||
|
||||
strings.Contains(strings.ToLower(v.OtherName), strings.ToLower(request.Search)) {
|
||||
searchResult = append(searchResult, v)
|
||||
}
|
||||
}
|
||||
recipeFilter = searchResult
|
||||
}
|
||||
|
||||
if len(request.MatIds) > 0 {
|
||||
matIdsFiltered := []models.Recipe01{}
|
||||
for _, v := range recipeFilter {
|
||||
for _, matID := range request.MatIds {
|
||||
for _, recipe := range v.Recipes {
|
||||
if recipe.IsUse && recipe.MaterialPathId == matID {
|
||||
matIdsFiltered = append(matIdsFiltered, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recipeFilter = matIdsFiltered
|
||||
}
|
||||
|
||||
// Map to contracts.RecipeOverview
|
||||
for _, v := range recipeFilter {
|
||||
result.Result = append(result.Result, contracts.RecipeOverview{
|
||||
ID: v.ID,
|
||||
ProductCode: v.ProductCode,
|
||||
Name: v.Name,
|
||||
OtherName: v.OtherName,
|
||||
Description: v.Description,
|
||||
LastUpdated: v.LastChange,
|
||||
})
|
||||
}
|
||||
|
||||
result.TotalCount = len(result.Result)
|
||||
|
||||
result.HasMore = result.TotalCount >= request.Take+request.Skip
|
||||
if result.HasMore {
|
||||
result.Result = result.Result[request.Skip : request.Take+request.Skip]
|
||||
sort.Slice(result.Result, func(i, j int) bool {
|
||||
return result.Result[i].ID < result.Result[j].ID
|
||||
})
|
||||
} else if result.TotalCount > request.Skip {
|
||||
result.Result = result.Result[request.Skip:]
|
||||
} else {
|
||||
result.Result = []contracts.RecipeOverview{}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NewRecipeService(db *data.Data) RecipeService {
|
||||
return &recipeService{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue