Supra_App/src/routes/(authed)/sheet/priceslot/[country]/+page.svelte

782 lines
25 KiB
Svelte
Raw Normal View History

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 {
getCountryPrimaryLanguage,
getPriceFromCells,
lastRequestSheetPrice,
2026-06-16 11:30:23 +07:00
priceSlots,
priceSlotsError,
priceSlotsLoading,
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';
2026-06-16 11:30:23 +07:00
import { requestPriceSlots, updatePriceSlot } from '$lib/core/services/sheetService.js';
2026-06-11 16:25:27 +07:00
import { waitForOpenSocket } from '$lib/core/stores/websocketStore.js';
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';
import { Calculator, RefreshCw, Save, RotateCcw, Search } from '@lucide/svelte/icons';
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: []
};
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);
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);
let createName = $state('ProfileIncrease15');
let createDescription = $state('increase price 15%');
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] ?? []);
let displaySlots = $derived([...backendSlots, ...localSlots].sort((a, b) => a.slot - b.slot));
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] ||
{}
);
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));
})
);
let changedCount = $derived(countChangedProducts(currentSlot, savedSlot ?? undefined));
let changedServiceCount = $derived(countChangedServiceRows(currentSlot, savedSlot ?? undefined));
let totalChangedCount = $derived(changedCount + changedServiceCount);
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
);
let hasChanges = $derived(totalChangedCount > 0 || hasHeaderChanges);
let resetButtonTitle = $derived(
!currentSlot.slot
? 'Select a PriceSlot first'
: !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
);
onMount(() => {
referenceFromPage.set('priceslot');
if (selectedCountry) {
departmentStore.set(selectedCountry);
}
const userPerms = get(currentPerms).filter((x) => x.startsWith('document.write'));
enabledCountries = userPerms.map((x) => x.split('.')[2]);
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;
const signature = JSON.stringify(selectedSourceSlot);
if (signature === lastLoadedSlotSignature) return;
if (hasChanges && workingSlot?.slot === selectedSourceSlot.slot) return;
lastLoadedSlotSignature = signature;
workingSlot = clonePriceSlot(selectedSourceSlot);
savedSlot = clonePriceSlot(selectedSourceSlot);
productCodeSearch = '';
2026-06-11 16:25:27 +07:00
});
2026-06-16 11:30:23 +07:00
function getBaseProducts(): PriceSlotProduct[] {
return currentSlot.products;
}
2026-06-11 16:25:27 +07:00
function getBasePrice(product: PriceSlotProduct): number | null {
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;
}
return product.price;
}
function getProductNames(product: PriceSlotProduct) {
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}`;
}
function openCreateDialog() {
applyCreateTemplate();
createDialogOpen = true;
}
function createPriceSlotFromBase() {
2026-06-16 11:30:23 +07:00
const baseProducts = getBaseProducts();
if (baseProducts.length === 0) {
addNotification('WARN:No backend PriceSlot data loaded');
return;
}
const nextSlotNumber = Math.max(0, ...displaySlots.map((slot) => slot.slot)) + 1;
2026-06-11 16:25:27 +07:00
2026-06-16 11:30:23 +07:00
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',
header: currentSlot.header,
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);
savedSlot = clonePriceSlot(nextSlot);
lastLoadedSlotSignature = JSON.stringify(nextSlot);
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) => {
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);
2026-06-11 16:25:27 +07:00
return savedProduct?.price !== product.price;
}).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;
}
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
}
function updateProductPrice(productCode: string, value: string) {
const price = value === '' ? null : Number(value);
2026-06-16 11:30:23 +07:00
if (!workingSlot) return;
workingSlot = {
...workingSlot,
products: workingSlot.products.map((product) =>
product.product_code === productCode
? { ...product, price: Number.isNaN(price) ? product.price : price }
: product
)
};
}
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);
2026-06-11 16:25:27 +07:00
addNotification(`INFO:Reset PriceSlot${selectedSlot}`);
}
function saveSlot() {
2026-06-16 11:30:23 +07:00
if (!currentSlot.slot) {
addNotification('WARN:No PriceSlot selected');
return;
}
const sent = updatePriceSlot(selectedCountry, currentSlot);
if (!sent) {
addNotification('ERR:Failed to send PriceSlot update');
return;
}
savedSlot = clonePriceSlot(currentSlot);
localSlots = localSlots.map((slot) =>
slot.slot === selectedSlot ? clonePriceSlot(currentSlot) : slot
2026-06-11 16:25:27 +07:00
);
2026-06-16 11:30:23 +07:00
addNotification(`INFO:PriceSlot${selectedSlot} update sent`);
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;
priceSlotsLoading.set(true);
priceSlotsError.set(null);
const socket = await waitForOpenSocket();
if (!socket) {
priceSlotsLoading.set(false);
addNotification('ERR:WebSocket not connected');
return;
}
const sent = requestPriceSlots(selectedCountry);
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'
]}
onclick={() => (selectedSlot = slot.slot)}
>
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}
disabled={isServiceSlot || currentSlot.products.length === 0}
>
<Calculator class="mr-2 h-4 w-4" />
Create PriceSlot
</Button>
<Button
class="h-11 rounded-lg px-4"
onclick={saveSlot}
disabled={!hasChanges || !currentSlot.slot}
>
<Save class="mr-2 h-4 w-4" />
Save Draft
</Button>
<Button
variant="outline"
class="h-11 rounded-lg px-4"
onclick={resetSlot}
disabled={!hasChanges || !currentSlot.slot}
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'}
bind:value={productCodeSearch}
/>
</div>
2026-06-11 16:25:27 +07:00
</div>
2026-06-16 11:30:23 +07:00
<p class="text-sm text-muted-foreground">
Showing {visibleRowCount} of {totalRowCount}
{isServiceSlot ? 'service rows' : 'products'}
</p>
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']}>
{#if loading}
<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>
{: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>
{#each filteredServiceRows as row, index (`${row.row_index ?? index}-${index}`)}
<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>
2026-06-16 11:30:23 +07:00
<Table.Head class="w-[190px]">ProductCode</Table.Head>
<Table.Head>ProductName [{selectedCountryLanguage}]</Table.Head>
<Table.Head>ProductNameEng</Table.Head>
<Table.Head class="w-[150px] text-right">Price</Table.Head>
2026-06-11 16:25:27 +07:00
</Table.Row>
2026-06-16 11:30:23 +07:00
</Table.Header>
<Table.Body>
{#each filteredProducts as product, index (`${product.product_code}-${product.row_index ?? index}`)}
{@const productNames = getProductNames(product)}
<Table.Row>
<Table.Cell class="font-mono text-sm font-semibold">
{product.product_code}
</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) =>
updateProductPrice(product.product_code, event.currentTarget.value)}
/>
</Table.Cell>
</Table.Row>
{/each}
{#if filteredProducts.length === 0}
<Table.Row>
<Table.Cell colspan={4} class="h-28 text-center text-muted-foreground">
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>
<Dialog.Description>
Choose how to adjust base prices before creating a new PriceSlot.
</Dialog.Description>
</Dialog.Header>
<div class="space-y-5">
<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>
<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>