update get data priceslot
This commit is contained in:
parent
cd88d5aed9
commit
6a2f4e5945
5 changed files with 912 additions and 337 deletions
|
|
@ -8,15 +8,17 @@
|
|||
import { permission as currentPerms } from '$lib/core/stores/permissions.js';
|
||||
import { referenceFromPage } from '$lib/core/stores/recipeStore.js';
|
||||
import {
|
||||
clearSheetPriceSentTypes,
|
||||
getCountryPrimaryLanguage,
|
||||
getPriceFromCells,
|
||||
lastRequestSheetPrice,
|
||||
sheetPriceLoading,
|
||||
priceSlots,
|
||||
priceSlotsError,
|
||||
priceSlotsLoading,
|
||||
type PriceSlot,
|
||||
type PriceSlotProduct
|
||||
type PriceSlotProduct,
|
||||
type PriceSlotServiceRow
|
||||
} from '$lib/core/stores/sheetStore.js';
|
||||
import { requestSheetPrice } from '$lib/core/services/sheetService.js';
|
||||
import { requestPriceSlots, updatePriceSlot } from '$lib/core/services/sheetService.js';
|
||||
import { waitForOpenSocket } from '$lib/core/stores/websocketStore.js';
|
||||
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
|
|
@ -30,77 +32,31 @@
|
|||
type AdjustmentMode =
|
||||
| 'increase_percent'
|
||||
| 'increase_amount'
|
||||
| 'decrease_amount'
|
||||
| 'decrease_percent';
|
||||
| 'decrease_percent'
|
||||
| 'decrease_amount';
|
||||
|
||||
const adjustmentModeLabels: Record<AdjustmentMode, string> = {
|
||||
increase_percent: 'Increase by Percentage (%)',
|
||||
increase_amount: 'Increase by Fixed Amount',
|
||||
decrease_amount: 'Decrease by Fixed Amount',
|
||||
decrease_percent: 'Decrease by Percentage (%)'
|
||||
decrease_percent: 'Decrease by Percentage (%)',
|
||||
decrease_amount: 'Decrease by Fixed Amount'
|
||||
};
|
||||
|
||||
const mockProducts: PriceSlotProduct[] = [
|
||||
{
|
||||
product_code: '12-01-01-0001',
|
||||
name: 'HOT ESPRESSO | เอสเพรสโซ่ร้อน',
|
||||
price: 30,
|
||||
row_index: 2
|
||||
},
|
||||
{ product_code: '12-01-01-0003', name: 'HOT AMERICANO | กาแฟดำร้อน', price: 35, row_index: 3 },
|
||||
{ product_code: '12-01-01-0004', name: 'HOT LATTE | ลาเต้ร้อน', price: 40, row_index: 5 },
|
||||
{ product_code: '12-01-01-0006', name: 'HOT MOCHA | มอคค่าร้อน', price: 55, row_index: 7 },
|
||||
{
|
||||
product_code: '12-01-02-0001',
|
||||
name: 'Iced AMERICANO | กาแฟดำเย็น',
|
||||
price: 40,
|
||||
row_index: 16
|
||||
},
|
||||
{ product_code: '12-01-02-0002', name: 'ICED LATTE | ลาเต้เย็น', price: 50, row_index: 17 },
|
||||
{ product_code: '12-01-02-0003', name: 'ICED MOCHA | มอคค่าเย็น', price: 60, row_index: 18 },
|
||||
{
|
||||
product_code: '12-02-01-0002',
|
||||
name: 'Hot THAI MILK TEA | ชาไทยร้อน',
|
||||
price: 40,
|
||||
row_index: 27
|
||||
},
|
||||
{
|
||||
product_code: '12-02-01-0004',
|
||||
name: 'Hot MATCHA LATTE | มัทฉะลาเต้ร้อน',
|
||||
price: 50,
|
||||
row_index: 29
|
||||
}
|
||||
];
|
||||
|
||||
function buildMockSlots(): PriceSlot[] {
|
||||
return Array.from({ length: 10 }, (_, index) => {
|
||||
const slot = index + 1;
|
||||
const increase = slot === 1 ? 15 : slot === 2 ? 25 : slot * 5;
|
||||
|
||||
return {
|
||||
slot,
|
||||
name: slot <= 2 ? `ProfileIncrease${increase}` : `PriceSlot${slot}`,
|
||||
description: slot <= 2 ? `increase price ${increase}%` : '',
|
||||
products: mockProducts.map((product) => ({
|
||||
...product,
|
||||
price:
|
||||
product.price === null
|
||||
? null
|
||||
: slot <= 2
|
||||
? Math.ceil((product.price * (1 + increase / 100)) / 5) * 5
|
||||
: product.price
|
||||
}))
|
||||
};
|
||||
});
|
||||
}
|
||||
const emptySlot: PriceSlot = {
|
||||
slot: 0,
|
||||
name: '',
|
||||
description: '',
|
||||
kind: 'price',
|
||||
header: [],
|
||||
products: []
|
||||
};
|
||||
|
||||
let selectedCountry = $state<string>($page.params.country || get(departmentStore) || '');
|
||||
let enabledCountries = $state<string[]>([]);
|
||||
let selectedSlot = $state(1);
|
||||
const initialSlots = buildMockSlots();
|
||||
let slots = $state<PriceSlot[]>(initialSlots);
|
||||
let savedSnapshot = $state<PriceSlot[]>(structuredClone(initialSlots));
|
||||
let loading = $state(false);
|
||||
let selectedSlot = $state(0);
|
||||
let localSlots = $state<PriceSlot[]>([]);
|
||||
let workingSlot = $state<PriceSlot | null>(null);
|
||||
let savedSlot = $state<PriceSlot | null>(null);
|
||||
let productCodeSearch = $state('');
|
||||
let createDialogOpen = $state(false);
|
||||
let adjustmentMode = $state<AdjustmentMode>('increase_percent');
|
||||
|
|
@ -108,17 +64,22 @@
|
|||
let createName = $state('ProfileIncrease15');
|
||||
let createDescription = $state('increase price 15%');
|
||||
|
||||
let currentSlot = $derived(slots.find((slot) => slot.slot === selectedSlot) ?? slots[0]);
|
||||
let selectedCountryKey = $derived(selectedCountry.toLowerCase());
|
||||
let selectedCountryLanguage = $derived(getCountryPrimaryLanguage(selectedCountry));
|
||||
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);
|
||||
let basePriceCells = $derived(
|
||||
$lastRequestSheetPrice[selectedCountry.toLowerCase()] ||
|
||||
$lastRequestSheetPrice[selectedCountry] ||
|
||||
{}
|
||||
);
|
||||
let basePricesLoadedCount = $derived(
|
||||
mockProducts.filter((product) => getBasePrice(product) !== null).length
|
||||
);
|
||||
let basePriceLoading = $derived($sheetPriceLoading);
|
||||
let filteredProducts = $derived(
|
||||
currentSlot.products.filter((product) => {
|
||||
const keyword = productCodeSearch.trim().toLowerCase();
|
||||
|
|
@ -127,12 +88,34 @@
|
|||
return product.product_code.toLowerCase().includes(keyword);
|
||||
})
|
||||
);
|
||||
let changedCount = $derived(countChangedProducts(currentSlot, savedSnapshot[selectedSlot - 1]));
|
||||
let hasHeaderChanges = $derived(
|
||||
currentSlot.name !== savedSnapshot[selectedSlot - 1]?.name ||
|
||||
currentSlot.description !== savedSnapshot[selectedSlot - 1]?.description
|
||||
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
|
||||
);
|
||||
let hasHeaderChanges = $derived(
|
||||
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'
|
||||
);
|
||||
let hasChanges = $derived(changedCount > 0 || hasHeaderChanges);
|
||||
|
||||
onMount(() => {
|
||||
referenceFromPage.set('priceslot');
|
||||
|
|
@ -143,8 +126,43 @@
|
|||
|
||||
const userPerms = get(currentPerms).filter((x) => x.startsWith('document.write'));
|
||||
enabledCountries = userPerms.map((x) => x.split('.')[2]);
|
||||
|
||||
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 = '';
|
||||
});
|
||||
|
||||
function getBaseProducts(): PriceSlotProduct[] {
|
||||
return currentSlot.products;
|
||||
}
|
||||
|
||||
function getBasePrice(product: PriceSlotProduct): number | null {
|
||||
const cells = basePriceCells[product.product_code];
|
||||
if (cells?.length > 0) {
|
||||
|
|
@ -165,6 +183,10 @@
|
|||
};
|
||||
}
|
||||
|
||||
function clonePriceSlot(slot: PriceSlot): PriceSlot {
|
||||
return JSON.parse(JSON.stringify(slot));
|
||||
}
|
||||
|
||||
function calculateAdjustedPrice(basePrice: number | null): number | null {
|
||||
if (basePrice === null) return null;
|
||||
|
||||
|
|
@ -183,23 +205,6 @@
|
|||
return Math.max(0, Math.round(nextPrice));
|
||||
}
|
||||
|
||||
async function loadBasePrices() {
|
||||
const productCodes = mockProducts.map((product) => product.product_code);
|
||||
if (productCodes.length === 0) return;
|
||||
|
||||
const socket = await waitForOpenSocket();
|
||||
if (!socket) {
|
||||
addNotification('WARN:WebSocket not connected. Using local base price sample.');
|
||||
return;
|
||||
}
|
||||
|
||||
clearSheetPriceSentTypes();
|
||||
const sent = requestSheetPrice(selectedCountry, productCodes);
|
||||
if (!sent) {
|
||||
addNotification('ERR:Failed to request base prices');
|
||||
}
|
||||
}
|
||||
|
||||
function applyCreateTemplate() {
|
||||
const value = Number(adjustmentValue);
|
||||
const formattedValue = Number.isNaN(value) ? 0 : value;
|
||||
|
|
@ -221,9 +226,15 @@
|
|||
}
|
||||
|
||||
function createPriceSlotFromBase() {
|
||||
const nextSlotNumber = Math.max(0, ...slots.map((slot) => slot.slot)) + 1;
|
||||
const baseProducts = getBaseProducts();
|
||||
if (baseProducts.length === 0) {
|
||||
addNotification('WARN:No backend PriceSlot data loaded');
|
||||
return;
|
||||
}
|
||||
|
||||
const products = mockProducts.map((product) => ({
|
||||
const nextSlotNumber = Math.max(0, ...displaySlots.map((slot) => slot.slot)) + 1;
|
||||
|
||||
const products = baseProducts.map((product) => ({
|
||||
...product,
|
||||
price: calculateAdjustedPrice(getBasePrice(product))
|
||||
}));
|
||||
|
|
@ -232,12 +243,16 @@
|
|||
slot: nextSlotNumber,
|
||||
name: createName.trim() || `PriceSlot${nextSlotNumber}`,
|
||||
description: createDescription.trim(),
|
||||
kind: 'price',
|
||||
header: currentSlot.header,
|
||||
products
|
||||
};
|
||||
|
||||
slots = [...slots, nextSlot];
|
||||
savedSnapshot = [...savedSnapshot, structuredClone(nextSlot)];
|
||||
localSlots = [...localSlots, nextSlot];
|
||||
selectedSlot = nextSlotNumber;
|
||||
workingSlot = clonePriceSlot(nextSlot);
|
||||
savedSlot = clonePriceSlot(nextSlot);
|
||||
lastLoadedSlotSignature = JSON.stringify(nextSlot);
|
||||
createDialogOpen = false;
|
||||
addNotification(`INFO:Created PriceSlot${nextSlotNumber} from base prices`);
|
||||
}
|
||||
|
|
@ -245,56 +260,160 @@
|
|||
function countChangedProducts(current: PriceSlot, saved: PriceSlot | undefined): number {
|
||||
if (!saved) return 0;
|
||||
|
||||
return current.products.filter((product) => {
|
||||
const savedProduct = saved.products.find(
|
||||
(item) => item.product_code === product.product_code
|
||||
);
|
||||
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);
|
||||
return savedProduct?.price !== product.price;
|
||||
}).length;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function updateSlotField(field: 'name' | 'description', value: string) {
|
||||
slots = slots.map((slot) => (slot.slot === selectedSlot ? { ...slot, [field]: value } : slot));
|
||||
if (!workingSlot) return;
|
||||
workingSlot = { ...workingSlot, [field]: value };
|
||||
}
|
||||
|
||||
function updateProductPrice(productCode: string, value: string) {
|
||||
const price = value === '' ? null : Number(value);
|
||||
|
||||
slots = slots.map((slot) => {
|
||||
if (slot.slot !== selectedSlot) return slot;
|
||||
if (!workingSlot) return;
|
||||
|
||||
return {
|
||||
...slot,
|
||||
products: slot.products.map((product) =>
|
||||
product.product_code === productCode
|
||||
? { ...product, price: Number.isNaN(price) ? product.price : price }
|
||||
: product
|
||||
)
|
||||
};
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
||||
function resetSlot() {
|
||||
const savedSlot = savedSnapshot[selectedSlot - 1];
|
||||
if (!savedSlot) return;
|
||||
|
||||
slots = slots.map((slot) => (slot.slot === selectedSlot ? structuredClone(savedSlot) : slot));
|
||||
workingSlot = clonePriceSlot(savedSlot);
|
||||
addNotification(`INFO:Reset PriceSlot${selectedSlot}`);
|
||||
}
|
||||
|
||||
function saveSlot() {
|
||||
savedSnapshot = savedSnapshot.map((slot) =>
|
||||
slot.slot === selectedSlot ? structuredClone(currentSlot) : slot
|
||||
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
|
||||
);
|
||||
addNotification('WARN:PriceSlot backend is not ready. Changes are kept in this UI only.');
|
||||
addNotification(`INFO:PriceSlot${selectedSlot} update sent`);
|
||||
}
|
||||
|
||||
function loadPriceSlots() {
|
||||
loading = true;
|
||||
setTimeout(() => {
|
||||
loading = false;
|
||||
addNotification('WARN:PriceSlot backend is not ready. Showing UI mock data.');
|
||||
}, 250);
|
||||
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');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -348,144 +467,233 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5 flex gap-2 overflow-x-auto border-b">
|
||||
{#each slots 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)}
|
||||
<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>
|
||||
|
||||
<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]"
|
||||
>
|
||||
PriceSlot{slot.slot}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="mb-5 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">PriceSlot{selectedSlot}</h2>
|
||||
<Badge variant={hasChanges ? 'default' : 'secondary'}>
|
||||
{hasChanges ? `${changedCount} changes` : 'No changes'}
|
||||
</Badge>
|
||||
<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> -->
|
||||
</div>
|
||||
<!-- <p class="text-sm text-muted-foreground">Column K/L</p> -->
|
||||
</div>
|
||||
|
||||
<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}
|
||||
oninput={(event) => updateSlotField('name', event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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}
|
||||
oninput={(event) => updateSlotField('description', event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-end justify-start gap-2 xl:justify-end">
|
||||
<Button class="h-11 rounded-lg" onclick={openCreateDialog}>
|
||||
<Calculator class="mr-2 h-4 w-4" />
|
||||
Create PriceSlot
|
||||
</Button>
|
||||
<Button class="h-11 rounded-lg px-4" onclick={saveSlot} disabled={!hasChanges}>
|
||||
<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}
|
||||
>
|
||||
<RotateCcw class="mr-2 h-4 w-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto w-full max-w-6xl">
|
||||
<div class="mb-3 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">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"
|
||||
/>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground" for="priceslot-name"
|
||||
>Name</label
|
||||
>
|
||||
<Input
|
||||
id="product-code-search"
|
||||
class="pl-9 font-mono"
|
||||
placeholder="12-01-01-0001"
|
||||
bind:value={productCodeSearch}
|
||||
id="priceslot-name"
|
||||
class="h-10"
|
||||
value={currentSlot.name}
|
||||
disabled={!currentSlot.slot}
|
||||
oninput={(event) => updateSlotField('name', event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Showing {filteredProducts.length} of {currentSlot.products.length} products
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-hidden rounded-lg border">
|
||||
<Table.Root>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<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>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each filteredProducts as product (product.product_code)}
|
||||
{@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 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>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Showing {visibleRowCount} of {totalRowCount}
|
||||
{isServiceSlot ? 'service rows' : 'products'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<Table.Row>
|
||||
{#each serviceHeaders as header}
|
||||
<Table.Head class={getServiceColumnClass(header)}>{header}</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
</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>
|
||||
<Table.Row>
|
||||
<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>
|
||||
</Table.Row>
|
||||
</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}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -499,19 +707,6 @@
|
|||
</Dialog.Header>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- <div class="flex items-center justify-between rounded-lg border bg-muted/30 px-4 py-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold">Base prices</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{basePriceLoading ? 'Loading prices from backend' : `${basePricesLoadedCount} products ready`}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onclick={loadBasePrices} disabled={basePriceLoading}>
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Load Base
|
||||
</Button>
|
||||
</div> -->
|
||||
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue