From bd239cf71b6a00e3a1f776f3cab6a90ac2271ff0 Mon Sep 17 00:00:00 2001 From: thanawat saiyota Date: Thu, 11 Jun 2026 16:25:27 +0700 Subject: [PATCH] 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