2026-06-11 16:25:27 +07:00
|
|
|
<script lang="ts">
|
|
|
|
|
import { onMount } from 'svelte';
|
|
|
|
|
import { page } from '$app/stores';
|
|
|
|
|
import { goto } from '$app/navigation';
|
|
|
|
|
import { get } from 'svelte/store';
|
|
|
|
|
import { addNotification } from '$lib/core/stores/noti.js';
|
|
|
|
|
import { departmentStore } from '$lib/core/stores/departments.js';
|
|
|
|
|
import { permission as currentPerms } from '$lib/core/stores/permissions.js';
|
|
|
|
|
import { referenceFromPage } from '$lib/core/stores/recipeStore.js';
|
|
|
|
|
import {
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
existingProductCodes,
|
|
|
|
|
getPriceColumnIndex,
|
2026-06-11 16:25:27 +07:00
|
|
|
getCountryPrimaryLanguage,
|
|
|
|
|
getPriceFromCells,
|
|
|
|
|
lastRequestSheetPrice,
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
priceSlotNamespaces,
|
2026-06-16 11:30:23 +07:00
|
|
|
priceSlots,
|
|
|
|
|
priceSlotsError,
|
|
|
|
|
priceSlotsLoading,
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
sheetPriceAllRows,
|
|
|
|
|
sheetPriceHeader,
|
|
|
|
|
sheetPriceLoading,
|
|
|
|
|
type GristCell,
|
2026-06-11 16:25:27 +07:00
|
|
|
type PriceSlot,
|
2026-06-16 11:30:23 +07:00
|
|
|
type PriceSlotProduct,
|
|
|
|
|
type PriceSlotServiceRow
|
2026-06-11 16:25:27 +07:00
|
|
|
} from '$lib/core/stores/sheetStore.js';
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
import {
|
|
|
|
|
addPriceSlot,
|
|
|
|
|
addPriceSlotRows,
|
|
|
|
|
deletePriceSlotRows,
|
|
|
|
|
requestListMenu,
|
|
|
|
|
requestPriceSlot,
|
|
|
|
|
requestPriceSlots,
|
|
|
|
|
requestSheetPrice,
|
|
|
|
|
updatePriceSlot
|
|
|
|
|
} from '$lib/core/services/sheetService.js';
|
|
|
|
|
import { waitForAuthenticatedSocket } from '$lib/core/stores/websocketStore.js';
|
2026-06-11 16:25:27 +07:00
|
|
|
|
|
|
|
|
import Button from '$lib/components/ui/button/button.svelte';
|
|
|
|
|
import Input from '$lib/components/ui/input/input.svelte';
|
|
|
|
|
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
|
|
|
|
import * as Select from '$lib/components/ui/select/index.js';
|
|
|
|
|
import * as Table from '$lib/components/ui/table/index.js';
|
|
|
|
|
import Badge from '$lib/components/ui/badge/badge.svelte';
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
import {
|
|
|
|
|
Calculator,
|
|
|
|
|
Plus,
|
|
|
|
|
RefreshCw,
|
|
|
|
|
Save,
|
|
|
|
|
RotateCcw,
|
|
|
|
|
Search,
|
|
|
|
|
Trash2
|
|
|
|
|
} from '@lucide/svelte/icons';
|
2026-06-11 16:25:27 +07:00
|
|
|
|
|
|
|
|
type AdjustmentMode =
|
|
|
|
|
| 'increase_percent'
|
|
|
|
|
| 'increase_amount'
|
2026-06-16 11:30:23 +07:00
|
|
|
| 'decrease_percent'
|
|
|
|
|
| 'decrease_amount';
|
2026-06-11 16:25:27 +07:00
|
|
|
|
|
|
|
|
const adjustmentModeLabels: Record<AdjustmentMode, string> = {
|
|
|
|
|
increase_percent: 'Increase by Percentage (%)',
|
|
|
|
|
increase_amount: 'Increase by Fixed Amount',
|
2026-06-16 11:30:23 +07:00
|
|
|
decrease_percent: 'Decrease by Percentage (%)',
|
|
|
|
|
decrease_amount: 'Decrease by Fixed Amount'
|
2026-06-11 16:25:27 +07:00
|
|
|
};
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
const emptySlot: PriceSlot = {
|
|
|
|
|
slot: 0,
|
|
|
|
|
name: '',
|
|
|
|
|
description: '',
|
|
|
|
|
kind: 'price',
|
|
|
|
|
header: [],
|
|
|
|
|
products: []
|
|
|
|
|
};
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
const rowsPerPage = 200;
|
|
|
|
|
const supportedCountries = new Set([
|
|
|
|
|
'aus',
|
|
|
|
|
'gbr',
|
|
|
|
|
'hkg',
|
|
|
|
|
'ltu',
|
|
|
|
|
'mys',
|
|
|
|
|
'rou',
|
|
|
|
|
'sgp',
|
|
|
|
|
'tha',
|
|
|
|
|
'uae_dubai'
|
|
|
|
|
]);
|
2026-06-11 16:25:27 +07:00
|
|
|
|
|
|
|
|
let selectedCountry = $state<string>($page.params.country || get(departmentStore) || '');
|
|
|
|
|
let enabledCountries = $state<string[]>([]);
|
2026-06-16 11:30:23 +07:00
|
|
|
let selectedSlot = $state(0);
|
|
|
|
|
let localSlots = $state<PriceSlot[]>([]);
|
|
|
|
|
let workingSlot = $state<PriceSlot | null>(null);
|
|
|
|
|
let savedSlot = $state<PriceSlot | null>(null);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
let saveLoading = $state(false);
|
2026-06-11 16:25:27 +07:00
|
|
|
let productCodeSearch = $state('');
|
|
|
|
|
let createDialogOpen = $state(false);
|
|
|
|
|
let adjustmentMode = $state<AdjustmentMode>('increase_percent');
|
|
|
|
|
let adjustmentValue = $state(15);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
let createSlotNumber = $state(1);
|
2026-06-11 16:25:27 +07:00
|
|
|
let createName = $state('ProfileIncrease15');
|
|
|
|
|
let createDescription = $state('increase price 15%');
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
let productPage = $state(1);
|
|
|
|
|
let servicePage = $state(1);
|
|
|
|
|
let requestedSlotKeys = new Set<string>();
|
|
|
|
|
let requestedProductCodeCountries = new Set<string>();
|
|
|
|
|
let requestedPriceCountries = new Set<string>();
|
|
|
|
|
let pendingCreateDialog = $state(false);
|
|
|
|
|
let deletedProductRowIds = $state<number[]>([]);
|
|
|
|
|
let createMode = $state<'from_price' | 'blank'>('from_price');
|
|
|
|
|
let addDialogOpen = $state(false);
|
|
|
|
|
let addProductCode = $state('');
|
|
|
|
|
let addProductName = $state('');
|
|
|
|
|
let addProductPrice = $state('');
|
2026-06-11 16:25:27 +07:00
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
let selectedCountryKey = $derived(selectedCountry.toLowerCase());
|
2026-06-11 16:25:27 +07:00
|
|
|
let selectedCountryLanguage = $derived(getCountryPrimaryLanguage(selectedCountry));
|
2026-06-16 11:30:23 +07:00
|
|
|
let backendSlots = $derived($priceSlots[selectedCountryKey] ?? []);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
let namespaceSlots = $derived($priceSlotNamespaces[selectedCountryKey] ?? []);
|
|
|
|
|
let displaySlots = $derived(
|
|
|
|
|
mergeDisplaySlots(namespaceSlots, backendSlots, localSlots).sort((a, b) => a.slot - b.slot)
|
|
|
|
|
);
|
2026-06-16 11:30:23 +07:00
|
|
|
let selectedSourceSlot = $derived(
|
|
|
|
|
displaySlots.find((slot) => slot.slot === selectedSlot) ?? displaySlots[0] ?? null
|
|
|
|
|
);
|
|
|
|
|
let currentSlot = $derived(workingSlot ?? emptySlot);
|
|
|
|
|
let isServiceSlot = $derived(currentSlot.kind === 'service');
|
|
|
|
|
let serviceHeaders = $derived(currentSlot.header?.filter(Boolean) ?? []);
|
|
|
|
|
let loading = $derived($priceSlotsLoading);
|
2026-06-11 16:25:27 +07:00
|
|
|
let basePriceCells = $derived(
|
|
|
|
|
$lastRequestSheetPrice[selectedCountry.toLowerCase()] ||
|
|
|
|
|
$lastRequestSheetPrice[selectedCountry] ||
|
|
|
|
|
{}
|
|
|
|
|
);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
let basePriceRows = $derived(
|
|
|
|
|
$sheetPriceAllRows[selectedCountry.toLowerCase()] || $sheetPriceAllRows[selectedCountry] || {}
|
|
|
|
|
);
|
|
|
|
|
let basePriceRowCount = $derived(
|
|
|
|
|
Object.values(basePriceRows).reduce((total, rows) => total + rows.length, 0)
|
|
|
|
|
);
|
|
|
|
|
let basePriceHeader = $derived(
|
|
|
|
|
$sheetPriceHeader[selectedCountry.toLowerCase()] || $sheetPriceHeader[selectedCountry] || []
|
|
|
|
|
);
|
|
|
|
|
let createPriceBaseLoading = $derived($sheetPriceLoading);
|
2026-06-11 16:25:27 +07:00
|
|
|
let filteredProducts = $derived(
|
|
|
|
|
currentSlot.products.filter((product) => {
|
|
|
|
|
const keyword = productCodeSearch.trim().toLowerCase();
|
|
|
|
|
if (!keyword) return true;
|
|
|
|
|
|
|
|
|
|
return product.product_code.toLowerCase().includes(keyword);
|
|
|
|
|
})
|
|
|
|
|
);
|
2026-06-16 11:30:23 +07:00
|
|
|
let filteredServiceRows = $derived(
|
|
|
|
|
(currentSlot.serviceRows ?? []).filter((row) => {
|
|
|
|
|
const keyword = productCodeSearch.trim().toLowerCase();
|
|
|
|
|
if (!keyword) return true;
|
|
|
|
|
|
|
|
|
|
return Object.values(row.cells).some((value) => value.toLowerCase().includes(keyword));
|
|
|
|
|
})
|
|
|
|
|
);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
let productPageCount = $derived(Math.max(1, Math.ceil(filteredProducts.length / rowsPerPage)));
|
|
|
|
|
let servicePageCount = $derived(Math.max(1, Math.ceil(filteredServiceRows.length / rowsPerPage)));
|
|
|
|
|
let pagedProducts = $derived(
|
|
|
|
|
filteredProducts.slice((productPage - 1) * rowsPerPage, productPage * rowsPerPage)
|
|
|
|
|
);
|
|
|
|
|
let pagedServiceRows = $derived(
|
|
|
|
|
filteredServiceRows.slice((servicePage - 1) * rowsPerPage, servicePage * rowsPerPage)
|
|
|
|
|
);
|
2026-06-16 11:30:23 +07:00
|
|
|
let changedCount = $derived(countChangedProducts(currentSlot, savedSlot ?? undefined));
|
|
|
|
|
let changedServiceCount = $derived(countChangedServiceRows(currentSlot, savedSlot ?? undefined));
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
let addedProductCount = $derived(countAddedProducts(currentSlot, savedSlot ?? undefined));
|
|
|
|
|
let deletedProductCount = $derived(deletedProductRowIds.length);
|
|
|
|
|
let totalChangedCount = $derived(
|
|
|
|
|
changedCount + changedServiceCount + addedProductCount + deletedProductCount
|
|
|
|
|
);
|
|
|
|
|
let isLocalSlot = $derived(localSlots.some((slot) => slot.slot === currentSlot.slot));
|
2026-06-16 11:30:23 +07:00
|
|
|
let visibleRowCount = $derived(
|
|
|
|
|
isServiceSlot ? filteredServiceRows.length : filteredProducts.length
|
|
|
|
|
);
|
|
|
|
|
let totalRowCount = $derived(
|
|
|
|
|
isServiceSlot ? (currentSlot.serviceRows?.length ?? 0) : currentSlot.products.length
|
|
|
|
|
);
|
2026-06-11 16:25:27 +07:00
|
|
|
let hasHeaderChanges = $derived(
|
2026-06-16 11:30:23 +07:00
|
|
|
currentSlot.name !== savedSlot?.name || currentSlot.description !== savedSlot?.description
|
|
|
|
|
);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
let hasChanges = $derived(totalChangedCount > 0 || hasHeaderChanges || isLocalSlot);
|
2026-06-16 11:30:23 +07:00
|
|
|
let resetButtonTitle = $derived(
|
|
|
|
|
!currentSlot.slot
|
|
|
|
|
? 'Select a PriceSlot first'
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
: !savedSlot
|
|
|
|
|
? 'This PriceSlot has not been saved yet'
|
|
|
|
|
: !hasChanges
|
|
|
|
|
? 'Reset is available after changing this PriceSlot'
|
|
|
|
|
: 'Discard unsaved changes and restore the last loaded values'
|
2026-06-11 16:25:27 +07:00
|
|
|
);
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
function getEnabledSheetCountries() {
|
|
|
|
|
return get(currentPerms)
|
|
|
|
|
.filter((x) => x.startsWith('document.write'))
|
|
|
|
|
.map((x) => x.split('.')[2])
|
|
|
|
|
.filter((country) => supportedCountries.has(country));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:25:27 +07:00
|
|
|
onMount(() => {
|
|
|
|
|
referenceFromPage.set('priceslot');
|
|
|
|
|
|
|
|
|
|
if (selectedCountry) {
|
|
|
|
|
departmentStore.set(selectedCountry);
|
|
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
enabledCountries = getEnabledSheetCountries();
|
2026-06-16 11:30:23 +07:00
|
|
|
|
|
|
|
|
if (selectedCountry) {
|
|
|
|
|
void loadPriceSlots();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let lastLoadedSlotSignature = $state('');
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (displaySlots.length === 0) {
|
|
|
|
|
workingSlot = null;
|
|
|
|
|
savedSlot = null;
|
|
|
|
|
lastLoadedSlotSignature = '';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!displaySlots.some((slot) => slot.slot === selectedSlot)) {
|
|
|
|
|
selectedSlot = displaySlots[0]?.slot ?? 0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!selectedSourceSlot) return;
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
const signature = getSlotSignature(selectedSourceSlot);
|
2026-06-16 11:30:23 +07:00
|
|
|
if (signature === lastLoadedSlotSignature) return;
|
|
|
|
|
if (hasChanges && workingSlot?.slot === selectedSourceSlot.slot) return;
|
|
|
|
|
|
|
|
|
|
lastLoadedSlotSignature = signature;
|
|
|
|
|
workingSlot = clonePriceSlot(selectedSourceSlot);
|
|
|
|
|
savedSlot = clonePriceSlot(selectedSourceSlot);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
deletedProductRowIds = [];
|
2026-06-16 11:30:23 +07:00
|
|
|
productCodeSearch = '';
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
productPage = 1;
|
|
|
|
|
servicePage = 1;
|
|
|
|
|
void ensureSelectedSlotLoaded(selectedSourceSlot);
|
2026-06-11 16:25:27 +07:00
|
|
|
});
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
$effect(() => {
|
|
|
|
|
if (!pendingCreateDialog) return;
|
|
|
|
|
|
|
|
|
|
if (basePriceRowCount > 0) {
|
|
|
|
|
pendingCreateDialog = false;
|
|
|
|
|
applyCreateTemplate();
|
|
|
|
|
createDialogOpen = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$sheetPriceLoading) {
|
|
|
|
|
const productCodes = getProductCodesForPriceBase();
|
|
|
|
|
if (productCodes.length > 0) void requestPriceBaseFromProductCodes(productCodes);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$effect(() => {
|
|
|
|
|
if (currentSlot.products.length === 0) return;
|
|
|
|
|
if (requestedPriceCountries.has(selectedCountryKey)) return;
|
|
|
|
|
if (isLocalSlot) return;
|
|
|
|
|
if (Object.keys(basePriceCells).length > 0) return;
|
|
|
|
|
|
|
|
|
|
const productCodes = currentSlot.products
|
|
|
|
|
.filter((p) => p.product_code)
|
|
|
|
|
.map((p) => p.product_code);
|
|
|
|
|
if (productCodes.length === 0) return;
|
|
|
|
|
|
|
|
|
|
void requestPriceBaseFromProductCodes(productCodes);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function getBaseProducts(): (PriceSlotProduct & { sourceCells: GristCell[] })[] {
|
|
|
|
|
return Object.entries(basePriceRows).flatMap(([productCode, rows]) =>
|
|
|
|
|
rows.map((row) => {
|
|
|
|
|
const productName = getCellValue(row.cells, 2);
|
|
|
|
|
const price = getPriceFromCells(selectedCountryKey, row.cells, 'cash_price');
|
|
|
|
|
const parsedPrice = Number(price);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
product_code: productCode,
|
|
|
|
|
name: productName,
|
|
|
|
|
price: Number.isNaN(parsedPrice) ? null : parsedPrice,
|
|
|
|
|
row_index: row.row,
|
|
|
|
|
sourceCells: row.cells
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergeDisplaySlots(
|
|
|
|
|
namespaces: PriceSlot[],
|
|
|
|
|
loadedSlots: PriceSlot[],
|
|
|
|
|
draftSlots: PriceSlot[]
|
|
|
|
|
) {
|
|
|
|
|
const merged = new Map<number, PriceSlot>();
|
|
|
|
|
for (const slot of namespaces) merged.set(slot.slot, slot);
|
|
|
|
|
for (const slot of loadedSlots) merged.set(slot.slot, slot);
|
|
|
|
|
for (const slot of draftSlots) merged.set(slot.slot, slot);
|
|
|
|
|
return Array.from(merged.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSlotSignature(slot: PriceSlot) {
|
|
|
|
|
return [
|
|
|
|
|
slot.slot,
|
|
|
|
|
slot.name,
|
|
|
|
|
slot.description,
|
|
|
|
|
slot.kind ?? '',
|
|
|
|
|
slot.products.length,
|
|
|
|
|
slot.serviceRows?.length ?? 0
|
|
|
|
|
].join('|');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSlotDataLoaded(slot: PriceSlot) {
|
|
|
|
|
return (
|
|
|
|
|
(slot.header?.length ?? 0) > 0 ||
|
|
|
|
|
slot.products.length > 0 ||
|
|
|
|
|
(slot.serviceRows?.length ?? 0) > 0
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureSelectedSlotLoaded(slot: PriceSlot) {
|
|
|
|
|
if (!slot.slot || isLocalSlot || isSlotDataLoaded(slot)) return;
|
|
|
|
|
|
|
|
|
|
const requestKey = `${selectedCountryKey}:${slot.slot}`;
|
|
|
|
|
if (requestedSlotKeys.has(requestKey)) return;
|
|
|
|
|
|
|
|
|
|
requestedSlotKeys.add(requestKey);
|
|
|
|
|
const sent = await requestPriceSlot(selectedCountry, slot.slot);
|
|
|
|
|
if (!sent) {
|
|
|
|
|
requestedSlotKeys.delete(requestKey);
|
|
|
|
|
addNotification(`ERR:Failed to request PriceSlot${slot.slot}`);
|
|
|
|
|
}
|
2026-06-16 11:30:23 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:25:27 +07:00
|
|
|
function getBasePrice(product: PriceSlotProduct): number | null {
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
const productWithSource = product as PriceSlotProduct & { sourceCells?: GristCell[] };
|
|
|
|
|
if (productWithSource.sourceCells?.length) {
|
|
|
|
|
const price = getPriceFromCells(
|
|
|
|
|
selectedCountryKey,
|
|
|
|
|
productWithSource.sourceCells,
|
|
|
|
|
'cash_price'
|
|
|
|
|
);
|
|
|
|
|
const parsed = Number(price);
|
|
|
|
|
if (!Number.isNaN(parsed)) return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:25:27 +07:00
|
|
|
const cells = basePriceCells[product.product_code];
|
|
|
|
|
if (cells?.length > 0) {
|
|
|
|
|
const price = getPriceFromCells(selectedCountry.toLowerCase(), cells, 'cash_price');
|
|
|
|
|
const parsed = Number(price);
|
|
|
|
|
if (!Number.isNaN(parsed)) return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCellValue(cells: GristCell[], col: number) {
|
|
|
|
|
return String(cells.find((cell) => cell.coord?.col === col)?.value ?? '');
|
2026-06-11 16:25:27 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getProductNames(product: PriceSlotProduct) {
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
const priceCells = basePriceCells[product.product_code];
|
|
|
|
|
if (priceCells?.length) {
|
|
|
|
|
const englishName = getCellValue(priceCells, 2);
|
|
|
|
|
const localName = getCellValue(priceCells, 3);
|
|
|
|
|
|
|
|
|
|
if (englishName) {
|
|
|
|
|
return {
|
|
|
|
|
english: englishName,
|
|
|
|
|
local: localName || englishName
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:25:27 +07:00
|
|
|
const [englishName = '', localName = ''] = product.name.split('|').map((name) => name.trim());
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
english: englishName || product.name,
|
|
|
|
|
local: localName || englishName || product.name
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
function clonePriceSlot(slot: PriceSlot): PriceSlot {
|
|
|
|
|
return JSON.parse(JSON.stringify(slot));
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:25:27 +07:00
|
|
|
function calculateAdjustedPrice(basePrice: number | null): number | null {
|
|
|
|
|
if (basePrice === null) return null;
|
|
|
|
|
|
|
|
|
|
const value = Number(adjustmentValue);
|
|
|
|
|
if (Number.isNaN(value)) return basePrice;
|
|
|
|
|
|
|
|
|
|
const nextPrice =
|
|
|
|
|
adjustmentMode === 'increase_percent'
|
|
|
|
|
? basePrice * (1 + value / 100)
|
|
|
|
|
: adjustmentMode === 'increase_amount'
|
|
|
|
|
? basePrice + value
|
|
|
|
|
: adjustmentMode === 'decrease_percent'
|
|
|
|
|
? basePrice * (1 - value / 100)
|
|
|
|
|
: basePrice - value;
|
|
|
|
|
|
|
|
|
|
return Math.max(0, Math.round(nextPrice));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyCreateTemplate() {
|
|
|
|
|
const value = Number(adjustmentValue);
|
|
|
|
|
const formattedValue = Number.isNaN(value) ? 0 : value;
|
|
|
|
|
const isPercent =
|
|
|
|
|
adjustmentMode === 'increase_percent' || adjustmentMode === 'decrease_percent';
|
|
|
|
|
const isIncrease =
|
|
|
|
|
adjustmentMode === 'increase_percent' || adjustmentMode === 'increase_amount';
|
|
|
|
|
const action = isIncrease ? 'Increase' : 'Decrease';
|
|
|
|
|
const valueLabel = isPercent ? `${formattedValue}%` : `${formattedValue}`;
|
|
|
|
|
const nameSuffix = isPercent ? `${formattedValue}` : `Fixed${formattedValue}`;
|
|
|
|
|
|
|
|
|
|
createName = `Profile${action}${nameSuffix}`;
|
|
|
|
|
createDescription = `${isIncrease ? 'increase' : 'decrease'} price ${valueLabel}`;
|
|
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
function getNextPriceSlotNumber() {
|
|
|
|
|
return Math.max(0, ...displaySlots.map((slot) => slot.slot)) + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCreateSlotNumber() {
|
|
|
|
|
const slotNumber = Number(createSlotNumber);
|
|
|
|
|
if (!Number.isInteger(slotNumber) || slotNumber <= 0) {
|
|
|
|
|
addNotification('WARN:PriceSlot number must be a positive integer');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (displaySlots.some((slot) => slot.slot === slotNumber)) {
|
|
|
|
|
addNotification(`WARN:PriceSlot${slotNumber} already exists`);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return slotNumber;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openCreateDialog() {
|
|
|
|
|
createSlotNumber = getNextPriceSlotNumber();
|
|
|
|
|
createName = '';
|
|
|
|
|
createDescription = '';
|
|
|
|
|
|
|
|
|
|
if (basePriceRowCount > 0) {
|
|
|
|
|
createMode = 'from_price';
|
|
|
|
|
applyCreateTemplate();
|
|
|
|
|
createDialogOpen = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createMode = 'blank';
|
2026-06-11 16:25:27 +07:00
|
|
|
createDialogOpen = true;
|
|
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
async function ensurePriceBaseRequested() {
|
|
|
|
|
const productCodes = getProductCodesForPriceBase();
|
|
|
|
|
if (productCodes.length > 0) {
|
|
|
|
|
await requestPriceBaseFromProductCodes(productCodes);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (requestedProductCodeCountries.has(selectedCountryKey)) {
|
|
|
|
|
addNotification('INFO:Waiting for product codes before loading Price data');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestedProductCodeCountries.add(selectedCountryKey);
|
|
|
|
|
const sent = await requestListMenu(selectedCountry);
|
|
|
|
|
if (sent) {
|
|
|
|
|
addNotification('INFO:Requesting product codes before loading Price data');
|
|
|
|
|
} else {
|
|
|
|
|
requestedProductCodeCountries.delete(selectedCountryKey);
|
|
|
|
|
pendingCreateDialog = false;
|
|
|
|
|
addNotification('ERR:Failed to request product codes');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getProductCodesForPriceBase() {
|
|
|
|
|
const productCodes = new Set<string>();
|
|
|
|
|
|
|
|
|
|
if (requestedProductCodeCountries.has(selectedCountryKey)) {
|
|
|
|
|
for (const code of $existingProductCodes) productCodes.add(code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const product of currentSlot.products) {
|
|
|
|
|
if (product.product_code) productCodes.add(product.product_code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (productCodes.size === 0) {
|
|
|
|
|
for (const slot of backendSlots) {
|
|
|
|
|
for (const product of slot.products) {
|
|
|
|
|
if (product.product_code) productCodes.add(product.product_code);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(productCodes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function requestPriceBaseFromProductCodes(productCodes = getProductCodesForPriceBase()) {
|
|
|
|
|
if (requestedPriceCountries.has(selectedCountryKey)) return;
|
|
|
|
|
|
|
|
|
|
if (productCodes.length === 0) return;
|
|
|
|
|
|
|
|
|
|
requestedPriceCountries.add(selectedCountryKey);
|
|
|
|
|
const sent = await requestSheetPrice(selectedCountry, productCodes, true);
|
|
|
|
|
if (sent) {
|
|
|
|
|
addNotification(`INFO:Requesting Price data for ${productCodes.length} product codes`);
|
|
|
|
|
} else {
|
|
|
|
|
requestedPriceCountries.delete(selectedCountryKey);
|
|
|
|
|
pendingCreateDialog = false;
|
|
|
|
|
addNotification('ERR:Failed to request Price data');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:25:27 +07:00
|
|
|
function createPriceSlotFromBase() {
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
const nextSlotNumber = getCreateSlotNumber();
|
|
|
|
|
if (!nextSlotNumber) return;
|
|
|
|
|
|
|
|
|
|
if (createMode === 'blank') {
|
|
|
|
|
const nextSlot: PriceSlot = {
|
|
|
|
|
slot: nextSlotNumber,
|
|
|
|
|
name: createName.trim() || `PriceSlot${nextSlotNumber}`,
|
|
|
|
|
description: createDescription.trim(),
|
|
|
|
|
kind: 'price',
|
|
|
|
|
header: [],
|
|
|
|
|
products: []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
localSlots = [...localSlots, nextSlot];
|
|
|
|
|
selectedSlot = nextSlotNumber;
|
|
|
|
|
workingSlot = clonePriceSlot(nextSlot);
|
|
|
|
|
savedSlot = null;
|
|
|
|
|
lastLoadedSlotSignature = getSlotSignature(nextSlot);
|
|
|
|
|
productPage = 1;
|
|
|
|
|
servicePage = 1;
|
|
|
|
|
createDialogOpen = false;
|
|
|
|
|
addNotification(`INFO:Created blank PriceSlot${nextSlotNumber}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
const baseProducts = getBaseProducts();
|
|
|
|
|
if (baseProducts.length === 0) {
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
addNotification('WARN:Main Price data is not ready');
|
2026-06-16 11:30:23 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const products = baseProducts.map((product) => ({
|
2026-06-11 16:25:27 +07:00
|
|
|
...product,
|
|
|
|
|
price: calculateAdjustedPrice(getBasePrice(product))
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const nextSlot: PriceSlot = {
|
|
|
|
|
slot: nextSlotNumber,
|
|
|
|
|
name: createName.trim() || `PriceSlot${nextSlotNumber}`,
|
|
|
|
|
description: createDescription.trim(),
|
2026-06-16 11:30:23 +07:00
|
|
|
kind: 'price',
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
header: basePriceHeader,
|
2026-06-11 16:25:27 +07:00
|
|
|
products
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
localSlots = [...localSlots, nextSlot];
|
2026-06-11 16:25:27 +07:00
|
|
|
selectedSlot = nextSlotNumber;
|
2026-06-16 11:30:23 +07:00
|
|
|
workingSlot = clonePriceSlot(nextSlot);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
savedSlot = null;
|
|
|
|
|
lastLoadedSlotSignature = getSlotSignature(nextSlot);
|
|
|
|
|
productPage = 1;
|
|
|
|
|
servicePage = 1;
|
2026-06-11 16:25:27 +07:00
|
|
|
createDialogOpen = false;
|
|
|
|
|
addNotification(`INFO:Created PriceSlot${nextSlotNumber} from base prices`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function countChangedProducts(current: PriceSlot, saved: PriceSlot | undefined): number {
|
|
|
|
|
if (!saved) return 0;
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
return current.products.filter((product, index) => {
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
if (!product.row_index) return false;
|
2026-06-16 11:30:23 +07:00
|
|
|
const savedProduct =
|
|
|
|
|
saved.products.find(
|
|
|
|
|
(item) => product.row_index !== undefined && item.row_index === product.row_index
|
|
|
|
|
) ??
|
|
|
|
|
saved.products[index] ??
|
|
|
|
|
saved.products.find((item) => item.product_code === product.product_code);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
return (
|
|
|
|
|
savedProduct?.product_code !== product.product_code || savedProduct?.price !== product.price
|
|
|
|
|
);
|
|
|
|
|
}).length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function countAddedProducts(current: PriceSlot, saved: PriceSlot | undefined): number {
|
|
|
|
|
if (current.kind === 'service') return 0;
|
|
|
|
|
return current.products.filter((product, index) => {
|
|
|
|
|
if (product.row_index) return false;
|
|
|
|
|
const savedProduct = saved?.products[index];
|
|
|
|
|
return (
|
|
|
|
|
!savedProduct ||
|
|
|
|
|
savedProduct.product_code !== product.product_code ||
|
|
|
|
|
savedProduct.price !== product.price
|
|
|
|
|
);
|
2026-06-11 16:25:27 +07:00
|
|
|
}).length;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
function countChangedServiceRows(current: PriceSlot, saved: PriceSlot | undefined): number {
|
|
|
|
|
if (!saved || current.kind !== 'service') return 0;
|
|
|
|
|
|
|
|
|
|
return (current.serviceRows ?? []).filter((row, index) => {
|
|
|
|
|
const savedRow =
|
|
|
|
|
saved.serviceRows?.find((item) => item.row_index === row.row_index) ??
|
|
|
|
|
saved.serviceRows?.[index];
|
|
|
|
|
return JSON.stringify(row.cells) !== JSON.stringify(savedRow?.cells ?? {});
|
|
|
|
|
}).length;
|
|
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
function getChangedPriceRows(current: PriceSlot, saved: PriceSlot | undefined) {
|
|
|
|
|
return current.products
|
|
|
|
|
.map((product, index) => {
|
|
|
|
|
if (!product.row_index) return null;
|
|
|
|
|
|
|
|
|
|
const savedProduct =
|
|
|
|
|
saved?.products.find(
|
|
|
|
|
(item) => product.row_index !== undefined && item.row_index === product.row_index
|
|
|
|
|
) ??
|
|
|
|
|
saved?.products[index] ??
|
|
|
|
|
saved?.products.find((item) => item.product_code === product.product_code);
|
|
|
|
|
|
|
|
|
|
const cells = [];
|
|
|
|
|
if (savedProduct?.product_code !== product.product_code) {
|
|
|
|
|
cells.push({
|
|
|
|
|
value: product.product_code,
|
|
|
|
|
coord: { row: product.row_index, col: 1 }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (savedProduct?.price !== product.price) {
|
|
|
|
|
cells.push({
|
|
|
|
|
value: product.price === null ? '' : String(product.price),
|
|
|
|
|
coord: { row: product.row_index, col: 5 }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cells.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
row_index: product.row_index,
|
|
|
|
|
cells
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter((row): row is NonNullable<typeof row> => row !== null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getAddedPriceSlotRows(slot: PriceSlot) {
|
|
|
|
|
const draftSlot = {
|
|
|
|
|
...slot,
|
|
|
|
|
products: slot.products.filter((product) => !product.row_index)
|
|
|
|
|
};
|
|
|
|
|
return getCreatePriceSlotRows(draftSlot).map(({ cells }) => ({ cells }));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getChangedServiceRows(current: PriceSlot, saved: PriceSlot | undefined) {
|
|
|
|
|
if (current.kind !== 'service') return [];
|
|
|
|
|
|
|
|
|
|
return (current.serviceRows ?? [])
|
|
|
|
|
.map((row, index) => {
|
|
|
|
|
const savedRow =
|
|
|
|
|
saved?.serviceRows?.find((item) => item.row_index === row.row_index) ??
|
|
|
|
|
saved?.serviceRows?.[index];
|
|
|
|
|
if (!row.row_index) return null;
|
|
|
|
|
|
|
|
|
|
const cells = serviceHeaders
|
|
|
|
|
.map((header, headerIndex) => {
|
|
|
|
|
const value = row.cells[header] ?? '';
|
|
|
|
|
if (value === (savedRow?.cells[header] ?? '')) return null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
value,
|
|
|
|
|
coord: { row: row.row_index!, col: headerIndex + 1 }
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter((cell): cell is NonNullable<typeof cell> => cell !== null);
|
|
|
|
|
|
|
|
|
|
if (cells.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
return { row_index: row.row_index, cells };
|
|
|
|
|
})
|
|
|
|
|
.filter((row): row is NonNullable<typeof row> => row !== null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCreatePriceCellIndex() {
|
|
|
|
|
const priceColumnIndex = getPriceColumnIndex(selectedCountryKey, 'cash_price');
|
|
|
|
|
return priceColumnIndex > 0 ? priceColumnIndex - 1 : 4;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCreatePriceSlotHeader(slot: PriceSlot) {
|
|
|
|
|
const header = Array.from({ length: 26 }, () => '');
|
|
|
|
|
const priceCellIndex = getCreatePriceCellIndex();
|
|
|
|
|
|
|
|
|
|
header[0] = 'ProductCode';
|
|
|
|
|
header[1] = 'Name';
|
|
|
|
|
header[priceCellIndex] = basePriceHeader[priceCellIndex] || 'Price';
|
|
|
|
|
header[10] = slot.name;
|
|
|
|
|
header[11] = slot.description;
|
|
|
|
|
|
|
|
|
|
return header;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getCreatePriceSlotRows(slot: PriceSlot) {
|
|
|
|
|
const header = getCreatePriceSlotHeader(slot);
|
|
|
|
|
const priceCellIndex = getCreatePriceCellIndex();
|
|
|
|
|
|
|
|
|
|
return slot.products.map((product, index) => {
|
|
|
|
|
const productWithSource = product as PriceSlotProduct & { sourceCells?: GristCell[] };
|
|
|
|
|
const sourceCells = productWithSource.sourceCells ?? [];
|
|
|
|
|
const cells = Array.from({ length: header.length }, () => '');
|
|
|
|
|
const englishName = getCellValue(sourceCells, 2) || product.name;
|
|
|
|
|
const localName = getCellValue(sourceCells, 3);
|
|
|
|
|
|
|
|
|
|
cells[0] = product.product_code;
|
|
|
|
|
cells[1] = englishName;
|
|
|
|
|
cells[2] = localName;
|
|
|
|
|
cells[priceCellIndex] = product.price === null ? '' : String(product.price);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
header: index === 0 ? header : undefined,
|
|
|
|
|
cells
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-11 16:25:27 +07:00
|
|
|
function updateSlotField(field: 'name' | 'description', value: string) {
|
2026-06-16 11:30:23 +07:00
|
|
|
if (!workingSlot) return;
|
|
|
|
|
workingSlot = { ...workingSlot, [field]: value };
|
2026-06-11 16:25:27 +07:00
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
function updateProductCode(
|
|
|
|
|
targetProduct: PriceSlotProduct,
|
|
|
|
|
fallbackIndex: number,
|
|
|
|
|
value: string
|
|
|
|
|
) {
|
|
|
|
|
if (!workingSlot) return;
|
|
|
|
|
|
|
|
|
|
workingSlot = {
|
|
|
|
|
...workingSlot,
|
|
|
|
|
products: workingSlot.products.map((product, index) => {
|
|
|
|
|
const sameProduct =
|
|
|
|
|
targetProduct.row_index !== undefined
|
|
|
|
|
? product.row_index === targetProduct.row_index
|
|
|
|
|
: product === targetProduct || index === fallbackIndex;
|
|
|
|
|
|
|
|
|
|
return sameProduct ? { ...product, product_code: value } : product;
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateProductPrice(
|
|
|
|
|
targetProduct: PriceSlotProduct,
|
|
|
|
|
fallbackIndex: number,
|
|
|
|
|
value: string
|
|
|
|
|
) {
|
2026-06-11 16:25:27 +07:00
|
|
|
const price = value === '' ? null : Number(value);
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
if (!workingSlot) return;
|
|
|
|
|
|
|
|
|
|
workingSlot = {
|
|
|
|
|
...workingSlot,
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
products: workingSlot.products.map((product, index) => {
|
|
|
|
|
const sameProduct =
|
|
|
|
|
targetProduct.row_index !== undefined
|
|
|
|
|
? product.row_index === targetProduct.row_index
|
|
|
|
|
: product === targetProduct || index === fallbackIndex;
|
|
|
|
|
|
|
|
|
|
return sameProduct
|
2026-06-16 11:30:23 +07:00
|
|
|
? { ...product, price: Number.isNaN(price) ? product.price : price }
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
: product;
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openAddDialog() {
|
|
|
|
|
if (!workingSlot || workingSlot.kind === 'service') return;
|
|
|
|
|
addProductCode = '';
|
|
|
|
|
addProductName = '';
|
|
|
|
|
addProductPrice = '';
|
|
|
|
|
addDialogOpen = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function confirmAddProductRow() {
|
|
|
|
|
if (!workingSlot || workingSlot.kind === 'service') return;
|
|
|
|
|
|
|
|
|
|
const code = addProductCode.trim();
|
|
|
|
|
if (!code) {
|
|
|
|
|
addNotification('WARN:Product code is required');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const price = addProductPrice === '' ? null : Number(addProductPrice);
|
|
|
|
|
|
|
|
|
|
workingSlot = {
|
|
|
|
|
...workingSlot,
|
|
|
|
|
products: [
|
|
|
|
|
...workingSlot.products,
|
|
|
|
|
{
|
|
|
|
|
product_code: code,
|
|
|
|
|
name: addProductName.trim(),
|
|
|
|
|
price: Number.isNaN(price) ? null : price
|
|
|
|
|
}
|
|
|
|
|
]
|
2026-06-16 11:30:23 +07:00
|
|
|
};
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
|
|
|
|
|
productPage = Math.max(1, Math.ceil(workingSlot.products.length / rowsPerPage));
|
|
|
|
|
addDialogOpen = false;
|
|
|
|
|
addNotification(`INFO:Added product code ${code}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteProductRow(targetProduct: PriceSlotProduct, fallbackIndex: number) {
|
|
|
|
|
if (!workingSlot || workingSlot.kind === 'service') return;
|
|
|
|
|
|
|
|
|
|
if (targetProduct.row_index) {
|
|
|
|
|
deletedProductRowIds = Array.from(
|
|
|
|
|
new Set([...deletedProductRowIds, targetProduct.row_index])
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
workingSlot = {
|
|
|
|
|
...workingSlot,
|
|
|
|
|
products: workingSlot.products.filter((product, index) => {
|
|
|
|
|
const sameProduct =
|
|
|
|
|
targetProduct.row_index !== undefined
|
|
|
|
|
? product.row_index === targetProduct.row_index
|
|
|
|
|
: product === targetProduct || index === fallbackIndex;
|
|
|
|
|
return !sameProduct;
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
productPage = Math.min(
|
|
|
|
|
productPage,
|
|
|
|
|
Math.max(1, Math.ceil(workingSlot.products.length / rowsPerPage))
|
|
|
|
|
);
|
2026-06-16 11:30:23 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateServiceCell(
|
|
|
|
|
row: PriceSlotServiceRow,
|
|
|
|
|
fallbackIndex: number,
|
|
|
|
|
columnName: string,
|
|
|
|
|
value: string
|
|
|
|
|
) {
|
|
|
|
|
if (!workingSlot) return;
|
|
|
|
|
|
|
|
|
|
workingSlot = {
|
|
|
|
|
...workingSlot,
|
|
|
|
|
serviceRows: (workingSlot.serviceRows ?? []).map((serviceRow, index) => {
|
|
|
|
|
const sameRow =
|
|
|
|
|
row.row_index !== undefined
|
|
|
|
|
? serviceRow.row_index === row.row_index
|
|
|
|
|
: serviceRow === row || index === fallbackIndex;
|
|
|
|
|
return sameRow
|
|
|
|
|
? {
|
|
|
|
|
...serviceRow,
|
|
|
|
|
cells: {
|
|
|
|
|
...serviceRow.cells,
|
|
|
|
|
[columnName]: value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
: serviceRow;
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getServiceColumnClass(columnName: string) {
|
|
|
|
|
const normalized = columnName.toLowerCase();
|
|
|
|
|
if (normalized === 'value') return 'min-w-[320px]';
|
|
|
|
|
if (normalized === 'desc') return 'min-w-[300px]';
|
|
|
|
|
if (normalized === 'l') return 'min-w-[360px]';
|
|
|
|
|
if (normalized.includes('schedule')) return 'min-w-[300px]';
|
|
|
|
|
if (normalized.includes('discount')) return 'min-w-[180px]';
|
|
|
|
|
if (normalized === 'type/key') return 'min-w-[150px]';
|
|
|
|
|
if (normalized === 'servicetype') return 'min-w-[130px]';
|
|
|
|
|
if (normalized === 'daytype') return 'min-w-[130px]';
|
|
|
|
|
if (normalized === 'command') return 'min-w-[130px]';
|
|
|
|
|
if (normalized === 'extendvalue') return 'min-w-[130px]';
|
|
|
|
|
if (normalized === 'time(24 hr)' || normalized === 'time( 24 hr)') return 'min-w-[130px]';
|
|
|
|
|
if (['year', 'month', 'day'].includes(normalized)) return 'min-w-[110px]';
|
|
|
|
|
return 'min-w-[120px]';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getServiceInputClass(columnName: string) {
|
|
|
|
|
const normalized = columnName.toLowerCase();
|
|
|
|
|
const textClass = normalized === 'l' || normalized.includes('schedule') ? '' : 'font-mono';
|
|
|
|
|
return ['h-10 w-full min-w-0 text-sm', textClass].filter(Boolean).join(' ');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getServiceTableMinWidth() {
|
|
|
|
|
return serviceHeaders.reduce((total, header) => {
|
|
|
|
|
const normalized = header.toLowerCase();
|
|
|
|
|
if (normalized === 'value') return total + 320;
|
|
|
|
|
if (normalized === 'desc') return total + 300;
|
|
|
|
|
if (normalized === 'l') return total + 360;
|
|
|
|
|
if (normalized.includes('schedule')) return total + 300;
|
|
|
|
|
if (normalized.includes('discount')) return total + 180;
|
|
|
|
|
if (['type/key', 'servicetype', 'daytype', 'command', 'extendvalue'].includes(normalized)) {
|
|
|
|
|
return total + 140;
|
|
|
|
|
}
|
|
|
|
|
return total + 120;
|
|
|
|
|
}, 0);
|
2026-06-11 16:25:27 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetSlot() {
|
|
|
|
|
if (!savedSlot) return;
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
workingSlot = clonePriceSlot(savedSlot);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
deletedProductRowIds = [];
|
2026-06-11 16:25:27 +07:00
|
|
|
addNotification(`INFO:Reset PriceSlot${selectedSlot}`);
|
|
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
async function saveSlot() {
|
2026-06-16 11:30:23 +07:00
|
|
|
if (!currentSlot.slot) {
|
|
|
|
|
addNotification('WARN:No PriceSlot selected');
|
|
|
|
|
return;
|
|
|
|
|
}
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
if (saveLoading) return;
|
|
|
|
|
|
|
|
|
|
const content =
|
|
|
|
|
currentSlot.kind === 'service'
|
|
|
|
|
? getChangedServiceRows(currentSlot, savedSlot ?? undefined)
|
|
|
|
|
: getChangedPriceRows(currentSlot, savedSlot ?? undefined);
|
|
|
|
|
const createContent =
|
|
|
|
|
isLocalSlot && currentSlot.kind !== 'service' ? getCreatePriceSlotRows(currentSlot) : [];
|
|
|
|
|
const addContent =
|
|
|
|
|
!isLocalSlot && currentSlot.kind !== 'service' ? getAddedPriceSlotRows(currentSlot) : [];
|
|
|
|
|
const deleteRowIds = currentSlot.kind !== 'service' ? deletedProductRowIds : [];
|
|
|
|
|
if (
|
|
|
|
|
!isLocalSlot &&
|
|
|
|
|
content.length === 0 &&
|
|
|
|
|
addContent.length === 0 &&
|
|
|
|
|
deleteRowIds.length === 0
|
|
|
|
|
) {
|
|
|
|
|
addNotification('WARN:No row changes to save');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (isLocalSlot && createContent.length === 0) {
|
|
|
|
|
addNotification('WARN:No PriceSlot rows to create');
|
2026-06-16 11:30:23 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
saveLoading = true;
|
|
|
|
|
try {
|
|
|
|
|
const slotSnapshot = clonePriceSlot(currentSlot);
|
|
|
|
|
const updateSent =
|
|
|
|
|
isLocalSlot || content.length === 0
|
|
|
|
|
? true
|
|
|
|
|
: await updatePriceSlot(selectedCountry, slotSnapshot, content);
|
|
|
|
|
const addSent = isLocalSlot
|
|
|
|
|
? await addPriceSlot(selectedCountry, slotSnapshot, createContent)
|
|
|
|
|
: await addPriceSlotRows(selectedCountry, slotSnapshot, addContent);
|
|
|
|
|
const deleteSent = await deletePriceSlotRows(selectedCountry, slotSnapshot, deleteRowIds);
|
|
|
|
|
|
|
|
|
|
if (!updateSent || !addSent || !deleteSent) {
|
|
|
|
|
addNotification('ERR:Failed to send PriceSlot update');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const savedSlotNumber = selectedSlot;
|
|
|
|
|
savedSlot = clonePriceSlot(currentSlot);
|
|
|
|
|
deletedProductRowIds = [];
|
|
|
|
|
localSlots = localSlots.filter((slot) => slot.slot !== savedSlotNumber);
|
|
|
|
|
requestedSlotKeys.delete(`${selectedCountryKey}:${savedSlotNumber}`);
|
|
|
|
|
selectedSlot = savedSlotNumber;
|
|
|
|
|
addNotification(`INFO:PriceSlot${savedSlotNumber} update sent`);
|
|
|
|
|
|
|
|
|
|
const refreshed = await requestPriceSlot(selectedCountry, savedSlotNumber);
|
|
|
|
|
if (!refreshed) {
|
|
|
|
|
addNotification(`WARN:PriceSlot${savedSlotNumber} saved but refresh failed`);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[PriceSlot] Failed to save PriceSlot', error);
|
|
|
|
|
addNotification('ERR:Failed to send PriceSlot update');
|
|
|
|
|
} finally {
|
|
|
|
|
saveLoading = false;
|
|
|
|
|
}
|
2026-06-11 16:25:27 +07:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
async function loadPriceSlots() {
|
|
|
|
|
localSlots = [];
|
|
|
|
|
workingSlot = null;
|
|
|
|
|
savedSlot = null;
|
|
|
|
|
selectedSlot = 0;
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
requestedSlotKeys = new Set<string>();
|
|
|
|
|
pendingCreateDialog = false;
|
|
|
|
|
deletedProductRowIds = [];
|
2026-06-16 11:30:23 +07:00
|
|
|
priceSlotsLoading.set(true);
|
|
|
|
|
priceSlotsError.set(null);
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
const socket = await waitForAuthenticatedSocket();
|
2026-06-16 11:30:23 +07:00
|
|
|
if (!socket) {
|
|
|
|
|
priceSlotsLoading.set(false);
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
addNotification('ERR:WebSocket auth not ready');
|
2026-06-16 11:30:23 +07:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
const sent = await requestPriceSlots(selectedCountry);
|
2026-06-16 11:30:23 +07:00
|
|
|
if (!sent) {
|
|
|
|
|
priceSlotsLoading.set(false);
|
|
|
|
|
addNotification('ERR:Failed to request PriceSlot data');
|
|
|
|
|
}
|
2026-06-11 16:25:27 +07:00
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<div class="min-h-screen bg-background">
|
|
|
|
|
<div class="w-full px-6 py-8 lg:px-8">
|
|
|
|
|
<div class="mb-7 flex flex-wrap items-start justify-between gap-5">
|
|
|
|
|
<div class="min-w-0">
|
|
|
|
|
<h1 class="text-4xl leading-tight font-bold tracking-normal">
|
|
|
|
|
PriceSlot [ {selectedCountry.toUpperCase()} ]
|
|
|
|
|
</h1>
|
|
|
|
|
<p class="mt-4 text-muted-foreground">
|
|
|
|
|
Edit sheet PriceSlot names, descriptions, and product prices.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
|
|
|
{#if enabledCountries.length > 0}
|
|
|
|
|
<Select.Root
|
|
|
|
|
type="single"
|
|
|
|
|
value={selectedCountry}
|
|
|
|
|
onValueChange={(v) => {
|
|
|
|
|
if (v) {
|
|
|
|
|
selectedCountry = v;
|
|
|
|
|
goto(`/sheet/priceslot/${v}`);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Select.Trigger
|
|
|
|
|
class="h-11 w-40 rounded-lg border-border/80 bg-card/70 px-4 font-semibold"
|
|
|
|
|
>
|
|
|
|
|
{selectedCountry.toUpperCase()}
|
|
|
|
|
</Select.Trigger>
|
|
|
|
|
<Select.Content>
|
|
|
|
|
{#each enabledCountries as country}
|
|
|
|
|
<Select.Item value={country}>
|
|
|
|
|
{country.toUpperCase()}
|
|
|
|
|
</Select.Item>
|
|
|
|
|
{/each}
|
|
|
|
|
</Select.Content>
|
|
|
|
|
</Select.Root>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
class="h-11 rounded-lg"
|
|
|
|
|
onclick={loadPriceSlots}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
>
|
|
|
|
|
<RefreshCw class="mr-2 h-4 w-4" />
|
|
|
|
|
Refresh
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
<div class="sticky top-0 z-30 mb-5 bg-background/95 pt-2 pb-1 backdrop-blur">
|
|
|
|
|
<div class="mb-4 flex gap-2 overflow-x-auto border-b">
|
|
|
|
|
{#if displaySlots.length > 0}
|
|
|
|
|
{#each displaySlots as slot}
|
|
|
|
|
<button
|
|
|
|
|
class={[
|
|
|
|
|
'min-w-28 border-b-2 px-4 py-3 text-sm font-semibold transition-colors',
|
|
|
|
|
selectedSlot === slot.slot
|
|
|
|
|
? 'border-emerald-500 text-foreground'
|
|
|
|
|
: 'border-transparent text-muted-foreground hover:text-foreground'
|
|
|
|
|
]}
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
onclick={() => {
|
|
|
|
|
selectedSlot = slot.slot;
|
|
|
|
|
productPage = 1;
|
|
|
|
|
servicePage = 1;
|
|
|
|
|
}}
|
2026-06-16 11:30:23 +07:00
|
|
|
>
|
|
|
|
|
PriceSlot{slot.slot}
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="py-3 text-sm text-muted-foreground">
|
|
|
|
|
{loading ? 'Loading PriceSlot data...' : 'No PriceSlot data loaded'}
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
<div class="rounded-lg border border-border/80 bg-card p-4 shadow-sm">
|
|
|
|
|
<div
|
|
|
|
|
class="grid grid-cols-1 items-end gap-4 xl:grid-cols-[180px_minmax(220px,1fr)_minmax(280px,1.25fr)_auto]"
|
|
|
|
|
>
|
|
|
|
|
<div class="flex min-w-0 flex-col justify-end gap-2 pb-1">
|
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
|
<h2 class="text-xl font-bold tracking-normal">
|
|
|
|
|
{currentSlot.slot ? `PriceSlot${selectedSlot}` : 'No PriceSlot'}
|
|
|
|
|
</h2>
|
|
|
|
|
<Badge variant={hasChanges ? 'default' : 'secondary'}>
|
|
|
|
|
{hasChanges ? `${totalChangedCount} changes` : 'No changes'}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- <p class="text-sm text-muted-foreground">Column K/L</p> -->
|
2026-06-11 16:25:27 +07:00
|
|
|
</div>
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
<label class="text-xs font-medium text-muted-foreground" for="priceslot-name"
|
|
|
|
|
>Name</label
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
id="priceslot-name"
|
|
|
|
|
class="h-10"
|
|
|
|
|
value={currentSlot.name}
|
|
|
|
|
disabled={!currentSlot.slot}
|
|
|
|
|
oninput={(event) => updateSlotField('name', event.currentTarget.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
<div class="space-y-1.5">
|
|
|
|
|
<label class="text-xs font-medium text-muted-foreground" for="priceslot-description">
|
|
|
|
|
Description
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="priceslot-description"
|
|
|
|
|
class="h-10"
|
|
|
|
|
value={currentSlot.description}
|
|
|
|
|
disabled={!currentSlot.slot}
|
|
|
|
|
oninput={(event) => updateSlotField('description', event.currentTarget.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
<div class="flex flex-wrap items-end justify-start gap-2 xl:justify-end">
|
|
|
|
|
<div class="flex flex-col gap-1">
|
|
|
|
|
<div class="flex flex-wrap gap-2 xl:justify-end">
|
|
|
|
|
<Button
|
|
|
|
|
class="h-11 rounded-lg"
|
|
|
|
|
onclick={openCreateDialog}
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
disabled={createPriceBaseLoading}
|
2026-06-16 11:30:23 +07:00
|
|
|
>
|
|
|
|
|
<Calculator class="mr-2 h-4 w-4" />
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
{createPriceBaseLoading ? 'Loading Price...' : 'Create PriceSlot'}
|
2026-06-16 11:30:23 +07:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
class="h-11 rounded-lg px-4"
|
|
|
|
|
onclick={saveSlot}
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
disabled={saveLoading || !hasChanges || !currentSlot.slot}
|
2026-06-16 11:30:23 +07:00
|
|
|
>
|
|
|
|
|
<Save class="mr-2 h-4 w-4" />
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
{saveLoading ? 'Saving...' : 'Save'}
|
2026-06-16 11:30:23 +07:00
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
class="h-11 rounded-lg px-4"
|
|
|
|
|
onclick={resetSlot}
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
disabled={!savedSlot || !hasChanges || !currentSlot.slot}
|
2026-06-16 11:30:23 +07:00
|
|
|
title={resetButtonTitle}
|
|
|
|
|
>
|
|
|
|
|
<RotateCcw class="mr-2 h-4 w-4" />
|
|
|
|
|
Reset
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<p class="text-right text-xs text-muted-foreground">
|
|
|
|
|
Reset discards unsaved changes for the selected PriceSlot.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
<div class={['mx-auto mt-4 w-full', isServiceSlot ? 'max-w-none' : 'max-w-6xl']}>
|
|
|
|
|
<div class="flex flex-wrap items-end justify-between gap-3">
|
|
|
|
|
<div class="w-full max-w-sm space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="product-code-search">
|
|
|
|
|
{isServiceSlot ? 'Search ServiceType' : 'Search ProductCode'}
|
|
|
|
|
</label>
|
|
|
|
|
<div class="relative">
|
|
|
|
|
<Search
|
|
|
|
|
class="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
id="product-code-search"
|
|
|
|
|
class="pl-9 font-mono"
|
|
|
|
|
placeholder={isServiceSlot ? 'Trigger1, Discount, ProductCode' : '12-01-01-0001'}
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
value={productCodeSearch}
|
|
|
|
|
oninput={(event) => {
|
|
|
|
|
productCodeSearch = event.currentTarget.value;
|
|
|
|
|
productPage = 1;
|
|
|
|
|
servicePage = 1;
|
|
|
|
|
}}
|
2026-06-16 11:30:23 +07:00
|
|
|
/>
|
|
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
</div>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<div class="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
{#if !isServiceSlot && currentSlot.slot}
|
|
|
|
|
<Button variant="outline" size="sm" onclick={openAddDialog}>
|
|
|
|
|
<Plus class="mr-2 h-4 w-4" />
|
|
|
|
|
Add ProductCode
|
|
|
|
|
</Button>
|
|
|
|
|
{/if}
|
|
|
|
|
<span>
|
|
|
|
|
Showing {isServiceSlot ? pagedServiceRows.length : pagedProducts.length} of
|
|
|
|
|
{visibleRowCount} filtered / {totalRowCount}
|
|
|
|
|
{isServiceSlot ? 'service rows' : 'products'}
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={isServiceSlot ? servicePage <= 1 : productPage <= 1}
|
|
|
|
|
onclick={() => {
|
|
|
|
|
if (isServiceSlot) servicePage = Math.max(1, servicePage - 1);
|
|
|
|
|
else productPage = Math.max(1, productPage - 1);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Prev
|
|
|
|
|
</Button>
|
|
|
|
|
<span>
|
|
|
|
|
Page {isServiceSlot ? servicePage : productPage} / {isServiceSlot
|
|
|
|
|
? servicePageCount
|
|
|
|
|
: productPageCount}
|
|
|
|
|
</span>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={isServiceSlot
|
|
|
|
|
? servicePage >= servicePageCount
|
|
|
|
|
: productPage >= productPageCount}
|
|
|
|
|
onclick={() => {
|
|
|
|
|
if (isServiceSlot) servicePage = Math.min(servicePageCount, servicePage + 1);
|
|
|
|
|
else productPage = Math.min(productPageCount, productPage + 1);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Next
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-06-16 11:30:23 +07:00
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
|
2026-06-16 11:30:23 +07:00
|
|
|
<div class={['mx-auto w-full', isServiceSlot ? 'max-w-none' : 'max-w-6xl']}>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
{#if loading && displaySlots.length === 0}
|
2026-06-16 11:30:23 +07:00
|
|
|
<div class="flex h-48 items-center justify-center rounded-lg border text-muted-foreground">
|
|
|
|
|
Loading PriceSlot data...
|
|
|
|
|
</div>
|
|
|
|
|
{:else if $priceSlotsError}
|
|
|
|
|
<div class="rounded-lg border border-red-300 bg-red-50 p-4 text-sm text-red-700">
|
|
|
|
|
{$priceSlotsError}
|
|
|
|
|
</div>
|
|
|
|
|
{:else if displaySlots.length === 0}
|
|
|
|
|
<div class="flex h-48 items-center justify-center rounded-lg border text-muted-foreground">
|
|
|
|
|
No PriceSlot data from backend.
|
|
|
|
|
</div>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
{:else if currentSlot.slot && !isServiceSlot && currentSlot.products.length === 0}
|
|
|
|
|
<div class="flex h-48 items-center justify-center rounded-lg border text-muted-foreground">
|
|
|
|
|
PriceSlot{currentSlot.slot} data is not loaded yet.
|
|
|
|
|
</div>
|
|
|
|
|
{:else if currentSlot.slot && isServiceSlot && (currentSlot.serviceRows?.length ?? 0) === 0}
|
|
|
|
|
<div class="flex h-48 items-center justify-center rounded-lg border text-muted-foreground">
|
|
|
|
|
PriceSlot{currentSlot.slot} data is not loaded yet.
|
|
|
|
|
</div>
|
2026-06-16 11:30:23 +07:00
|
|
|
{:else if isServiceSlot}
|
|
|
|
|
<div class="w-full overflow-x-auto rounded-lg border">
|
|
|
|
|
<Table.Root style={`min-width: ${Math.max(1400, getServiceTableMinWidth())}px`}>
|
|
|
|
|
<Table.Header>
|
2026-06-11 16:25:27 +07:00
|
|
|
<Table.Row>
|
2026-06-16 11:30:23 +07:00
|
|
|
{#each serviceHeaders as header}
|
|
|
|
|
<Table.Head class={getServiceColumnClass(header)}>{header}</Table.Head>
|
|
|
|
|
{/each}
|
2026-06-11 16:25:27 +07:00
|
|
|
</Table.Row>
|
2026-06-16 11:30:23 +07:00
|
|
|
</Table.Header>
|
|
|
|
|
<Table.Body>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
{#each pagedServiceRows as row, index (`${row.row_index ?? index}-${index}`)}
|
2026-06-16 11:30:23 +07:00
|
|
|
<Table.Row>
|
|
|
|
|
{#each serviceHeaders as header}
|
|
|
|
|
<Table.Cell class={getServiceColumnClass(header)}>
|
|
|
|
|
<Input
|
|
|
|
|
class={getServiceInputClass(header)}
|
|
|
|
|
value={row.cells[header] ?? ''}
|
|
|
|
|
oninput={(event) =>
|
|
|
|
|
updateServiceCell(row, index, header, event.currentTarget.value)}
|
|
|
|
|
/>
|
|
|
|
|
</Table.Cell>
|
|
|
|
|
{/each}
|
|
|
|
|
</Table.Row>
|
|
|
|
|
{/each}
|
|
|
|
|
{#if filteredServiceRows.length === 0}
|
|
|
|
|
<Table.Row>
|
|
|
|
|
<Table.Cell
|
|
|
|
|
colspan={Math.max(serviceHeaders.length, 1)}
|
|
|
|
|
class="h-28 text-center text-muted-foreground"
|
|
|
|
|
>
|
|
|
|
|
No service rows found.
|
|
|
|
|
</Table.Cell>
|
|
|
|
|
</Table.Row>
|
|
|
|
|
{/if}
|
|
|
|
|
</Table.Body>
|
|
|
|
|
</Table.Root>
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div class="w-full overflow-hidden rounded-lg border">
|
|
|
|
|
<Table.Root>
|
|
|
|
|
<Table.Header>
|
2026-06-11 16:25:27 +07:00
|
|
|
<Table.Row>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<Table.Head class="w-[230px]">ProductCode</Table.Head>
|
2026-06-16 11:30:23 +07:00
|
|
|
<Table.Head>ProductName [{selectedCountryLanguage}]</Table.Head>
|
|
|
|
|
<Table.Head>ProductNameEng</Table.Head>
|
|
|
|
|
<Table.Head class="w-[150px] text-right">Price</Table.Head>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<Table.Head class="w-[90px]"></Table.Head>
|
2026-06-11 16:25:27 +07:00
|
|
|
</Table.Row>
|
2026-06-16 11:30:23 +07:00
|
|
|
</Table.Header>
|
|
|
|
|
<Table.Body>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
{#each pagedProducts as product, index (`${product.product_code}-${product.row_index ?? 'new'}-${index}`)}
|
2026-06-16 11:30:23 +07:00
|
|
|
{@const productNames = getProductNames(product)}
|
|
|
|
|
<Table.Row>
|
|
|
|
|
<Table.Cell class="font-mono text-sm font-semibold">
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<Input
|
|
|
|
|
class="font-mono text-sm font-semibold"
|
|
|
|
|
value={product.product_code}
|
|
|
|
|
oninput={(event) =>
|
|
|
|
|
updateProductCode(product, index, event.currentTarget.value)}
|
|
|
|
|
/>
|
2026-06-16 11:30:23 +07:00
|
|
|
</Table.Cell>
|
|
|
|
|
<Table.Cell class="min-w-64 font-medium">{productNames.local}</Table.Cell>
|
|
|
|
|
<Table.Cell class="min-w-64 text-muted-foreground"
|
|
|
|
|
>{productNames.english}</Table.Cell
|
|
|
|
|
>
|
|
|
|
|
<Table.Cell>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
step="1"
|
|
|
|
|
class="ml-auto w-32 text-right font-semibold"
|
|
|
|
|
value={product.price ?? ''}
|
|
|
|
|
oninput={(event) =>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
updateProductPrice(product, index, event.currentTarget.value)}
|
2026-06-16 11:30:23 +07:00
|
|
|
/>
|
|
|
|
|
</Table.Cell>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<Table.Cell class="text-right">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
class="text-destructive hover:text-destructive"
|
|
|
|
|
onclick={() => deleteProductRow(product, index)}
|
|
|
|
|
aria-label={`Delete ${product.product_code || 'product row'}`}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 class="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</Table.Cell>
|
2026-06-16 11:30:23 +07:00
|
|
|
</Table.Row>
|
|
|
|
|
{/each}
|
|
|
|
|
{#if filteredProducts.length === 0}
|
|
|
|
|
<Table.Row>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<Table.Cell colspan={5} class="h-28 text-center text-muted-foreground">
|
2026-06-16 11:30:23 +07:00
|
|
|
No product code found.
|
|
|
|
|
</Table.Cell>
|
|
|
|
|
</Table.Row>
|
|
|
|
|
{/if}
|
|
|
|
|
</Table.Body>
|
|
|
|
|
</Table.Root>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
2026-06-11 16:25:27 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Dialog.Root bind:open={createDialogOpen}>
|
|
|
|
|
<Dialog.Content class="sm:max-w-2xl">
|
|
|
|
|
<Dialog.Header>
|
|
|
|
|
<Dialog.Title>Create PriceSlot</Dialog.Title>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<Dialog.Description>Choose how to create a new PriceSlot.</Dialog.Description>
|
2026-06-11 16:25:27 +07:00
|
|
|
</Dialog.Header>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-5">
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="create-slot-number">PriceSlot Number</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="create-slot-number"
|
|
|
|
|
type="number"
|
|
|
|
|
min="1"
|
|
|
|
|
step="1"
|
|
|
|
|
value={createSlotNumber}
|
|
|
|
|
oninput={(event) => (createSlotNumber = Number(event.currentTarget.value))}
|
|
|
|
|
/>
|
|
|
|
|
<p class="text-xs text-muted-foreground">
|
|
|
|
|
This creates the Google Sheet/Grist namespace as PriceSlot{createSlotNumber || ''}.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="create-mode">Create Mode</label>
|
|
|
|
|
<Select.Root
|
|
|
|
|
type="single"
|
|
|
|
|
value={createMode}
|
|
|
|
|
onValueChange={(v) => {
|
|
|
|
|
if (v === 'from_price' || v === 'blank') {
|
|
|
|
|
createMode = v;
|
|
|
|
|
if (v === 'from_price') applyCreateTemplate();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Select.Trigger id="create-mode" class="h-10 rounded-lg">
|
|
|
|
|
{createMode === 'from_price'
|
|
|
|
|
? 'From base Price with adjustment'
|
|
|
|
|
: 'Blank (add manually)'}
|
|
|
|
|
</Select.Trigger>
|
|
|
|
|
<Select.Content>
|
|
|
|
|
<Select.Item value="from_price">From base Price with adjustment</Select.Item>
|
|
|
|
|
<Select.Item value="blank">Blank (add manually)</Select.Item>
|
|
|
|
|
</Select.Content>
|
|
|
|
|
</Select.Root>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if createMode === 'from_price'}
|
|
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="adjustment-mode">Adjustment Mode</label>
|
|
|
|
|
<Select.Root
|
|
|
|
|
type="single"
|
|
|
|
|
value={adjustmentMode}
|
|
|
|
|
onValueChange={(v) => {
|
|
|
|
|
if (v) adjustmentMode = v as AdjustmentMode;
|
|
|
|
|
applyCreateTemplate();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Select.Trigger id="adjustment-mode" class="h-10 rounded-lg">
|
|
|
|
|
{adjustmentModeLabels[adjustmentMode]}
|
|
|
|
|
</Select.Trigger>
|
|
|
|
|
<Select.Content>
|
|
|
|
|
<Select.Item value="increase_percent">Increase by Percentage (%)</Select.Item>
|
|
|
|
|
<Select.Item value="increase_amount">Increase by Fixed Amount</Select.Item>
|
|
|
|
|
<Select.Item value="decrease_percent">Decrease by Percentage (%)</Select.Item>
|
|
|
|
|
<Select.Item value="decrease_amount">Decrease by Fixed Amount</Select.Item>
|
|
|
|
|
</Select.Content>
|
|
|
|
|
</Select.Root>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="adjustment-value">
|
|
|
|
|
Adjustment Value
|
|
|
|
|
{adjustmentMode === 'increase_percent' || adjustmentMode === 'decrease_percent'
|
|
|
|
|
? '(%)'
|
|
|
|
|
: ''}
|
|
|
|
|
</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="adjustment-value"
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
step="1"
|
|
|
|
|
value={adjustmentValue}
|
|
|
|
|
oninput={(event) => {
|
|
|
|
|
adjustmentValue = Number(event.currentTarget.value);
|
|
|
|
|
applyCreateTemplate();
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-06-11 16:25:27 +07:00
|
|
|
</div>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
{/if}
|
2026-06-11 16:25:27 +07:00
|
|
|
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
2026-06-11 16:25:27 +07:00
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="create-name">Name</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="create-name"
|
|
|
|
|
value={createName}
|
|
|
|
|
oninput={(event) => (createName = event.currentTarget.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="create-description">Description</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="create-description"
|
|
|
|
|
value={createDescription}
|
|
|
|
|
oninput={(event) => (createDescription = event.currentTarget.value)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Dialog.Footer>
|
|
|
|
|
<Button variant="outline" onclick={() => (createDialogOpen = false)}>Cancel</Button>
|
|
|
|
|
<Button onclick={createPriceSlotFromBase}>
|
|
|
|
|
<Calculator class="mr-2 h-4 w-4" />
|
|
|
|
|
Confirm Create
|
|
|
|
|
</Button>
|
|
|
|
|
</Dialog.Footer>
|
|
|
|
|
</Dialog.Content>
|
|
|
|
|
</Dialog.Root>
|
feat: main & brewing video tool, catalog APIs, sheet/recipe updates
- Add /tools/video-mainpage page (main + brewing-page advertisement videos,
date-gated, per-country, push to machine over ADB) + api/video-mainpage
create/list/update proxies; sidebar entry "Main & Brewing Video"
- Add catalog API proxies (catalog-create, catalog-list, catalog-banner,
catalog-banner-image)
- Sheet: overview/edit/add/priceslot/price updates, stores & services
- Misc: adb, websocket/message handlers, crypto, recipe & brew tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:38:59 +07:00
|
|
|
|
|
|
|
|
<Dialog.Root bind:open={addDialogOpen}>
|
|
|
|
|
<Dialog.Content class="sm:max-w-md">
|
|
|
|
|
<Dialog.Header>
|
|
|
|
|
<Dialog.Title>Add ProductCode</Dialog.Title>
|
|
|
|
|
<Dialog.Description>
|
|
|
|
|
Enter the product code, name, and price for the new row.
|
|
|
|
|
</Dialog.Description>
|
|
|
|
|
</Dialog.Header>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-4 py-2">
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="add-product-code">Product Code</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="add-product-code"
|
|
|
|
|
class="font-mono"
|
|
|
|
|
placeholder="12-01-01-0001"
|
|
|
|
|
bind:value={addProductCode}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="add-product-name">Product Name</label>
|
|
|
|
|
<Input id="add-product-name" placeholder="Product name" bind:value={addProductName} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="space-y-2">
|
|
|
|
|
<label class="text-sm font-medium" for="add-product-price">Price</label>
|
|
|
|
|
<Input
|
|
|
|
|
id="add-product-price"
|
|
|
|
|
type="number"
|
|
|
|
|
min="0"
|
|
|
|
|
step="1"
|
|
|
|
|
placeholder="0"
|
|
|
|
|
bind:value={addProductPrice}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Dialog.Footer>
|
|
|
|
|
<Button variant="outline" onclick={() => (addDialogOpen = false)}>Cancel</Button>
|
|
|
|
|
<Button onclick={confirmAddProductRow}>
|
|
|
|
|
<Plus class="mr-2 h-4 w-4" />
|
|
|
|
|
Add
|
|
|
|
|
</Button>
|
|
|
|
|
</Dialog.Footer>
|
|
|
|
|
</Dialog.Content>
|
|
|
|
|
</Dialog.Root>
|