From 07977ce8967f75ccfe7896e6857767386416abf6 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Tue, 26 May 2026 17:08:44 +0700 Subject: [PATCH 01/10] change: wip testing price from sheet Signed-off-by: pakintada@gmail.com --- .../recipe-details/recipe-detail.svelte | 33 +++--- .../components/recipe-editor-dialog.svelte | 33 +++++- src/lib/core/handlers/adbPayloadHandler.ts | 14 +++ src/lib/core/handlers/messageHandler.ts | 100 +++++++++++++++++- src/lib/core/handlers/sheetNotiHandler.ts | 68 ++++++++++++ src/lib/core/stores/recipeStore.ts | 11 ++ src/lib/core/types/outMessage.ts | 3 +- src/lib/helpers/formatDate.ts | 18 ++++ src/routes/(authed)/tools/brew/+page.svelte | 6 +- 9 files changed, 268 insertions(+), 18 deletions(-) create mode 100644 src/lib/core/handlers/sheetNotiHandler.ts create mode 100644 src/lib/helpers/formatDate.ts diff --git a/src/lib/components/recipe-details/recipe-detail.svelte b/src/lib/components/recipe-details/recipe-detail.svelte index b1100ea..6e68b03 100644 --- a/src/lib/components/recipe-details/recipe-detail.svelte +++ b/src/lib/components/recipe-details/recipe-detail.svelte @@ -5,6 +5,7 @@ import Input from '$lib/components/ui/input/input.svelte'; import Button from '$lib/components/ui/button/button.svelte'; import Spinner from '$lib/components/ui/spinner/spinner.svelte'; + import Checkbox from '$lib/components/ui/checkbox/checkbox.svelte'; import { Badge } from '$lib/components/ui/badge/index'; import * as Item from '$lib/components/ui/item/index'; import { createEventDispatcher, onDestroy, onMount } from 'svelte'; @@ -265,22 +266,28 @@ -
- - - - +
+
+ + +
+ +
+ + +
-
+
- - + +
+ + +
+ + Disabled +
diff --git a/src/lib/components/recipe-editor-dialog.svelte b/src/lib/components/recipe-editor-dialog.svelte index c80a281..995bb37 100644 --- a/src/lib/components/recipe-editor-dialog.svelte +++ b/src/lib/components/recipe-editor-dialog.svelte @@ -30,6 +30,7 @@ machineInfoStore, updateMachineStatus } from '$lib/core/stores/machineInfoStore'; + import { formatCustomDate } from '$lib/helpers/formatDate'; const isDesktop = new MediaQuery('(min-width: 768px)'); @@ -125,7 +126,7 @@ ready_to_send_brew.push(new_change); - callback_revert_value_if_not_save = (save: any) => { + callback_revert_value_if_not_save = async (save: any) => { if (!save) { latestRecipeToppingData.set(topping_value_for_revert); console.log('revert change', get(latestRecipeToppingData)); @@ -141,6 +142,16 @@ // currentData['ToppingSet'] = latestRecipeToppingData; // console.log('current data', currentData); + let curr_user = get(auth); + + let user_info: any; + if (curr_user) { + user_info = { + displayName: curr_user.displayName, + email: curr_user.email, + uid: curr_user.uid + }; + } if (get(referenceFromPage) == 'brew') { // send change to machine @@ -164,11 +175,29 @@ data: ready_to_send_brew[0] } }); + + let country = await adb.pull('/sdcard/coffeevending/country/short'); + let box_id = await adb.pull('/sdcard/coffeevending/.bid'); + + // update last change + // format 16-Feb-2026 10:31:18 + let date = new Date(); + let formatted = formatCustomDate(date); + ready_to_send_brew[0].LastChange = formatted; + + sendMessage({ + type: 'save_recipe', + payload: { + user_info, + country: `${country?.toLowerCase() ?? 'unknown'}_${box_id ?? ''}`, + values: ready_to_send_brew[0] + } + }); } else if (get(referenceFromPage) == 'overview') { sendMessage({ type: 'save_recipe', payload: { - user: get(auth)?.displayName ?? 'unknown', + user_info, country: get(departmentStore) ?? 'unknown', values: currentData } diff --git a/src/lib/core/handlers/adbPayloadHandler.ts b/src/lib/core/handlers/adbPayloadHandler.ts index 63cb669..2c84e6d 100644 --- a/src/lib/core/handlers/adbPayloadHandler.ts +++ b/src/lib/core/handlers/adbPayloadHandler.ts @@ -1,6 +1,8 @@ +import { get } from 'svelte/store'; import { updateMachineStatus } from '../stores/machineInfoStore'; import { addNotification } from '../stores/noti'; import { handleIncomingMessages } from './messageHandler'; +import { recipeFromMachineQuery } from '../stores/recipeStore'; type AdbPayload = { type: string; payload: any }; @@ -73,9 +75,21 @@ async function handleAdbPayload(raw_payload: string) { let plist = payload.payload.split('/'); let pd = plist[0] ?? ''; let total_time = plist[1] ?? ''; + let mode_ref = plist[2] ?? ''; // update recipe data store console.log('brewing finish', pd, 'total time', total_time); + + // update recipe from brew now + let recipeDevSnapshot = get(recipeFromMachineQuery) ?? {}; + let recipe01Snap = recipeDevSnapshot['recipe']; + if (recipe01Snap) { + recipe01Snap[pd].total_time = + mode_ref != 'sim' ? total_time : recipe01Snap[pd].total_time; + + recipeDevSnapshot['recipe'] = recipe01Snap; + recipeFromMachineQuery.set(recipeDevSnapshot); + } } break; diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts index c7e2088..c7b3d50 100644 --- a/src/lib/core/handlers/messageHandler.ts +++ b/src/lib/core/handlers/messageHandler.ts @@ -2,6 +2,7 @@ import { get, writable } from 'svelte/store'; import { addNotification, notiStore } from '../stores/noti'; import { currentRecipeVersionsSelector, + lastRequestSheetPrice, materialFromServerQuery, priceRecipeData, recipeData, @@ -9,6 +10,8 @@ import { recipeLoading, recipeOverviewData, recipeStreamMeta, + streamingRawData, + streamingRawMeta, toppingGroupFromServerQuery, toppingListFromServerQuery } from '../stores/recipeStore'; @@ -18,13 +21,16 @@ import { type RecipeVersion } from '$lib/models/recipe_version.model'; import { goto } from '$app/navigation'; import { socketAlreadySendHeartbeat, socketConnectionOfflineCount } from '../stores/websocketStore'; import type { RecipePrice } from '$lib/models/price.model'; -import { sendMessage } from './ws_messageSender'; +import { sendCommandRequest, sendMessage } from './ws_messageSender'; import { auth as authStore } from '../stores/auth'; +import { v4 as uuidv4 } from 'uuid'; +import { handleSheetResponseFromNoti } from './sheetNotiHandler'; export const messages = writable([]); type WSMessage = { type: string; payload: any }; +// MAXIMUM LIMIT = 1814355 const handlers: Record void> = { chat: (p) => messages.update((m) => [...m, p]), ping: (p) => console.log('ping from server'), @@ -206,6 +212,9 @@ const handlers: Record void> = { let msg = p.msg ?? `Notify from ${p.from}`; let target = p.to; + let from_service = p.from ?? ''; + let ref_service = p.ref ?? ''; + if (target) { // let currentUsername = auth.currentUser?.displayName; @@ -249,11 +258,91 @@ const handlers: Record void> = { console.log('get price length: ', content.length); let current_price = get(priceRecipeData); + let lastRequestPriceInstance = get(lastRequestSheetPrice); + let saved_product_code_to_get_from_sheet = []; + let current_meta = get(recipeStreamMeta); + lastRequestPriceInstance[current_meta?.country ?? 'unknown'] = {}; for (const c of content) { current_price[c.ProductCode] = c.NewPrice + (c.StringParam ? `,${c.StringParam}` : ''); + lastRequestPriceInstance[current_meta?.country ?? 'unknown'][c.ProductCode] = ''; + saved_product_code_to_get_from_sheet.push({ + product_code: c.ProductCode + }); } priceRecipeData.set(current_price); + + console.log('check length', saved_product_code_to_get_from_sheet.length); + // set command request to stream mode so + let request_id = uuidv4(); + + lastRequestPriceInstance[request_id] = current_meta?.country ?? ''; + let current_streaming_instance = get(streamingRawData); + current_streaming_instance[request_id] = ''; + streamingRawData.set(current_streaming_instance); + + sendCommandRequest('sheet', { + country: current_meta?.country ?? '', + content: saved_product_code_to_get_from_sheet, + param: 'price', + stream: true, + request_id + }); + + lastRequestSheetPrice.set(lastRequestPriceInstance); + }, + raw_stream: (p) => { + let streamRawInstance = get(streamingRawData); + let sub_type = p.sub_type; + let request_id = p.request_id; + let size_per_chunk = p.size_per_chunk; + let total_chunks = p.total_chunks; + let idx = p.idx; + + switch (sub_type) { + case 'price': + streamingRawMeta.set({ + id: request_id, + total_size: total_chunks, + chunk_size: size_per_chunk, + progress: 0 + }); + break; + case 'chunk_price': + streamingRawMeta.set({ + id: request_id, + total_size: total_chunks, + chunk_size: size_per_chunk, + progress: idx + }); + + let raw_payload = p.raw ?? ''; + streamRawInstance[request_id] += raw_payload; + streamingRawData.set(streamRawInstance); + + break; + case 'end_price': + let lastRequestPriceInstance = get(lastRequestSheetPrice); + let country = lastRequestPriceInstance[request_id]; + + try { + let raw_payload = JSON.parse(streamRawInstance[request_id]); + let ref_from_raw = raw_payload.ref ?? ''; + let from_service_raw = raw_payload.from ?? ''; + let parsed_payload = raw_payload.payload ?? ''; + + if (from_service_raw == 'sheet-service') { + handleSheetResponseFromNoti(parsed_payload, ref_from_raw, country); + delete streamRawInstance[request_id]; + streamingRawData.set(streamRawInstance); + } + } catch (e) { + console.log(`end price process error: ${e}`); + } + + break; + default: + } }, heartbeat: (p) => { socketConnectionOfflineCount.set(0); @@ -270,5 +359,14 @@ export function handleIncomingMessages(raw: string) { addNotification('ERR:No response from server'); return; } + + // raw streaming type + if (msg.type.startsWith('raw_stream')) { + // convert + let sub_type = msg.type.replace('raw_stream_', ''); + msg.payload.sub_type = sub_type; + msg.type = 'raw_stream'; + } + handlers[msg.type]?.(msg.payload); } diff --git a/src/lib/core/handlers/sheetNotiHandler.ts b/src/lib/core/handlers/sheetNotiHandler.ts new file mode 100644 index 0000000..a51b1ba --- /dev/null +++ b/src/lib/core/handlers/sheetNotiHandler.ts @@ -0,0 +1,68 @@ +import { get } from 'svelte/store'; +import { lastRequestSheetPrice } from '../stores/recipeStore'; + +export interface PayloadFromSheet { + header: string[]; + key: string; + payload: GristCell[]; +} + +export interface GristCell { + cells: { + coord: { + col: number; + row: number; + }; + value: string; + }[]; + row_index: number; +} + +const PRICE_SHEET_DEFINITION_BY_COUNTRY: any = { + ltu: { + expect_header: ['Name', 'Price in Euro'], + get_header_idx: (x: string[]) => { + let result = []; + for (const header of PRICE_SHEET_DEFINITION_BY_COUNTRY['ltu'].expect_header) { + let found = x.findIndex((y) => y == header); + result.push(found); + } + + return result; + } + } +}; + +export function handleSheetResponseFromNoti(raw_payload: any, ref: string, country?: string) { + switch (ref) { + case 'price': + let price_contents: PayloadFromSheet[] = raw_payload.content; + let header_idx = PRICE_SHEET_DEFINITION_BY_COUNTRY[country ?? 'unknown'].get_header_idx( + price_contents[0].header + ); + + let lastRequestSheetInstance = get(lastRequestSheetPrice); + let products = lastRequestSheetInstance[country ?? 'unknown']; + + for (let c of price_contents) { + let curr_product_code = c.key; + // price idx should be last + let price_idx = header_idx[header_idx.length - 1]; + let price_rows = c.payload; + // get last because last row will always override + let expected_row = price_rows[price_rows.length - 1]; + let price_col = expected_row.cells[price_idx]; + products[curr_product_code] = price_col; + console.log(`[handleSheetPrice][country] ${curr_product_code} --> ${price_col}`); + } + + lastRequestSheetInstance[country ?? 'unknown'] = products; + lastRequestSheetPrice.set({ + ...lastRequestSheetInstance, + products + }); + + break; + default: + } +} diff --git a/src/lib/core/stores/recipeStore.ts b/src/lib/core/stores/recipeStore.ts index 0a97cc2..ac21a3a 100644 --- a/src/lib/core/stores/recipeStore.ts +++ b/src/lib/core/stores/recipeStore.ts @@ -23,6 +23,17 @@ export const recipeOverviewData = writable(null); export const materialData = writable(); // price from recipe repo export const priceRecipeData = writable<{ [key: string]: any }>({}); +export const lastRequestSheetPrice = writable<{ [key: string]: any }>({}); + +// Streaming raw +export const streamingRawData = writable<{ [key: string]: any }>({}); +export const streamingRawMeta = writable<{ + id: string; + total_size: number; + chunk_size: number; + progress: number; + country?: string; +} | null>(null); // machine recipe export const recipeFromMachine = writable(null); diff --git a/src/lib/core/types/outMessage.ts b/src/lib/core/types/outMessage.ts index afe8c23..bd14e6e 100644 --- a/src/lib/core/types/outMessage.ts +++ b/src/lib/core/types/outMessage.ts @@ -36,9 +36,10 @@ export type OutMessage = | { type: 'save_recipe'; payload: { - user: string; + user_info: any; country: string; values: any; + plugins?: string; }; } | { diff --git a/src/lib/helpers/formatDate.ts b/src/lib/helpers/formatDate.ts new file mode 100644 index 0000000..9015264 --- /dev/null +++ b/src/lib/helpers/formatDate.ts @@ -0,0 +1,18 @@ +export function formatCustomDate(date: Date): string { + const formatter = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: 'short', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false + }); + + // Extract all the formatted parts into an object + const parts = formatter.formatToParts(date); + const partMap = Object.fromEntries(parts.map((p) => [p.type, p.value])); + + // Construct your exact string: 16-Feb-2026 10:31:18 + return `${partMap.day}-${partMap.month}-${partMap.year} ${partMap.hour}:${partMap.minute}:${partMap.second}`; +} diff --git a/src/routes/(authed)/tools/brew/+page.svelte b/src/routes/(authed)/tools/brew/+page.svelte index 2dfaf9e..1bbba35 100644 --- a/src/routes/(authed)/tools/brew/+page.svelte +++ b/src/routes/(authed)/tools/brew/+page.svelte @@ -53,7 +53,11 @@ if (instance) { console.log('instance passed!'); let dev_recipe = await adb.pull(`${sourceDir}/cfg/recipe_branch_dev.json`); - console.log('dev recipe ok', dev_recipe != undefined, dev_recipe); + console.log('dev recipe ok', dev_recipe != undefined); + if (dev_recipe == undefined || dev_recipe == null || dev_recipe?.length == 0) { + dev_recipe = await adb.pull(`${sourceDir}/coffeethai02.json`); + console.log('dev recipe ok by production', dev_recipe != undefined); + } if (dev_recipe) { if (dev_recipe.length == 0) { // case error, do last retry From 79a76f5c3eba2c8140991e05ca8c2fe68e30fec9 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 27 May 2026 07:58:33 +0700 Subject: [PATCH 02/10] change: disable machine status update if not brew - fix: payload of price get unknown service ref Signed-off-by: pakintada@gmail.com --- .../components/recipe-editor-dialog.svelte | 50 ++++++++++--------- src/lib/core/handlers/messageHandler.ts | 4 +- src/lib/core/handlers/sheetNotiHandler.ts | 2 + 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/lib/components/recipe-editor-dialog.svelte b/src/lib/components/recipe-editor-dialog.svelte index 995bb37..1a4ab83 100644 --- a/src/lib/components/recipe-editor-dialog.svelte +++ b/src/lib/components/recipe-editor-dialog.svelte @@ -336,31 +336,33 @@ $effect(() => { // interval check 1s // machine - interval_get_machine_status = setInterval(() => { - if ( - getMachineStatus() == undefined || - getMachineStatus() == null || - $machineInfoStore?.status === undefined - ) { - // set default now - updateMachineStatus(''); - } - - console.log( - 'machine status pinging recipe editor dialog', - getMachineStatus(), - $machineInfoStore?.status - ); - - // update machine status - // check-connection - sendToAndroid({ - type: 'check-connection', - payload: { - start: new Date().toLocaleTimeString() + if (refPage === 'brew') { + interval_get_machine_status = setInterval(() => { + if ( + getMachineStatus() == undefined || + getMachineStatus() == null || + $machineInfoStore?.status === undefined + ) { + // set default now + updateMachineStatus(''); } - }); - }, 1000); + + console.log( + 'machine status pinging recipe editor dialog', + getMachineStatus(), + $machineInfoStore?.status + ); + + // update machine status + // check-connection + sendToAndroid({ + type: 'check-connection', + payload: { + start: new Date().toLocaleTimeString() + } + }); + }, 1000); + } }); onDestroy(() => { diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts index c7b3d50..80b8c6b 100644 --- a/src/lib/core/handlers/messageHandler.ts +++ b/src/lib/core/handlers/messageHandler.ts @@ -327,8 +327,8 @@ const handlers: Record void> = { try { let raw_payload = JSON.parse(streamRawInstance[request_id]); - let ref_from_raw = raw_payload.ref ?? ''; - let from_service_raw = raw_payload.from ?? ''; + let ref_from_raw = raw_payload.payload.ref ?? ''; + let from_service_raw = raw_payload.payload.from ?? ''; let parsed_payload = raw_payload.payload ?? ''; if (from_service_raw == 'sheet-service') { diff --git a/src/lib/core/handlers/sheetNotiHandler.ts b/src/lib/core/handlers/sheetNotiHandler.ts index a51b1ba..81367ba 100644 --- a/src/lib/core/handlers/sheetNotiHandler.ts +++ b/src/lib/core/handlers/sheetNotiHandler.ts @@ -37,9 +37,11 @@ export function handleSheetResponseFromNoti(raw_payload: any, ref: string, count switch (ref) { case 'price': let price_contents: PayloadFromSheet[] = raw_payload.content; + console.log(`price content length: ${price_contents.length}`); let header_idx = PRICE_SHEET_DEFINITION_BY_COUNTRY[country ?? 'unknown'].get_header_idx( price_contents[0].header ); + console.log(`header idx: ${header_idx}`); let lastRequestSheetInstance = get(lastRequestSheetPrice); let products = lastRequestSheetInstance[country ?? 'unknown']; From 244a23483351da089b3b0c6ce296b4485f9b0f60 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 27 May 2026 08:15:45 +0700 Subject: [PATCH 03/10] fix: cells not found - change: disable log health check websocket Signed-off-by: pakintada@gmail.com --- src/lib/core/handlers/sheetNotiHandler.ts | 16 ++++++++++------ src/lib/core/stores/websocketStore.ts | 5 ++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/lib/core/handlers/sheetNotiHandler.ts b/src/lib/core/handlers/sheetNotiHandler.ts index 81367ba..bfcec2f 100644 --- a/src/lib/core/handlers/sheetNotiHandler.ts +++ b/src/lib/core/handlers/sheetNotiHandler.ts @@ -4,18 +4,18 @@ import { lastRequestSheetPrice } from '../stores/recipeStore'; export interface PayloadFromSheet { header: string[]; key: string; - payload: GristCell[]; + payload?: GristCell[]; } export interface GristCell { - cells: { + cells?: { coord: { col: number; row: number; }; value: string; }[]; - row_index: number; + row_index?: number; } const PRICE_SHEET_DEFINITION_BY_COUNTRY: any = { @@ -53,9 +53,13 @@ export function handleSheetResponseFromNoti(raw_payload: any, ref: string, count let price_rows = c.payload; // get last because last row will always override let expected_row = price_rows[price_rows.length - 1]; - let price_col = expected_row.cells[price_idx]; - products[curr_product_code] = price_col; - console.log(`[handleSheetPrice][country] ${curr_product_code} --> ${price_col}`); + if (expected_row.cells != undefined) { + let price_col = expected_row.cells[price_idx]; + products[curr_product_code] = price_col; + console.log(`[handleSheetPrice][country] ${curr_product_code} --> ${price_col}`); + } else { + console.log(`[handleSheetPrice][country] ${curr_product_code} not found cell`); + } } lastRequestSheetInstance[country ?? 'unknown'] = products; diff --git a/src/lib/core/stores/websocketStore.ts b/src/lib/core/stores/websocketStore.ts index 6d9271d..f3f7a18 100644 --- a/src/lib/core/stores/websocketStore.ts +++ b/src/lib/core/stores/websocketStore.ts @@ -10,6 +10,7 @@ import { permission } from './permissions'; let socket: WebSocket | null = null; let reconnectTimeout: any; +let socketCheck: any; const ENABLE_WS_DEBUG: boolean = false; export const socketConnectionOfflineCount = writable(0); @@ -55,7 +56,7 @@ export function connectToWebsocket(id_token?: string) { } // heartbeat 10s - setInterval(() => { + socketCheck = setInterval(() => { if (get(socketAlreadySendHeartbeat) > 0) { let heartbeat_may_offline_count = get(socketConnectionOfflineCount); @@ -103,6 +104,8 @@ export function connectToWebsocket(id_token?: string) { socketStore.set(null); socket = null; + clearInterval(socketCheck); + if (auth.currentUser && !socket) { console.log('try reconnect websocket ...'); // retry again From 8b0479bf5869edcea0d541d3c460823fc532f756 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 27 May 2026 08:22:14 +0700 Subject: [PATCH 04/10] fix: case cell not found Signed-off-by: pakintada@gmail.com --- src/lib/core/handlers/sheetNotiHandler.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/lib/core/handlers/sheetNotiHandler.ts b/src/lib/core/handlers/sheetNotiHandler.ts index bfcec2f..f3c95fa 100644 --- a/src/lib/core/handlers/sheetNotiHandler.ts +++ b/src/lib/core/handlers/sheetNotiHandler.ts @@ -51,14 +51,19 @@ export function handleSheetResponseFromNoti(raw_payload: any, ref: string, count // price idx should be last let price_idx = header_idx[header_idx.length - 1]; let price_rows = c.payload; + if (!price_rows) { + continue; + } // get last because last row will always override let expected_row = price_rows[price_rows.length - 1]; - if (expected_row.cells != undefined) { + if (expected_row != undefined && expected_row.cells != undefined) { let price_col = expected_row.cells[price_idx]; products[curr_product_code] = price_col; console.log(`[handleSheetPrice][country] ${curr_product_code} --> ${price_col}`); } else { - console.log(`[handleSheetPrice][country] ${curr_product_code} not found cell`); + console.log( + `[handleSheetPrice][country] ${curr_product_code} not found cell, ${JSON.stringify(price_rows)}` + ); } } From be785825fe87b7826108d7ce25ceead632b210b9 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 10 Jun 2026 15:24:21 +0700 Subject: [PATCH 05/10] wip: add more detail in recipe dialog Signed-off-by: pakintada@gmail.com --- ISSUES.txt | 4 +- bun.lockb | Bin 197391 -> 197439 bytes package.json | 2 +- .../recipe-details/recipe-detail.svelte | 221 ++++++++++++++++-- 4 files changed, 202 insertions(+), 25 deletions(-) diff --git a/ISSUES.txt b/ISSUES.txt index 279018a..9ed05e6 100644 --- a/ISSUES.txt +++ b/ISSUES.txt @@ -5,8 +5,6 @@ Idea, Issue, Work Tracking [Pending] -- [] #3: Save value to recipe -- [] #6: display all recipes with materials from csv [material usages with product code] - [] #7: material & menu creation - [] #9: show & edit price @@ -19,5 +17,7 @@ Idea, Issue, Work Tracking - [x] #2: Send change value from editing in recipe to machine - [x] #5: revert value on close dialog recipe - [x] #8: change recipe version +- [x] #3: Save value to recipe +- [x] #6: display all recipes with materials from csv [material usages with product code] diff --git a/bun.lockb b/bun.lockb index 52bad831ce0369ba833140d1d46118d17a57f553..9851d42d89cdb58214e184f379e021b0bcb0471e 100755 GIT binary patch delta 31524 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 C$*leW delta 31479 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 diff --git a/package.json b/package.json index e8cdbaa..eb20ce4 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@yume-chan/scrcpy": "^2.3.0", "@yume-chan/stream-extra": "^2.5.3", "animejs": "^4.3.6", - "firebase": "^12.11.0", + "firebase": "^12.14.0", "idb": "^8.0.3", "mode-watcher": "^1.1.0", "usb": "^2.17.0", diff --git a/src/lib/components/recipe-details/recipe-detail.svelte b/src/lib/components/recipe-details/recipe-detail.svelte index 6e68b03..967d1bb 100644 --- a/src/lib/components/recipe-details/recipe-detail.svelte +++ b/src/lib/components/recipe-details/recipe-detail.svelte @@ -16,6 +16,7 @@ import { get, readable, writable } from 'svelte/store'; import { currentEditingRecipeProductCode, + lastRequestSheetPrice, latestRecipeToppingData, materialFromMachineQuery, materialFromServerQuery, @@ -28,6 +29,8 @@ import { addNotification } from '$lib/core/stores/noti'; import { env } from '$env/dynamic/public'; import { sendCommand, sendReset } from '$lib/core/brew/command'; + import { sendCommandRequest } from '$lib/core/handlers/ws_messageSender'; + import { needPermission } from '$lib/core/handlers/permissionHandler'; import { isAdbWriterAvailable } from '$lib/core/stores/adbWriter'; import { sendToAndroid } from '$lib/core/stores/adbWriter'; import { departmentStore } from '$lib/core/stores/departments'; @@ -43,6 +46,10 @@ let menuName: string = $state(''); let menuCurrentPrice: number = $state(0); let isMenuHideByPrice: boolean = $state(false); + let sheetPriceValue: number | null = $state(null); + let showSheetPrice: boolean = $state(false); + let canEditSheetPrice: boolean = $state(false); + let sheetPriceRawCell: any = $state(null); let materialSnapshot: any = $state(); let machineInfoSnapshot: any = $state(); @@ -52,6 +59,8 @@ let toppingSlotState: any = $state([]); + let unsubSheetPrice: (() => void) | null = null; + const recipeDetailDispatch = createEventDispatcher(); function remappingToColumn(data: any[]): RecipelistMaterial[] { @@ -167,6 +176,22 @@ } } + function saveSheetPrice() { + if (!canEditSheetPrice || sheetPriceValue === null) return; + sendCommandRequest('sheet', { + country: get(departmentStore), + content: [ + { + product_code: productCode, + new_price: sheetPriceValue, + cell_coord: sheetPriceRawCell?.coord + } + ], + param: 'price', + action: 'update' + }); + } + async function checkChanges(productCode: string, original: any) { // console.log('old', original, 'updated', recipeListMatState); if (recipeListOriginal.length == 0) { @@ -182,6 +207,26 @@ } } + function updateSheetPrice(sheetData: any) { + if (!productCode) return; + const country = get(departmentStore); + if (!country) return; + + const sheetEntry = sheetData[country]?.[productCode]; + if (sheetEntry && typeof sheetEntry === 'object' && sheetEntry?.value) { + sheetPriceRawCell = sheetEntry; + const parsed = parseInt(sheetEntry.value); + if (!isNaN(parsed) && parsed !== menuCurrentPrice) { + sheetPriceValue = parsed; + showSheetPrice = true; + return; + } + } + + sheetPriceValue = null; + showSheetPrice = false; + } + onMount(() => { machineInfoSnapshot = get(machineInfoStore); @@ -231,8 +276,19 @@ } catch (e) {} } - // save old value\ + // save old value } + + canEditSheetPrice = needPermission('document.write.*'); + + updateSheetPrice(get(lastRequestSheetPrice)); + unsubSheetPrice = lastRequestSheetPrice.subscribe((data) => { + updateSheetPrice(data); + }); + }); + + onDestroy(() => { + if (unsubSheetPrice) unsubSheetPrice(); }); @@ -265,31 +321,152 @@ Info about this menu - -
-
- - + + + + Basic Information + + +
+
+ + +
+
+ + +
+
+
-
- - + + + + Price Information + + +
+ +
+ + +
+ + {#if showSheetPrice} + +
+ +
+ + {#if canEditSheetPrice} + + {/if} +
+
+ {/if} + + +
+
+ + Disabled by Price +
+
-
+ + -
- - -
- - -
- - Disabled - -
- + + + + Additional Recipe Data + + + {#if recipeData} +
+
+ + + {recipeData.LastChange ?? 'N/A'} + +
+
+ + { + const input = e.target as HTMLInputElement | null; + if (input && input.value !== '') { + const value = parseInt(input.value); + if (!isNaN(value)) { + recipeData.total_time = value; + // Notify parent of change + onPendingChange({ + target: 'recipeData', + ref_pd: productCode, + value: { ...recipeData, total_time: value } + }); + } + } + }} + /> +
+
+ +
+
+ + { + const input = e.target as HTMLInputElement | null; + if (input) { + recipeData.uriData = input.value; + onPendingChange({ + target: 'recipeData', + ref_pd: productCode, + value: { ...recipeData, uriData: input.value } + }); + } + }} + /> +
+
+ {:else} +

No recipe data available

+ {/if} +
+
From 828089ed2fabcb938567d2c39d4555afb363858f Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 10 Jun 2026 16:21:34 +0700 Subject: [PATCH 06/10] change: disable price display on recipe detail Signed-off-by: pakintada@gmail.com --- .../recipe-details/recipe-detail.svelte | 48 +------------------ 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/src/lib/components/recipe-details/recipe-detail.svelte b/src/lib/components/recipe-details/recipe-detail.svelte index f9bb96b..455dcfd 100644 --- a/src/lib/components/recipe-details/recipe-detail.svelte +++ b/src/lib/components/recipe-details/recipe-detail.svelte @@ -348,50 +348,6 @@ - - - Price Information - - -
- -
- - -
- - {#if showSheetPrice} - -
- -
- - {#if canEditSheetPrice} - - {/if} -
-
- {/if} - - -
-
- - Disabled by Price -
-
-
-
-
@@ -403,7 +359,7 @@
- + {recipeData.LastChange ?? 'N/A'}
@@ -412,7 +368,7 @@ { const input = e.target as HTMLInputElement | null; if (input && input.value !== '') { From 9dc1e65f5eabf310448f880f6a619abc06cb6a6a Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 10 Jun 2026 16:31:15 +0700 Subject: [PATCH 07/10] change: disable auto connect - from case first time connection may not able to connect to socket on android Signed-off-by: pakintada@gmail.com --- src/routes/(authed)/+layout.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/(authed)/+layout.svelte b/src/routes/(authed)/+layout.svelte index 34b2e1b..926c2d3 100644 --- a/src/routes/(authed)/+layout.svelte +++ b/src/routes/(authed)/+layout.svelte @@ -104,7 +104,7 @@ if (adbReconnectTriedForUid !== currentUser.uid && !adb.getAdbInstance()) { adbReconnectTriedForUid = currentUser.uid; - void tryAutoConnect(); + // void tryAutoConnect(); } }); From b3024e92c7629a6db6d46269d48895feb8afc93b Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 10 Jun 2026 16:54:43 +0700 Subject: [PATCH 08/10] add: open brew app when enter brew Signed-off-by: pakintada@gmail.com --- src/routes/(authed)/tools/brew/+page.svelte | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/routes/(authed)/tools/brew/+page.svelte b/src/routes/(authed)/tools/brew/+page.svelte index eddda99..e5e4c92 100644 --- a/src/routes/(authed)/tools/brew/+page.svelte +++ b/src/routes/(authed)/tools/brew/+page.svelte @@ -183,6 +183,7 @@ await startFetchRecipeFromMachine(); await loadEssentialFiles(); await loadStagedMenusFromAndroid(); + await openBrewApp(); } async function tryAutoConnect() { @@ -233,6 +234,55 @@ } } + async function openBrewApp() { + try { + let instance = adb.getAdbInstance(); + if (instance) { + try { + // bypass + await adb.executeCmd('echo -n hurr > /sdcard/coffeevending/ignore_pass'); + } catch (e) {} + + let result = await adb.executeCmd( + 'am start -n com.forthvending.coffeemain/com.forthvending.coffeemain.MainActivity' + ); + // if (result?.output) { + // toast.success('Open app success!'); + // machineStatus = 'open app success, check the screen and put the password'; + // } else if (result?.error) { + // // case usb connection cutoff + // if (result.error === 'ExactReadableEndedError') { + // toast.warning('Connection unstable'); + // machineStatus = 'app maybe opened, check the screen'; + // } else { + // throw new Error(`Exit ${result.exitCode}. ${result.error}`); + // } + // } else { + // throw new Error('Instance not found or error while executing'); + // } + + // hasOpenedBrewOnce = true; + + try { + // bypass + await adb.executeCmd('echo -n hurr > /sdcard/coffeevending/ignore_pass'); + } catch (e) {} + + setTimeout(async () => { + try { + // bypass + await adb.executeCmd('input tap 336 795'); + } catch (e) {} + }, 3000); + } + } catch (e: any) { + // machineStatus = 'Cannot open brew app'; + // toast.error('Error while trying to open brew app, please check the screen. ', { + // description: e.toString() + // }); + } + } + async function ensureAndroidSocket() { if (isAdbWriterAvailable()) return true; From 4ca8b3b2707120842c84a293e4c495d69be608b1 Mon Sep 17 00:00:00 2001 From: "pakintada@gmail.com" Date: Wed, 10 Jun 2026 17:03:36 +0700 Subject: [PATCH 09/10] update tasks todo Signed-off-by: pakintada@gmail.com --- ISSUES.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ISSUES.txt b/ISSUES.txt index 9ed05e6..005e4a5 100644 --- a/ISSUES.txt +++ b/ISSUES.txt @@ -1,11 +1,14 @@ Idea, Issue, Work Tracking [TODO] - +- [] #10: [MenuCreation] recipe fill in until maximum limit (30) +- [] #11: Bring android app to front (`/brew` -> do swap to brew app, `/sheet` -> do swap to xml engine app) +- [] #12: [MenuCreation] Adjust input per material type [Pending] -- [] #7: material & menu creation +- [-] #7: material & menu creation + > Menu creation ready! - [] #9: show & edit price [Rejected] From bd239cf71b6a00e3a1f776f3cab6a90ac2271ff0 Mon Sep 17 00:00:00 2001 From: thanawat saiyota Date: Thu, 11 Jun 2026 16:25:27 +0700 Subject: [PATCH 10/10] create topping and material page --- src/lib/components/app-sidebar.svelte | 9 +- src/lib/core/handlers/messageHandler.ts | 106 +- src/lib/core/services/sheetService.ts | 23 + src/lib/core/stores/sheetStore.ts | 18 + src/routes/(authed)/departments/+page.svelte | 4 +- .../(authed)/recipe/material/+page.svelte | 842 +++++++++++++ .../(authed)/recipe/topping/+page.svelte | 1071 +++++++++++++++++ .../sheet/priceslot/[country]/+page.svelte | 586 +++++++++ .../(authed)/tools/adv-upload/+page.svelte | 4 +- 9 files changed, 2606 insertions(+), 57 deletions(-) create mode 100644 src/routes/(authed)/sheet/priceslot/[country]/+page.svelte diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte index d7475dd..34de1f0 100644 --- a/src/lib/components/app-sidebar.svelte +++ b/src/lib/components/app-sidebar.svelte @@ -12,6 +12,7 @@ CupSodaIcon, Shield, FileSpreadsheet, + DollarSign, MonitorSmartphone, PlusCircle, ImageUp, @@ -135,6 +136,12 @@ url: '/departments', icon: FileSpreadsheet, requirePerm: 'document.write.*' + }, + { + title: 'PriceSlot', + url: '/departments', + icon: DollarSign, + requirePerm: 'document.write.*' } ] } @@ -228,7 +235,7 @@ onclick={(e) => { if (nav.title === 'Sheet') { e.preventDefault(); - referenceFromPage.set('sheet'); + referenceFromPage.set(sub.title === 'PriceSlot' ? 'priceslot' : 'sheet'); goto(sub.url); } }} diff --git a/src/lib/core/handlers/messageHandler.ts b/src/lib/core/handlers/messageHandler.ts index b875029..61cd6a3 100644 --- a/src/lib/core/handlers/messageHandler.ts +++ b/src/lib/core/handlers/messageHandler.ts @@ -395,59 +395,59 @@ const handlers: Record void> = { lastRequestSheetPrice.set(lastRequestPriceInstance); }, - raw_stream: (p) => { - let streamRawInstance = get(streamingRawData); - let sub_type = p.sub_type; - let request_id = p.request_id; - let size_per_chunk = p.size_per_chunk; - let total_chunks = p.total_chunks; - let idx = p.idx; + // raw_stream: (p) => { + // let streamRawInstance = get(streamingRawData); + // let sub_type = p.sub_type; + // let request_id = p.request_id; + // let size_per_chunk = p.size_per_chunk; + // let total_chunks = p.total_chunks; + // let idx = p.idx; - switch (sub_type) { - case 'price': - streamingRawMeta.set({ - id: request_id, - total_size: total_chunks, - chunk_size: size_per_chunk, - progress: 0 - }); - break; - case 'chunk_price': - streamingRawMeta.set({ - id: request_id, - total_size: total_chunks, - chunk_size: size_per_chunk, - progress: idx - }); + // switch (sub_type) { + // case 'price': + // streamingRawMeta.set({ + // id: request_id, + // total_size: total_chunks, + // chunk_size: size_per_chunk, + // progress: 0 + // }); + // break; + // case 'chunk_price': + // streamingRawMeta.set({ + // id: request_id, + // total_size: total_chunks, + // chunk_size: size_per_chunk, + // progress: idx + // }); - let raw_payload = p.raw ?? ''; - streamRawInstance[request_id] += raw_payload; - streamingRawData.set(streamRawInstance); + // let raw_payload = p.raw ?? ''; + // streamRawInstance[request_id] += raw_payload; + // streamingRawData.set(streamRawInstance); - break; - case 'end_price': - let lastRequestPriceInstance = get(lastRequestSheetPrice); - let country = lastRequestPriceInstance[request_id]; + // break; + // case 'end_price': + // let lastRequestPriceInstance = get(lastRequestSheetPrice); + // let country = lastRequestPriceInstance[request_id]; - try { - let raw_payload = JSON.parse(streamRawInstance[request_id]); - let ref_from_raw = raw_payload.payload.ref ?? ''; - let from_service_raw = raw_payload.payload.from ?? ''; - let parsed_payload = raw_payload.payload ?? ''; + // try { + // let raw_payload = JSON.parse(streamRawInstance[request_id]); + // let ref_from_raw = raw_payload.payload.ref ?? ''; + // let from_service_raw = raw_payload.payload.from ?? ''; + // let parsed_payload = raw_payload.payload ?? ''; - if (from_service_raw == 'sheet-service') { - handleSheetResponseFromNoti(parsed_payload, ref_from_raw, country); - delete streamRawInstance[request_id]; - streamingRawData.set(streamRawInstance); - } - } catch (e) { - console.log(`end price process error: ${e}`); - } + // if (from_service_raw == 'sheet-service') { + // handleSheetResponseFromNoti(parsed_payload, ref_from_raw, country); + // delete streamRawInstance[request_id]; + // streamingRawData.set(streamRawInstance); + // } + // } catch (e) { + // console.log(`end price process error: ${e}`); + // } - break; - default: - } - }, + // break; + // default: + // } + // }, heartbeat: (p) => { socketConnectionOfflineCount.set(0); socketAlreadySendHeartbeat.set(0); @@ -486,12 +486,12 @@ export function handleIncomingMessages(raw: string) { } // raw streaming type - if (msg.type.startsWith('raw_stream')) { - // convert - let sub_type = msg.type.replace('raw_stream_', ''); - msg.payload.sub_type = sub_type; - msg.type = 'raw_stream'; - } + // if (msg.type.startsWith('raw_stream')) { + // // convert + // let sub_type = msg.type.replace('raw_stream_', ''); + // msg.payload.sub_type = sub_type; + // msg.type = 'raw_stream'; + // } handlers[msg.type]?.(msg.payload); } diff --git a/src/lib/core/services/sheetService.ts b/src/lib/core/services/sheetService.ts index 2e15f35..8fb6c39 100644 --- a/src/lib/core/services/sheetService.ts +++ b/src/lib/core/services/sheetService.ts @@ -18,6 +18,29 @@ export function requestCatalogs(country: string): boolean { }); } +export function requestPriceSlots(country: string): boolean { + return sendCommandRequest('sheet', { + country: country, + param: 'priceslot' + }); +} + +export function updatePriceSlot( + country: string, + content: { + slot: number; + name: string; + description: string; + products: { product_code: string; price: number | null; row_index?: number }[]; + } +): boolean { + return sendCommandRequest('sheet', { + country: country, + content: content, + param: 'update/priceslot' + }); +} + export function enterRoom(country: string, catalog: string): boolean { return sendCommandRequest('sheet', { country: country, diff --git a/src/lib/core/stores/sheetStore.ts b/src/lib/core/stores/sheetStore.ts index 5a2e0dc..eb2d626 100644 --- a/src/lib/core/stores/sheetStore.ts +++ b/src/lib/core/stores/sheetStore.ts @@ -17,6 +17,24 @@ export interface CatalogsResponse { export const sheetCatalogs = writable([]); export const sheetCatalogsLoading = writable(false); +export interface PriceSlotProduct { + product_code: string; + name: string; + price: number | null; + row_index?: number; +} + +export interface PriceSlot { + slot: number; + name: string; + description: string; + products: PriceSlotProduct[]; +} + +export const priceSlots = writable>({}); +export const priceSlotsLoading = writable(false); +export const priceSlotsError = writable(null); + export const countryPrimaryLanguageMap: Record = { THAI: 'Thai', tha: 'Thai', diff --git a/src/routes/(authed)/departments/+page.svelte b/src/routes/(authed)/departments/+page.svelte index e2e80f6..77ce3d9 100644 --- a/src/routes/(authed)/departments/+page.svelte +++ b/src/routes/(authed)/departments/+page.svelte @@ -25,7 +25,9 @@ console.log(get(departmentStore)); departmentStore.set(cnt); - if (refPage === 'sheet') { + if (refPage === 'priceslot') { + await goto(`/sheet/priceslot/${cnt}`); + } else if (refPage === 'sheet') { await goto(`/sheet/overview/${cnt}`); } else { await goto('/recipe/overview'); diff --git a/src/routes/(authed)/recipe/material/+page.svelte b/src/routes/(authed)/recipe/material/+page.svelte index e69de29..e7beba8 100644 --- a/src/routes/(authed)/recipe/material/+page.svelte +++ b/src/routes/(authed)/recipe/material/+page.svelte @@ -0,0 +1,842 @@ + + +
+
+
+
+

+ Android Recipe +

+

Material Setting

+ + {#if loadedRecipePath} +

Loaded: {loadedRecipePath}

+ {/if} +
+ +
+ +
+
+
+ +
+
+
+
Total materials
+
{materials.length}
+
+
+
+
Active materials
+
{activeMaterialCount}
+
+
+
+
Channels in use
+
{channelSummary.length}
+
+
+ + + + + {existingMaterial ? 'Edit Material' : 'Add Material'} + + Create or update one MaterialSetting entry. The JSON preview shows the payload + before saving to Android. + + + +
+ + + {existingMaterial ? 'Edit Material' : 'Add Material'} + + + {#if existingMaterial} +
+ Material ID {form.id} already exists. Saving will update this MaterialSetting. +
+ {/if} + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +

+ Examples: refill=$bag,sum=$gram,rec=$gram, + refill=$L,sum=$ml,rec=$ml +

+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + + +
+ +
+ + +
+ +
+ + + +
+
+
+ + + + Preview JSON + + Payload that will be upserted into MaterialSetting. + + + +
{previewJson}
+
+
+
+
+
+ + + +
+
+ Existing Materials + + Use Edit to update a material, or Delete to remove it after confirmation. + +
+ +
+
+ +
+ +
+ {#each channelSummary as channel} + + {channel.label}: {channel.count} + + {/each} +
+
+
+ {#if loading} +
+ + Loading materials from Android... +
+ {:else if !devRecipe} +
Connect and load recipe first.
+ {:else if filteredMaterials.length === 0} +
No materials found.
+ {:else} + +
+ {#each filteredMaterials as material} +
+ {material.id} + {material.materialName || '-'} + {material.materialOtherName || '-'} + {material.pathOtherName || '-'} + + {(material.isUse as boolean) !== false ? 'Use' : 'Not use'} + +
+ + +
+
+ {/each} +
+ {/if} +
+
+
+ + + + + Delete Material? + + This will remove the material from MaterialSetting in the Android recipe JSON. + + + + {#if pendingDeleteMaterial} +
+
Material
+
{pendingDeleteMaterial.id}
+
+ {pendingDeleteMaterial.materialName || + pendingDeleteMaterial.materialOtherName || + 'Unnamed'} +
+
+ {/if} + +
+ + +
+
+
+
diff --git a/src/routes/(authed)/recipe/topping/+page.svelte b/src/routes/(authed)/recipe/topping/+page.svelte index e69de29..c0e2a25 100644 --- a/src/routes/(authed)/recipe/topping/+page.svelte +++ b/src/routes/(authed)/recipe/topping/+page.svelte @@ -0,0 +1,1071 @@ + + +
+
+
+
+

+ Android Recipe +

+

Topping

+ + {#if loadedRecipePath} +

Loaded: {loadedRecipePath}

+ {/if} +
+ + +
+
+ +
+
+
+
ToppingList
+
{toppingList.length}
+
+
+
+
Active list items
+
{activeListCount}
+
+
+
+
ToppingGroup
+
{toppingGroups.length}
+
+
+ + + +
+
+ {activeTab === 'list' ? 'Topping List' : 'Topping Group'} + + Switch between list items and groups. Edit/Delete actions are explicit per row. + +
+
+ + +
+
+
+ +
+ + {#if activeTab === 'list'} + + {:else} + + {/if} +
+ +
+ {#if loading} +
+ + Loading toppings from Android... +
+ {:else if !devRecipe} +
Connect and load recipe first.
+ {:else if activeTab === 'list'} + {#if filteredToppingList.length === 0} +
No topping list items found.
+ {:else} + +
+ {#each filteredToppingList as item} +
+ {item.id} + {item.name || '-'} + {item.otherName || '-'} + + {(item.isUse as boolean) !== false ? 'Use' : 'Not use'} + +
+ + +
+
+ {/each} +
+ {/if} + {:else if filteredToppingGroups.length === 0} +
No topping groups found.
+ {:else} + +
+ {#each filteredToppingGroups as group} +
+ {group.groupID} + {group.name || '-'} + {group.otherName || '-'} + {group.idInGroup || '-'} + + {(group.inUse as boolean) !== false ? 'Use' : 'Not use'} + +
+ + +
+
+ {/each} +
+ {/if} +
+
+
+ + + + + {existingListItem ? 'Edit Topping' : 'Add Topping'} + Manage one item inside ToppingList. + + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + + +
+
+
+
Recipe steps
+
+ Some toppings need multiple recipe JSON steps, such as sugar plus stir. +
+
+ +
+ +
+ {#each listForm.recipeSteps as step, index} +
+
+
Step {index + 1}
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ {/each} +
+
+ +
+ + +
+
+
+ + + + Preview JSON + + +
{JSON.stringify(
+								listPreview,
+								null,
+								2
+							)}
+
+
+
+
+
+ + + + + {existingGroup ? 'Edit Topping Group' : 'Add Topping Group'} + Manage one group inside ToppingGroup. + + +
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +

Comma-separated `ToppingList.id` values.

+
+ +
+ + +
+
+
+ + + + Preview JSON + + +
{JSON.stringify(
+								groupPreview,
+								null,
+								2
+							)}
+
+
+
+
+
+ + + + + Delete Topping? + This will remove the selected entry from Android recipe JSON. + + + {#if pendingDelete} +
+
+ {pendingDelete.type === 'list' ? 'ToppingList' : 'ToppingGroup'} +
+
+ {pendingDelete.type === 'list' + ? (pendingDelete.item as ToppingListItem).id + : (pendingDelete.item as ToppingGroup).groupID} +
+
+ {pendingDelete.item.name || pendingDelete.item.otherName || 'Unnamed'} +
+
+ {/if} + +
+ + +
+
+
+
diff --git a/src/routes/(authed)/sheet/priceslot/[country]/+page.svelte b/src/routes/(authed)/sheet/priceslot/[country]/+page.svelte new file mode 100644 index 0000000..86a2542 --- /dev/null +++ b/src/routes/(authed)/sheet/priceslot/[country]/+page.svelte @@ -0,0 +1,586 @@ + + +
+
+
+
+

+ PriceSlot [ {selectedCountry.toUpperCase()} ] +

+

+ Edit sheet PriceSlot names, descriptions, and product prices. +

+
+
+ {#if enabledCountries.length > 0} + { + if (v) { + selectedCountry = v; + goto(`/sheet/priceslot/${v}`); + } + }} + > + + {selectedCountry.toUpperCase()} + + + {#each enabledCountries as country} + + {country.toUpperCase()} + + {/each} + + + {/if} + + +
+
+ +
+ {#each slots as slot} + + {/each} +
+ +
+
+
+
+

PriceSlot{selectedSlot}

+ + {hasChanges ? `${changedCount} changes` : 'No changes'} + +
+ +
+ +
+ + updateSlotField('name', event.currentTarget.value)} + /> +
+ +
+ + updateSlotField('description', event.currentTarget.value)} + /> +
+ +
+ + + +
+
+
+ +
+
+
+ +
+ + +
+
+

+ Showing {filteredProducts.length} of {currentSlot.products.length} products +

+
+ +
+ + + + ProductCode + ProductName [{selectedCountryLanguage}] + ProductNameEng + Price + + + + {#each filteredProducts as product (product.product_code)} + {@const productNames = getProductNames(product)} + + + {product.product_code} + + {productNames.local} + {productNames.english} + + + updateProductPrice(product.product_code, event.currentTarget.value)} + /> + + + {/each} + {#if filteredProducts.length === 0} + + + No product code found. + + + {/if} + + +
+
+
+
+ + + + + Create PriceSlot + + Choose how to adjust base prices before creating a new PriceSlot. + + + +
+ + +
+
+ + { + if (v) adjustmentMode = v as AdjustmentMode; + applyCreateTemplate(); + }} + > + + {adjustmentModeLabels[adjustmentMode]} + + + Increase by Percentage (%) + Increase by Fixed Amount + Decrease by Percentage (%) + Decrease by Fixed Amount + + +
+ +
+ + { + adjustmentValue = Number(event.currentTarget.value); + applyCreateTemplate(); + }} + /> +
+ +
+ + (createName = event.currentTarget.value)} + /> +
+ +
+ + (createDescription = event.currentTarget.value)} + /> +
+
+
+ + + + + +
+
diff --git a/src/routes/(authed)/tools/adv-upload/+page.svelte b/src/routes/(authed)/tools/adv-upload/+page.svelte index ec63447..1735f59 100644 --- a/src/routes/(authed)/tools/adv-upload/+page.svelte +++ b/src/routes/(authed)/tools/adv-upload/+page.svelte @@ -28,8 +28,8 @@ // pushes the selected .mp4 (from the browser), then // `ls -l > sync_1.file` on the machine, pulls it, uploads it. // ⚠️ FULL REPLACE — requires ADB; select the COMPLETE adv set. - //const MANIFEST_MODE: 'ftp_listdir' | 'machine' = 'ftp_listdir'; - const MANIFEST_MODE: 'ftp_listdir' | 'machine' = 'machine'; + const MANIFEST_MODE: 'ftp_listdir' | 'machine' = 'ftp_listdir'; + //const MANIFEST_MODE: 'ftp_listdir' | 'machine' = 'machine'; // ───────────────────────────────────────────────────────────────────────── // adv folder on the machine. Domestic Thailand uses the flat folder; every