diff --git a/ISSUES.txt b/ISSUES.txt index 279018a..005e4a5 100644 --- a/ISSUES.txt +++ b/ISSUES.txt @@ -1,13 +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] -- [] #3: Save value to recipe -- [] #6: display all recipes with materials from csv [material usages with product code] -- [] #7: material & menu creation +- [-] #7: material & menu creation + > Menu creation ready! - [] #9: show & edit price [Rejected] @@ -19,5 +20,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 new file mode 100755 index 0000000..9851d42 Binary files /dev/null and b/bun.lockb differ 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/components/recipe-details/recipe-detail.svelte b/src/lib/components/recipe-details/recipe-detail.svelte index 77673d0..455dcfd 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'; @@ -15,6 +16,7 @@ import { get, readable, writable } from 'svelte/store'; import { currentEditingRecipeProductCode, + lastRequestSheetPrice, latestRecipeToppingData, materialFromMachineQuery, materialFromServerQuery, @@ -27,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'; @@ -42,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(); @@ -51,6 +59,8 @@ let toppingSlotState: any = $state([]); + let unsubSheetPrice: (() => void) | null = null; + const recipeDetailDispatch = createEventDispatcher(); function remappingToColumn(data: any[]): RecipelistMaterial[] { @@ -173,6 +183,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) { @@ -188,6 +214,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); @@ -237,8 +283,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(); }); @@ -271,25 +328,108 @@ Info about this menu - -
- - - - -
+ + + + Basic Information + + +
+
+ + +
+
+ + +
+
+
+
-
- - - -
-
+ + + + + + 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} +
+
diff --git a/src/lib/components/recipe-editor-dialog.svelte b/src/lib/components/recipe-editor-dialog.svelte index 8ee583d..e593d4b 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 } @@ -322,31 +351,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/adbPayloadHandler.ts b/src/lib/core/handlers/adbPayloadHandler.ts index 8d6f63d..fbc3f2b 100644 --- a/src/lib/core/handlers/adbPayloadHandler.ts +++ b/src/lib/core/handlers/adbPayloadHandler.ts @@ -1,3 +1,4 @@ +import { get } from 'svelte/store'; import { updateMachineStatus } from '../stores/machineInfoStore'; import { addNotification } from '../stores/noti'; import { @@ -6,6 +7,7 @@ import { } from '../services/androidRecipeExportService'; import { handleIncomingMessages } from './messageHandler'; import { setMenuSaved, setMenuSaveError } from '../stores/menuSaveStore'; +import { recipeFromMachineQuery } from '../stores/recipeStore'; type AdbPayload = { type: string; payload: any }; @@ -104,9 +106,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 1f8c69b..61cd6a3 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'; @@ -36,13 +39,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'), @@ -311,6 +317,9 @@ const handlers: Record void> = { } // Default notification handling + let from_service = p.from ?? ''; + let ref_service = p.ref ?? ''; + if (target) { let currentUsername = auth.currentUser?.displayName; if (currentUsername && currentUsername === target) { @@ -353,12 +362,92 @@ 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.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}`); + // } + + // break; + // default: + // } + // }, heartbeat: (p) => { socketConnectionOfflineCount.set(0); socketAlreadySendHeartbeat.set(0); @@ -395,5 +484,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..f3c95fa --- /dev/null +++ b/src/lib/core/handlers/sheetNotiHandler.ts @@ -0,0 +1,79 @@ +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; + 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']; + + 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; + if (!price_rows) { + continue; + } + // get last because last row will always override + let expected_row = price_rows[price_rows.length - 1]; + 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, ${JSON.stringify(price_rows)}` + ); + } + } + + lastRequestSheetInstance[country ?? 'unknown'] = products; + lastRequestSheetPrice.set({ + ...lastRequestSheetInstance, + products + }); + + break; + default: + } +} 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/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/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/lib/core/stores/websocketStore.ts b/src/lib/core/stores/websocketStore.ts index d43e064..720fb7e 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); @@ -95,7 +96,7 @@ export function connectToWebsocket(id_token?: string) { console.log(socket); // heartbeat 10s - setInterval(() => { + socketCheck = setInterval(() => { if (get(socketAlreadySendHeartbeat) > 0) { let heartbeat_may_offline_count = get(socketConnectionOfflineCount); @@ -143,6 +144,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 diff --git a/src/lib/core/types/outMessage.ts b/src/lib/core/types/outMessage.ts index ff24f95..f51bd46 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)/+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(); } }); 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 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;