From 230d4abe0c46c1d269be078e893d40b37c766ccb Mon Sep 17 00:00:00 2001
From: "pakintada@gmail.com"
Date: Mon, 20 Apr 2026 10:37:02 +0700
Subject: [PATCH] feat: save recipe in progress
Signed-off-by: pakintada@gmail.com
---
ISSUES.txt | 20 +++
src/lib/components/app-account-select.svelte | 9 +-
src/lib/components/recipe-details/columns.ts | 4 +-
.../recipe-details/recipe-detail.svelte | 22 +++-
.../recipe-details/recipelist-table.svelte | 21 ++-
.../recipelist-value-editor.svelte | 31 +++--
.../recipe-details/recipelist-value.svelte | 122 ++++++++++++++++--
.../components/recipe-editor-dialog.svelte | 101 ++++++++++++++-
src/lib/core/stores/recipeStore.ts | 1 +
src/lib/core/stores/websocketStore.ts | 12 +-
src/lib/core/types/outMessage.ts | 8 ++
11 files changed, 310 insertions(+), 41 deletions(-)
create mode 100644 ISSUES.txt
diff --git a/ISSUES.txt b/ISSUES.txt
new file mode 100644
index 0000000..8fce82a
--- /dev/null
+++ b/ISSUES.txt
@@ -0,0 +1,20 @@
+Idea, Issue, Work Tracking
+
+[TODO]
+
+
+[Pending]
+
+- [] #2: Send change value from editing in recipe to machine
+- [] #3: Save value to recipe
+- [] #5: revert value on close dialog recipe
+
+
+[Rejected]
+- [] #4: From #1, will do sync value from server, so that user could save their current edit too
+
+
+[Done]
+- [x] #1: Topping value saving bug, fix by snapshot value
+
+
diff --git a/src/lib/components/app-account-select.svelte b/src/lib/components/app-account-select.svelte
index 2d4a4a3..319a787 100644
--- a/src/lib/components/app-account-select.svelte
+++ b/src/lib/components/app-account-select.svelte
@@ -47,9 +47,12 @@
authStore.set(null);
let socket = get(socketStore);
- if (socket) {
- socket.close(1000, 'logout');
- }
+
+ try {
+ if (socket) {
+ socket.close(1000, 'logout');
+ }
+ } catch (e) {}
socketStore.set(null);
if (browser && 'cookieStore' in window) await cookieStore.delete('logged_in');
diff --git a/src/lib/components/recipe-details/columns.ts b/src/lib/components/recipe-details/columns.ts
index c679d24..db81e79 100644
--- a/src/lib/components/recipe-details/columns.ts
+++ b/src/lib/components/recipe-details/columns.ts
@@ -112,9 +112,11 @@ export const columns: ColumnDef[] = [
row_uid: row.original.id,
mat_id: row.original.material_id,
onEditValue: (changes: any) => {
+ // console.log('triggered on edit value', changes);
+
recipeDataEvent.set({
event_type: 'edit_change_value_rpl',
- payload: changes,
+ payload: JSON.parse(changes),
index: row.original.id
});
diff --git a/src/lib/components/recipe-details/recipe-detail.svelte b/src/lib/components/recipe-details/recipe-detail.svelte
index 3d80f8f..445d547 100644
--- a/src/lib/components/recipe-details/recipe-detail.svelte
+++ b/src/lib/components/recipe-details/recipe-detail.svelte
@@ -11,6 +11,7 @@
import { columns, type RecipelistMaterial } from './columns';
import { get, readable, writable } from 'svelte/store';
import {
+ currentEditingRecipeProductCode,
latestRecipeToppingData,
materialFromMachineQuery,
materialFromServerQuery
@@ -29,8 +30,9 @@
let {
recipeData,
onPendingChange,
+ productCode,
refPage
- }: { recipeData: any; onPendingChange: any; refPage: string } = $props();
+ }: { recipeData: any; onPendingChange: any; productCode: string; refPage: string } = $props();
let menuName: string = $state('');
@@ -126,7 +128,9 @@
}
}
- async function saveRecipe() {}
+ async function saveRecipe() {
+ recipeDetailDispatch('saveRecipe');
+ }
async function sendTriggerBrewNow() {
// check queue ready
@@ -155,8 +159,8 @@
}
}
- async function checkChanges(original: any) {
- console.log('old', original, 'updated', recipeListMatState);
+ async function checkChanges(productCode: string, original: any) {
+ // console.log('old', original, 'updated', recipeListMatState);
if (recipeListOriginal.length == 0) {
recipeListOriginal = original;
}
@@ -164,6 +168,7 @@
if (original !== recipeListMatState) {
await onPendingChange({
target: 'recipeList',
+ ref_pd: productCode,
value: original
});
}
@@ -187,6 +192,8 @@
latestRecipeToppingData.set(toppingSlotState);
+ currentEditingRecipeProductCode.set(productCode);
+
// save old value\
}
});
@@ -237,7 +244,12 @@
-
+
diff --git a/src/lib/components/recipe-details/recipelist-table.svelte b/src/lib/components/recipe-details/recipelist-table.svelte
index ad6445a..caf08f3 100644
--- a/src/lib/components/recipe-details/recipelist-table.svelte
+++ b/src/lib/components/recipe-details/recipelist-table.svelte
@@ -42,11 +42,13 @@
let {
data,
columns,
- onStateChange
+ onStateChange,
+ productCode
}: {
data: RecipelistMaterial[];
columns: ColumnDef[];
onStateChange: any;
+ productCode: string;
} = $props();
let sorting = $state([]);
@@ -75,7 +77,10 @@
getFacetedRowModel: getFacetedRowModel(),
onStateChange: async (updater) => {
console.log('table state change', data);
- await onStateChange(table.getRowModel().rows.map((x) => x.original));
+ await onStateChange(
+ productCode,
+ table.getRowModel().rows.map((x) => x.original)
+ );
},
onSortingChange: async (updater) => {
console.log('triggering sorting');
@@ -84,7 +89,10 @@
} else {
sorting = updater;
}
- await onStateChange(table.getRowModel().rows.map((x) => x.original));
+ await onStateChange(
+ productCode,
+ table.getRowModel().rows.map((x) => x.original)
+ );
},
onRowSelectionChange: async (updater) => {
// table.getRowModel().rows.find((x) => x.original.id == )
@@ -92,11 +100,14 @@
rowSelection = updater(rowSelection);
let rows = table.getRowModel().rows;
- console.log('state size', data, rows);
+ // console.log('state size', data, rows);
} else {
rowSelection = updater;
}
- await onStateChange(table.getRowModel().rows.map((x) => x.original));
+ await onStateChange(
+ productCode,
+ table.getRowModel().rows.map((x) => x.original)
+ );
}
});
diff --git a/src/lib/components/recipe-details/recipelist-value-editor.svelte b/src/lib/components/recipe-details/recipelist-value-editor.svelte
index 994144e..14f728d 100644
--- a/src/lib/components/recipe-details/recipelist-value-editor.svelte
+++ b/src/lib/components/recipe-details/recipelist-value-editor.svelte
@@ -176,7 +176,7 @@
function saveEditingValue() {
console.log('saving value ...', value_event_state);
- if (value_event_state === ValueEvent.EDITED) {
+ if (value_event_state === ValueEvent.EDITED || value_event_state === ValueEvent.SAVED) {
let payload = {
source: current_editing_data,
change: changed_data
@@ -209,28 +209,36 @@
function handleToppingGroupChange(v: any) {
console.log('change topping group');
+
+ // TODO: clear topping list, otherwise, it will continue to append filter to list by each navigate back and forth
+
selected_category_id = v.groupID;
// get default
selected_topping_list_id = v.idDefault ?? 0;
// set to edit state even if selected same group again
value_event_state = ValueEvent.EDITED;
- if (current_editing_data['toppings'] == undefined || current_editing_data['toppings'] == null) {
+ if (
+ current_editing_data['toppings'] !== undefined ||
+ current_editing_data['toppings'] !== null
+ ) {
let toppings_length = current_editing_data['toppings'].length;
if (changed_data['toppings'] == undefined || changed_data['toppings'] == null) {
changed_data['toppings'] = new Array(toppings_length);
- // console.log('filling change topping', JSON.stringify(changed_data));
+ console.log('filling change topping', JSON.stringify(changed_data));
}
}
+ console.log('current editing data', current_editing_data['toppings']);
+
let idx = getToppingSlotIndex(current_editing_data['mat_id']);
// get old state topping idx
let current_selection = current_editing_data['toppings'][idx];
// let default_from_recipe = current_editing_data['toppings'][idx]['ListGroupID'][0];
- // console.log(`Current TG: `, JSON.stringify(current_selection));
+ console.log(`Current TG: `, JSON.stringify(current_selection));
if (current_selection['groupID'] !== undefined || current_selection['groupID'] !== null) {
try {
changed_data['toppings'][idx] = current_selection;
@@ -250,7 +258,10 @@
// set to edit state even if selected same group again
value_event_state = ValueEvent.EDITED;
let idx = getToppingSlotIndex(current_editing_data['mat_id']);
- if (current_editing_data['toppings'] == undefined || current_editing_data['toppings'] == null) {
+ if (
+ current_editing_data['toppings'] !== undefined ||
+ current_editing_data['toppings'] !== null
+ ) {
let toppings_length = current_editing_data['toppings'].length;
// case: topping not init
if (changed_data['toppings'] == null || changed_data['toppings'] == undefined) {
@@ -590,12 +601,12 @@
-
+ > -->
+
diff --git a/src/lib/components/recipe-details/recipelist-value.svelte b/src/lib/components/recipe-details/recipelist-value.svelte
index ba31fa2..0c7a0c6 100644
--- a/src/lib/components/recipe-details/recipelist-value.svelte
+++ b/src/lib/components/recipe-details/recipelist-value.svelte
@@ -10,7 +10,7 @@
import Input from '../ui/input/input.svelte';
import Separator from '$lib/components/ui/separator/separator.svelte';
import Button from '../ui/button/button.svelte';
- import { PencilIcon } from '@lucide/svelte/icons';
+ import { PencilIcon, Undo } from '@lucide/svelte/icons';
import * as Tooltip from '../ui/tooltip/index';
import {
latestRecipeToppingData,
@@ -22,6 +22,9 @@
referenceFromPage
} from '$lib/core/stores/recipeStore';
import { get } from 'svelte/store';
+ import { sendMessage } from '$lib/core/handlers/ws_messageSender';
+ import { auth } from '$lib/core/stores/auth';
+ import { departmentStore } from '$lib/core/stores/departments';
let {
row_uid,
@@ -51,11 +54,16 @@
// toppings
let currentToppings: any = $state([]);
+ let oldToppings: any = $state(null);
// current topping of this row
let currentToppingInRow: any = $state();
let selectableToppingInGroup: any[] = $state([]);
+ let currentToppingNamesOnly = $state('');
+ let oldToppingNamesOnly = $state(null);
+
let unsubRecipeDataEvent: any;
+ let unsubRecipeTopping: any;
function isToppingId(mat_id: string): boolean {
let mat_num = extractMaterialIdFromDisplay(mat_id);
@@ -118,21 +126,54 @@
refFrom === 'overview' ? toppingListFromServerQuery : toppingListFromMachineQuery
);
- // console.log(JSON.stringify(groupQuery[0]));
- // console.log(JSON.stringify(listQuery[0]));
+ console.log('old topping', oldToppings[getToppingSlot()]);
+
+ console.log('current topping', current_row_topping);
+ oldToppingNamesOnly = null; // reset first
+ if (oldToppings[getToppingSlot()] != current_row_topping) {
+ console.log('detect change on topping row', row_uid);
+
+ let groupIDchange =
+ oldToppings[getToppingSlot()]['groupID'] !== current_row_topping['groupID'];
+ let defaultIDchange =
+ oldToppings[getToppingSlot()]['defaultIDSelect'] !== current_row_topping['defaultIDSelect'];
+
+ console.log('group change', groupIDchange, 'default id change', defaultIDchange);
+
+ if (!groupIDchange && !defaultIDchange) {
+ oldToppingNamesOnly = null;
+ } else {
+ // has change, display old name
+ let oldGroupData = groupQuery.find(
+ (x: any) => x.groupID.toString() === oldToppings[getToppingSlot()]['groupID']
+ );
+ let oldListData = listQuery.filter(
+ (x: any) =>
+ oldGroupData != undefined &&
+ Object.keys(oldGroupData).includes('idInGroup') &&
+ oldGroupData['idInGroup'].split(',').includes(x.id.toString())
+ );
+ oldToppingNamesOnly = oldGroupData['otherName'];
+ }
+ } else {
+ oldToppingNamesOnly = null;
+ }
let groupData = groupQuery.find(
(x: any) => x.groupID.toString() === current_row_topping['groupID']
);
let listData = listQuery.filter(
(x: any) =>
- groupData &&
+ groupData != undefined &&
Object.keys(groupData).includes('idInGroup') &&
groupData['idInGroup'].split(',').includes(x.id.toString())
);
console.log('topping data', JSON.stringify(groupData), 'list', listData.length);
+ currentToppingNamesOnly = groupData ? (groupData['otherName'] ?? '-') : '-';
+
+ // NOTE: send topping data to value editor
currentToppingInRow =
current_row_topping['groupID'] === 0 || current_row_topping['groupID'] === '0'
? {
@@ -178,13 +219,20 @@
}
function initialize() {
- currentToppings = get(latestRecipeToppingData);
-
hasMixOrder = mix_order == 1;
extractStringParam();
if (isToppingId(mat_id) && onDetectToppingSlot) {
+ // FIXME: this should not be updated yet, must be updated after user pressed save only
+ let latest_topping_data_snapshot = $state.snapshot(get(latestRecipeToppingData));
+ currentToppings = latest_topping_data_snapshot;
+ // console.log('current topping in initialize', currentToppings);
+ if (oldToppings == null) {
+ // console.log('saving original topping', oldToppings);
+ oldToppings = $state.snapshot(currentToppings);
+ }
+
currentMaterialType = 'topping';
getToppingDisplay();
@@ -200,9 +248,9 @@
// console.log('type get', mat_type_t1, mat_type_t2);
currentMaterialType = mat_type_t1 === mat_type_t2 ? mat_type_t1 : mat_type_t2;
- if (hasMixOrder) {
- console.log('detect mix order', mat_num);
- }
+ // if (hasMixOrder) {
+ // console.log('detect mix order', mat_num);
+ // }
// if (feed.parameter > 0 || feed.pattern > 0) {
// console.log('has feed fields', JSON.stringify(feed));
@@ -210,6 +258,19 @@
}
}
+ function applyChanges(value: any) {
+ let keys = Object.keys(value);
+
+ for (const key of keys) {
+ if (key == 'toppings' && currentMaterialType == 'topping') {
+ let topping_change = value[key][getToppingSlot()];
+ console.log('topping applying', topping_change);
+ currentToppings[getToppingSlot()] = topping_change;
+ } else {
+ }
+ }
+ }
+
function handleEvents(event: { event_type: string; payload: any; index: number | undefined }) {
// console.log('triggered event', event.event_type, JSON.stringify(event.payload));
if (event.event_type === 'mat_change') {
@@ -217,6 +278,10 @@
initialize();
} else if (event.event_type === 'edit_mat_field') {
console.log('request edit mat');
+
+ // fix bug: topping instant change by unlink topping
+ let _current_toppings = $state.snapshot(currentToppings);
+
// pack all shown data
recipeDataEvent.set({
event_type: 'edit_mat_field_prep',
@@ -225,7 +290,7 @@
mat_type: currentMaterialType,
mat_name: mat_id,
params: currentStringParams,
- toppings: currentToppings,
+ toppings: _current_toppings,
current_topping_group: currentToppingInRow,
current_topping_list: selectableToppingInGroup,
has_mix_ord: hasMixOrder,
@@ -244,6 +309,23 @@
// apply now
let keys = Object.keys(change_values);
console.log('change keys', JSON.stringify(keys));
+ let _current_toppings = $state.snapshot(currentToppings);
+
+ // initialize();
+ applyChanges(change_values);
+
+ event.payload = {
+ current_toppings: _current_toppings,
+ ...event.payload
+ };
+
+ // console.log('check on change before trigger', change_values);
+
+ if (onEditValue) onEditValue(JSON.stringify(event.payload));
+ } else if (event.event_type === 'revert_change') {
+ // console.log('revert back ...', row_uid);
+
+ currentToppings = oldToppings;
}
}
@@ -255,11 +337,23 @@
handleEvents(event);
}
});
+
+ unsubRecipeTopping = latestRecipeToppingData.subscribe((payload) => {
+ // console.log('topping data subscribe', payload);
+ });
});
onDestroy(() => {
if (unsubRecipeDataEvent) {
+ // do last event before destroy
+ // console.log('trigger last event before destroy', get(latestRecipeToppingData));
+ handleEvents(get(recipeDataEvent) ?? { event_type: '', payload: {}, index: -1 });
+
unsubRecipeDataEvent();
+ // console.log('destroy recipe data event listener');
+ }
+ if (unsubRecipeTopping) {
+ unsubRecipeTopping();
}
});
@@ -275,6 +369,14 @@
{getCurrentSelectedToppingGroup()}
, {getCurrentSelectedToppingList()}
+ {#if oldToppingNamesOnly != null}
+
+
+ {/if}
{:else}
diff --git a/src/lib/components/recipe-editor-dialog.svelte b/src/lib/components/recipe-editor-dialog.svelte
index 06b6457..f5b3986 100644
--- a/src/lib/components/recipe-editor-dialog.svelte
+++ b/src/lib/components/recipe-editor-dialog.svelte
@@ -1,5 +1,11 @@
{#if isDesktop.current}
-
+ onCloseDialog()}>
e.preventDefault()}
>View
@@ -126,8 +207,16 @@
sendBrewNow()}
+ on:saveRecipe={async () => {
+ save_change = true;
+
+ console.log('save change, check state', callback_revert_value_if_not_save);
+
+ addNotification('INFO:Save recipe');
+ }}
/>
diff --git a/src/lib/core/stores/recipeStore.ts b/src/lib/core/stores/recipeStore.ts
index f35ec51..e7d9340 100644
--- a/src/lib/core/stores/recipeStore.ts
+++ b/src/lib/core/stores/recipeStore.ts
@@ -44,6 +44,7 @@ export const recipeFromMachineQuery = writable({});
export const materialFromMachineQuery = writable({});
export const referenceFromPage = writable('');
+export const currentEditingRecipeProductCode = writable('');
let worker: Worker | null = null;
let initialized = false;
diff --git a/src/lib/core/stores/websocketStore.ts b/src/lib/core/stores/websocketStore.ts
index 3cc983b..49c3efb 100644
--- a/src/lib/core/stores/websocketStore.ts
+++ b/src/lib/core/stores/websocketStore.ts
@@ -2,7 +2,7 @@ import { browser } from '$app/environment';
import { env } from '$env/dynamic/public';
import { get, writable } from 'svelte/store';
import { handleIncomingMessages } from '../handlers/messageHandler';
-import { queue as msgQueue } from '../handlers/ws_messageSender';
+import { queue as msgQueue, sendMessage } from '../handlers/ws_messageSender';
import { auth } from '../client/firebase';
import { addNotification } from './noti';
@@ -31,6 +31,16 @@ export function connectToWebsocket() {
msgQueue.set(queue);
}
}
+
+ // heartbeat 10s
+ setInterval(() => {
+ if (socket) {
+ sendMessage({
+ type: 'heartbeat',
+ payload: {}
+ });
+ }
+ }, 10000);
});
socket.addEventListener('message', (event) => {
diff --git a/src/lib/core/types/outMessage.ts b/src/lib/core/types/outMessage.ts
index 138e41a..e40c0e0 100644
--- a/src/lib/core/types/outMessage.ts
+++ b/src/lib/core/types/outMessage.ts
@@ -32,6 +32,14 @@ export type OutMessage =
values: any;
};
}
+ | {
+ type: 'save_recipe';
+ payload: {
+ user: string;
+ country: string;
+ values: any;
+ };
+ }
| {
type: 'heartbeat';
payload: {};