1236 lines
50 KiB
JavaScript
1236 lines
50 KiB
JavaScript
// @ts-nocheck
|
||
const fieldGrid = document.querySelector("#fieldGrid");
|
||
const variantHeader = document.querySelector("#variantHeader");
|
||
const manufacturerSelect = document.querySelector("#manufacturerSelect");
|
||
const manufacturerTrigger = document.querySelector("#manufacturerTrigger");
|
||
const manufacturerMenu = document.querySelector("#manufacturerMenu");
|
||
const manufacturerSearch = document.querySelector("#manufacturerSearch");
|
||
const manufacturerOptions = document.querySelector("#manufacturerOptions");
|
||
const manufacturerValue = document.querySelector("#manufacturerValue");
|
||
const languageSelect = document.querySelector("#languageSelect");
|
||
const recordIdElement = document.querySelector("#recordId");
|
||
const loadProductButton = document.querySelector("#loadProductButton");
|
||
const previousProductButton = document.querySelector("#previousProductButton");
|
||
const nextProductButton = document.querySelector("#nextProductButton");
|
||
const tableBody = document.querySelector("#variantRows");
|
||
const variantTable = document.querySelector(".variant-grid");
|
||
const pictureSlot = document.querySelector("#pictureSlot");
|
||
const scanInput = document.querySelector("#scanInput");
|
||
const suggestedModal = document.querySelector("#suggestedModal");
|
||
const suggestedTitle = document.querySelector("#suggestedTitle");
|
||
const suggestedList = document.querySelector("#suggestedList");
|
||
const suggestedCloseButton = document.querySelector("#suggestedCloseButton");
|
||
const mappingState = document.querySelector("#mappingState");
|
||
const mappingList = document.querySelector("#mappingList");
|
||
const mappingDetails = document.querySelector("#mappingDetails");
|
||
const findSourcesButton = document.querySelector("#findSourcesButton");
|
||
const getPicturesButton = document.querySelector("#getPicturesButton");
|
||
const parserDrawerToggle = document.querySelector("#parserDrawerToggle");
|
||
const parserDrawer = document.querySelector("#parserDrawer");
|
||
const sidebarDrawer = document.querySelector("#sidebarDrawer");
|
||
const sidebarOpenButton = document.querySelector("#sidebarOpenButton");
|
||
const sidebarCloseButton = document.querySelector("#sidebarCloseButton");
|
||
const sidebarOverlay = document.querySelector("#sidebarOverlay");
|
||
const sourceModal = document.querySelector("#sourceModal");
|
||
const sourceTitle = document.querySelector("#sourceTitle");
|
||
const sourceList = document.querySelector("#sourceList");
|
||
const sourceCloseButton = document.querySelector("#sourceCloseButton");
|
||
const settingsButton = document.querySelector("#settingsButton");
|
||
const themeToggle = document.querySelector("#themeToggle");
|
||
const settingsModal = document.querySelector("#settingsModal");
|
||
const settingsCloseButton = document.querySelector("#settingsCloseButton");
|
||
const databaseModeNote = document.querySelector("#databaseModeNote");
|
||
const databaseModeInputs = document.querySelectorAll('input[name="databaseMode"]');
|
||
const permissionInputs = document.querySelectorAll("#allowInsertToggle, #allowUpdateToggle, #allowDeleteToggle");
|
||
const transactionModeInputs = document.querySelectorAll('input[name="transactionMode"]');
|
||
const databaseStatus = document.querySelector("#databaseStatus");
|
||
const localDbFields = document.querySelector("#localDbFields");
|
||
const localDbInputs = document.querySelectorAll("#localDbFields input");
|
||
const testLocalDbButton = document.querySelector("#testLocalDbButton");
|
||
const localDbTestResult = document.querySelector("#localDbTestResult");
|
||
const lightContrastSelect = document.querySelector("#lightContrastSelect");
|
||
const darkContrastSelect = document.querySelector("#darkContrastSelect");
|
||
const catalogScreen = document.querySelector(".catalog-screen");
|
||
const secondaryPanelToggle = document.querySelector("#secondaryPanelToggle");
|
||
const mainWorkspaceToggle = document.querySelector("#mainWorkspaceToggle");
|
||
let sidebarUserClosed = false;
|
||
const nativeFetch = window.fetch.bind(window);
|
||
window.fetch = (input, init = {}) => {
|
||
const url = typeof input === "string" ? input : input.url;
|
||
if (!url.startsWith("/api/"))
|
||
return nativeFetch(input, init);
|
||
const headers = new Headers(init.headers || (typeof input !== "string" ? input.headers : undefined));
|
||
const selectedMode = localStorage.getItem("catalog-maker:database-mode") || "local";
|
||
// Scrape storage is separate; existing catalog endpoints continue reading Local 9bplus.
|
||
headers.set("X-Database-Mode", selectedMode === "live" ? "live" : "local");
|
||
headers.set("X-Scrape-Database", selectedMode === "scrape" ? "1" : "0");
|
||
return nativeFetch(input, { ...init, headers });
|
||
};
|
||
let currentPosition = 1;
|
||
let totalPositions = 0;
|
||
let currentProduct = null;
|
||
let currentCombinations = [];
|
||
let loadRequestId = 0;
|
||
let applicationMode = "dry-run";
|
||
let databaseConnectionState = "unknown";
|
||
const localDraftKey = "catalog-maker:current-product-draft:v2";
|
||
let localDraftProductKey = "";
|
||
const fieldNames = [
|
||
"id_manufacturer",
|
||
"code_manufacture",
|
||
"voc_discount",
|
||
"max_moc_discount",
|
||
"supplier_reference",
|
||
"catalog_valid_until",
|
||
"name",
|
||
"price - moc",
|
||
"combinations",
|
||
"id_attribute_group",
|
||
"id_supplier",
|
||
"id_category_default",
|
||
"9b_category_name",
|
||
"manufacturer_name",
|
||
"catalog category",
|
||
"gender",
|
||
"position/total",
|
||
"Photos loaded",
|
||
"Reference separator",
|
||
];
|
||
const variantColumns = [
|
||
{ key: "status", label: "status", width: 92 },
|
||
{ key: "color", label: "color", width: 156 },
|
||
{ key: "combi", label: "combi", width: 60, align: "center" },
|
||
{ key: "refer", label: "refer", width: 64 },
|
||
{ key: "ean13", label: "ean13", width: 148, align: "center", problem: true },
|
||
{ key: "nineB", label: "9b", width: 58, align: "center" },
|
||
{ key: "supplierStock", label: "sup", width: 58, align: "center" },
|
||
{ key: "id", label: "id", width: 74, align: "center" },
|
||
{ key: "voc", label: "voc", width: 74, align: "center", amount: true },
|
||
{ key: "moc", label: "moc", width: 74, align: "center", amount: true },
|
||
{ key: "productAttribute", label: "pr.at", width: 70, align: "center" },
|
||
{ key: "catalogColor", label: "color", width: 110 },
|
||
{ key: "catalogCombi", label: "combi", width: 70, align: "center" },
|
||
{ key: "nineBVoc", label: "9b.voc", width: 74, align: "center", amount: true },
|
||
{ key: "nineBMoc", label: "9b.moc", width: 74, align: "center", amount: true },
|
||
{ key: "eanStatus", label: "", width: 84, align: "center" },
|
||
];
|
||
const productFieldMap = {
|
||
id_manufacturer: "idManufacturer",
|
||
code_manufacture: "codeManufacture",
|
||
voc_discount: "vocDiscount",
|
||
max_moc_discount: "maxMocDiscount",
|
||
supplier_reference: "supplierReference",
|
||
catalog_valid_until: "catalogValidUntil",
|
||
name: "name",
|
||
"price - moc": "priceMoc",
|
||
combinations: "combinations",
|
||
id_attribute_group: "idAttributeGroup",
|
||
id_supplier: "idSupplier",
|
||
id_category_default: "idCategoryDefault",
|
||
"9b_category_name": "category9bName",
|
||
manufacturer_name: "manufacturerName",
|
||
"catalog category": "catalogCategory",
|
||
gender: "gender",
|
||
"position/total": "positionTotal",
|
||
"Photos loaded": "photosLoaded",
|
||
"Reference separator": "referenceSeparator",
|
||
};
|
||
await loadStatus();
|
||
loadTheme();
|
||
loadAppearance();
|
||
renderFields();
|
||
renderTableHeader();
|
||
setTableWidth();
|
||
await loadManufacturers();
|
||
await loadLanguages();
|
||
manufacturerSelect.addEventListener("change", () => {
|
||
syncManufacturerPickerLabel();
|
||
closeManufacturerPicker();
|
||
if (manufacturerSelect.value) {
|
||
loadMappingSources();
|
||
loadProductForSelectedManufacturer(1);
|
||
}
|
||
else {
|
||
renderMappingSources({ mapped: false, items: [], message: "Select manufacturer." });
|
||
clearProductState("Select manufacturer to load data.");
|
||
}
|
||
});
|
||
manufacturerTrigger.addEventListener("click", toggleManufacturerPicker);
|
||
manufacturerSearch.addEventListener("input", renderManufacturerOptions);
|
||
manufacturerSearch.addEventListener("keydown", (event) => {
|
||
if (event.key === "Escape")
|
||
closeManufacturerPicker();
|
||
});
|
||
document.addEventListener("click", (event) => {
|
||
if (!event.target.closest("#manufacturerPicker"))
|
||
closeManufacturerPicker();
|
||
});
|
||
loadProductButton.disabled = true;
|
||
loadProductButton.title = "Load existing product attribute info.";
|
||
loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
||
findSourcesButton.addEventListener("click", findSourcesForCurrentProduct);
|
||
getPicturesButton.addEventListener("click", getPicturesForCurrentProduct);
|
||
parserDrawerToggle.addEventListener("click", toggleParserDrawer);
|
||
secondaryPanelToggle.addEventListener("click", toggleSecondaryPanel);
|
||
mainWorkspaceToggle.addEventListener("click", toggleMainWorkspace);
|
||
sidebarOpenButton.addEventListener("click", openSidebar);
|
||
sidebarCloseButton.addEventListener("click", closeSidebar);
|
||
sidebarOverlay.addEventListener("click", closeSidebar);
|
||
document.addEventListener("keydown", (event) => {
|
||
if (event.key === "Escape")
|
||
closeSidebar();
|
||
});
|
||
previousProductButton.addEventListener("click", () => {
|
||
if (!manufacturerSelect.value || currentPosition <= 1)
|
||
return;
|
||
loadProductForSelectedManufacturer(currentPosition - 1);
|
||
});
|
||
nextProductButton.addEventListener("click", () => {
|
||
if (!manufacturerSelect.value || (totalPositions && currentPosition >= totalPositions))
|
||
return;
|
||
loadProductForSelectedManufacturer(currentPosition + 1);
|
||
});
|
||
scanInput.addEventListener("keydown", (event) => {
|
||
if (event.key !== "Enter")
|
||
return;
|
||
event.preventDefault();
|
||
loadProductByScannedEan();
|
||
});
|
||
suggestedCloseButton.addEventListener("click", closeSuggestedProducts);
|
||
suggestedModal.addEventListener("click", (event) => {
|
||
if (event.target === suggestedModal)
|
||
closeSuggestedProducts();
|
||
});
|
||
sourceCloseButton.addEventListener("click", closeSourceCandidates);
|
||
sourceModal.addEventListener("click", (event) => {
|
||
if (event.target === sourceModal)
|
||
closeSourceCandidates();
|
||
});
|
||
settingsButton.addEventListener("click", openSettings);
|
||
themeToggle.addEventListener("click", toggleTheme);
|
||
settingsCloseButton.addEventListener("click", closeSettings);
|
||
settingsModal.addEventListener("click", (event) => {
|
||
if (event.target === settingsModal)
|
||
closeSettings();
|
||
});
|
||
for (const input of databaseModeInputs)
|
||
input.addEventListener("change", saveDatabaseMode);
|
||
for (const input of permissionInputs)
|
||
input.addEventListener("change", saveSettings);
|
||
for (const input of transactionModeInputs)
|
||
input.addEventListener("change", saveSettings);
|
||
for (const input of localDbInputs)
|
||
input.addEventListener("change", saveLocalDbSettings);
|
||
testLocalDbButton.addEventListener("click", testLocalDbConnection);
|
||
lightContrastSelect.addEventListener("change", saveAppearance);
|
||
darkContrastSelect.addEventListener("change", saveAppearance);
|
||
loadDatabaseMode();
|
||
syncSidebarForViewport();
|
||
setTimeout(() => {
|
||
if (manufacturerSelect.value) {
|
||
loadMappingSources();
|
||
loadProductForSelectedManufacturer(1);
|
||
}
|
||
else {
|
||
renderMappingSources({ mapped: false, items: [], message: "Select manufacturer." });
|
||
updateNavigationState();
|
||
}
|
||
}, 0);
|
||
async function loadStatus() {
|
||
const response = await fetch("/api/status");
|
||
const status = await response.json();
|
||
applicationMode = status.mode || "dry-run";
|
||
if (status.database) {
|
||
databaseStatus.dataset.driver = status.database.driver;
|
||
const selectedMode = localStorage.getItem("catalog-maker:database-mode") || "local";
|
||
databaseConnectionState = selectedMode === "local" && status.database.driver !== "mariadb"
|
||
? "error"
|
||
: "ready";
|
||
}
|
||
}
|
||
function renderFields() {
|
||
fieldGrid.replaceChildren(...fieldNames.flatMap((name) => {
|
||
const label = document.createElement("div");
|
||
label.className = "field-label";
|
||
label.textContent = name;
|
||
const value = document.createElement("div");
|
||
value.className = "field-value";
|
||
value.dataset.field = name;
|
||
value.textContent = "";
|
||
return [label, value];
|
||
}));
|
||
}
|
||
function renderTableHeader() {
|
||
variantHeader.replaceChildren(...variantColumns.map((column) => {
|
||
const cell = document.createElement("div");
|
||
cell.className = "variant-header-cell";
|
||
cell.setAttribute("role", "columnheader");
|
||
cell.textContent = column.label;
|
||
cell.dataset.column = column.key;
|
||
cell.style.textAlign = column.align || "";
|
||
return cell;
|
||
}));
|
||
}
|
||
function setTableWidth() {
|
||
const totalWidth = variantColumns.reduce((sum, column) => sum + column.width, 0);
|
||
variantTable.style.setProperty("--variant-min-width", `${Math.max(totalWidth, 820)}px`);
|
||
variantTable.style.setProperty("--variant-columns", variantColumns.map((column) => `${column.width}px`).join(" "));
|
||
}
|
||
async function loadManufacturers() {
|
||
manufacturerSelect.replaceChildren(createOption("", "Select manufacturer"));
|
||
try {
|
||
const response = await fetch("/api/manufacturers");
|
||
const data = await response.json();
|
||
if (!response.ok)
|
||
throw new Error(data.message || "Local DB is not configured.");
|
||
for (const manufacturer of data.items || []) {
|
||
manufacturerSelect.append(createOption(String(manufacturer.id), manufacturer.name));
|
||
}
|
||
if (data.message)
|
||
manufacturerSelect.append(createOption("", data.message));
|
||
syncManufacturerPickerLabel();
|
||
renderManufacturerOptions();
|
||
}
|
||
catch (error) {
|
||
manufacturerSelect.append(createOption("", error.message || "Local DB is not configured."));
|
||
syncManufacturerPickerLabel();
|
||
renderManufacturerOptions();
|
||
}
|
||
}
|
||
function toggleManufacturerPicker() {
|
||
if (manufacturerMenu.hidden) {
|
||
manufacturerMenu.hidden = false;
|
||
manufacturerTrigger.setAttribute("aria-expanded", "true");
|
||
manufacturerSearch.value = "";
|
||
renderManufacturerOptions();
|
||
if (manufacturerSelect.options.length > 11)
|
||
manufacturerSearch.focus();
|
||
}
|
||
else {
|
||
closeManufacturerPicker();
|
||
}
|
||
}
|
||
function closeManufacturerPicker() {
|
||
manufacturerMenu.hidden = true;
|
||
manufacturerTrigger.setAttribute("aria-expanded", "false");
|
||
}
|
||
function syncManufacturerPickerLabel() {
|
||
const option = manufacturerSelect.selectedOptions[0];
|
||
manufacturerValue.textContent = option?.textContent || "Select manufacturer";
|
||
}
|
||
function renderManufacturerOptions() {
|
||
const options = [...manufacturerSelect.options].filter((option) => option.value);
|
||
const query = manufacturerSearch.value.trim().toLocaleLowerCase();
|
||
const filtered = options.filter((option) => option.textContent.toLocaleLowerCase().includes(query));
|
||
const visible = filtered.slice(0, 10);
|
||
manufacturerOptions.replaceChildren(...visible.map((option) => {
|
||
const item = document.createElement("button");
|
||
item.className = "manufacturer-option";
|
||
item.type = "button";
|
||
item.setAttribute("role", "option");
|
||
item.textContent = option.textContent;
|
||
item.setAttribute("aria-selected", String(option.value === manufacturerSelect.value));
|
||
item.addEventListener("click", () => {
|
||
manufacturerSelect.value = option.value;
|
||
manufacturerSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||
});
|
||
return item;
|
||
}));
|
||
if (!visible.length) {
|
||
const empty = document.createElement("div");
|
||
empty.className = "manufacturer-empty";
|
||
empty.textContent = "No manufacturer found.";
|
||
manufacturerOptions.append(empty);
|
||
}
|
||
}
|
||
function createOption(value, label) {
|
||
const option = document.createElement("option");
|
||
option.value = value;
|
||
option.textContent = label;
|
||
return option;
|
||
}
|
||
async function loadLanguages() {
|
||
languageSelect.replaceChildren(createOption("", "Select language"));
|
||
try {
|
||
const response = await fetch("/api/languages");
|
||
const data = await response.json();
|
||
if (!response.ok)
|
||
throw new Error(data.message || "Local DB is not configured.");
|
||
for (const language of data.items || []) {
|
||
const label = language.isoCode ? `${language.name} (${language.isoCode})` : language.name;
|
||
languageSelect.append(createOption(String(language.id), label));
|
||
}
|
||
if (data.message)
|
||
languageSelect.append(createOption("", data.message));
|
||
}
|
||
catch (error) {
|
||
languageSelect.append(createOption("", error.message || "Local DB is not configured."));
|
||
}
|
||
}
|
||
async function loadMappingSources() {
|
||
const manufacturerId = manufacturerSelect.value;
|
||
if (!manufacturerId) {
|
||
renderMappingSources({ mapped: false, items: [], message: "Select manufacturer." });
|
||
return;
|
||
}
|
||
mappingState.textContent = "Loading mapping...";
|
||
mappingState.className = "mapping-state";
|
||
mappingList.replaceChildren();
|
||
try {
|
||
const params = new URLSearchParams({ manufacturerId, check: "1" });
|
||
const response = await fetch(`/api/catalog-maker/mapping-sources?${params.toString()}`);
|
||
const data = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(data.message || "Mapping load failed.");
|
||
}
|
||
renderMappingSources(data);
|
||
}
|
||
catch (error) {
|
||
renderMappingSources({
|
||
mapped: false,
|
||
items: [],
|
||
message: error.message || "Mapping load failed.",
|
||
});
|
||
}
|
||
}
|
||
function renderMappingSources(data) {
|
||
const items = data.items || [];
|
||
if (!data.mapped || !items.length) {
|
||
mappingState.textContent = data.message || "Mapping missing";
|
||
mappingState.className = "mapping-state is-missing";
|
||
mappingList.replaceChildren();
|
||
mappingDetails.open = true;
|
||
return;
|
||
}
|
||
const enabledCount = items.filter((item) => item.enabled).length;
|
||
const failedCount = items.filter((item) => item.enabled && item.health && item.health !== "ok").length;
|
||
const testedCount = items.filter((item) => item.enabled && item.health === "ok").length;
|
||
const ready = enabledCount > 0 && failedCount === 0;
|
||
mappingState.textContent = ready
|
||
? `Sources OK ✓ (${testedCount}/${enabledCount})`
|
||
: data.message || `Sources problem (${testedCount}/${enabledCount})`;
|
||
mappingState.className = ready ? "mapping-state is-ready" : "mapping-state is-missing";
|
||
mappingDetails.open = !ready || failedCount > 0;
|
||
mappingList.replaceChildren(...items.map((item) => {
|
||
const row = document.createElement("div");
|
||
row.className = `mapping-source-row${item.enabled ? "" : " is-disabled"}`;
|
||
const name = document.createElement("span");
|
||
name.className = "mapping-source-name";
|
||
name.textContent = formatValue(item.name || item.key || item.url || "Source");
|
||
const status = document.createElement("span");
|
||
status.className = `mapping-source-status ${getMappingHealthClass(item)}`;
|
||
status.textContent = getMappingHealthText(item);
|
||
row.append(name, status);
|
||
return row;
|
||
}));
|
||
}
|
||
function getMappingHealthText(item) {
|
||
if (!item.enabled)
|
||
return "off";
|
||
if (item.health === "ok")
|
||
return `${item.httpStatus || 200} OK`;
|
||
if (item.health === "missing-url")
|
||
return "no URL";
|
||
if (item.httpStatus)
|
||
return `${item.httpStatus}`;
|
||
if (item.health)
|
||
return "error";
|
||
return "netest";
|
||
}
|
||
function getMappingHealthClass(item) {
|
||
if (!item.enabled)
|
||
return "is-muted";
|
||
if (item.health === "ok")
|
||
return "is-ok";
|
||
return "is-bad";
|
||
}
|
||
async function loadProductForSelectedManufacturer(position = currentPosition) {
|
||
const requestId = ++loadRequestId;
|
||
const manufacturerId = manufacturerSelect.value;
|
||
if (!manufacturerId) {
|
||
clearProductState("Select manufacturer to load data.");
|
||
return;
|
||
}
|
||
setNavigationEnabled(false);
|
||
setTableMessage("Loading product data...");
|
||
try {
|
||
const response = await fetch(`/api/catalog-maker/product?manufacturerId=${encodeURIComponent(manufacturerId)}&position=${encodeURIComponent(position)}`);
|
||
const data = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(data.message || "Product load failed.");
|
||
}
|
||
if (!data.product) {
|
||
if (requestId !== loadRequestId)
|
||
return;
|
||
clearProductState(data.message || "No product to do for selected manufacturer.");
|
||
return;
|
||
}
|
||
if (requestId !== loadRequestId)
|
||
return;
|
||
renderProduct(data.product);
|
||
currentCombinations = data.combinations;
|
||
renderCombinations(currentCombinations);
|
||
}
|
||
catch (error) {
|
||
setTableMessage(error.message);
|
||
}
|
||
finally {
|
||
updateNavigationState();
|
||
}
|
||
}
|
||
async function loadProductByScannedEan() {
|
||
const requestId = ++loadRequestId;
|
||
const manufacturerId = manufacturerSelect.value;
|
||
const ean = normalizeScannedEan(scanInput.value);
|
||
if (!manufacturerId) {
|
||
setTableMessage("Select manufacturer before scanning EAN.");
|
||
return;
|
||
}
|
||
if (!ean) {
|
||
setTableMessage("Scan EAN first.");
|
||
return;
|
||
}
|
||
setNavigationEnabled(false);
|
||
scanInput.disabled = true;
|
||
setTableMessage(`Searching EAN ${ean}...`);
|
||
try {
|
||
const params = new URLSearchParams({ manufacturerId, ean });
|
||
const response = await fetch(`/api/catalog-maker/product-by-ean?${params.toString()}`);
|
||
const data = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(data.message || "EAN search failed.");
|
||
}
|
||
if (!data.product) {
|
||
if (requestId !== loadRequestId)
|
||
return;
|
||
await showSuggestedProducts({ ean });
|
||
return;
|
||
}
|
||
if (requestId !== loadRequestId)
|
||
return;
|
||
renderProduct(data.product);
|
||
currentCombinations = data.combinations;
|
||
renderCombinations(currentCombinations);
|
||
scanInput.value = "";
|
||
if (!data.product.idProduct) {
|
||
await showSuggestedProducts({
|
||
ean,
|
||
supplierReference: data.product.supplierReference,
|
||
productName: data.product.name,
|
||
});
|
||
}
|
||
}
|
||
catch (error) {
|
||
setTableMessage(error.message);
|
||
}
|
||
finally {
|
||
scanInput.disabled = false;
|
||
scanInput.focus();
|
||
updateNavigationState();
|
||
}
|
||
}
|
||
async function showSuggestedProducts({ ean, supplierReference = "", productName = "" }) {
|
||
const params = new URLSearchParams({
|
||
manufacturerId: manufacturerSelect.value,
|
||
supplierReference: supplierReference || currentProduct?.supplierReference || scanInput.value || ean,
|
||
productName: productName || currentProduct?.name || "",
|
||
});
|
||
const response = await fetch(`/api/catalog-maker/suggested-products?${params.toString()}`);
|
||
const data = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(data.message || "Suggested product lookup failed.");
|
||
}
|
||
suggestedTitle.textContent = `Sugested product for ${ean}`;
|
||
renderSuggestedProducts(data.items);
|
||
suggestedModal.hidden = false;
|
||
}
|
||
function renderSuggestedProducts(items) {
|
||
if (!items.length) {
|
||
const empty = document.createElement("div");
|
||
empty.className = "suggested-row";
|
||
empty.textContent = "No suggested products found.";
|
||
suggestedList.replaceChildren(empty);
|
||
return;
|
||
}
|
||
suggestedList.replaceChildren(...items.map((item) => {
|
||
const row = document.createElement("div");
|
||
row.className = "suggested-row";
|
||
const image = document.createElement("img");
|
||
image.alt = item.name || "Product";
|
||
if (item.imageUrl)
|
||
image.src = item.imageUrl;
|
||
const id = document.createElement("div");
|
||
id.className = "suggested-id";
|
||
id.textContent = formatValue(item.idProduct);
|
||
const name = document.createElement("div");
|
||
name.className = "suggested-name";
|
||
name.textContent = formatValue(item.name);
|
||
name.title = name.textContent;
|
||
const reference = document.createElement("div");
|
||
reference.className = "suggested-reference";
|
||
reference.textContent = formatValue(item.supplierReference);
|
||
reference.title = reference.textContent;
|
||
const active = document.createElement("div");
|
||
active.className = "suggested-active";
|
||
active.textContent = formatValue(item.active);
|
||
const pick = document.createElement("button");
|
||
pick.className = "suggested-pick";
|
||
pick.type = "button";
|
||
pick.textContent = "Use";
|
||
pick.addEventListener("click", () => selectSuggestedProduct(item));
|
||
row.append(image, id, name, reference, active, pick);
|
||
return row;
|
||
}));
|
||
}
|
||
function selectSuggestedProduct(item) {
|
||
if (!currentProduct)
|
||
return;
|
||
currentProduct = {
|
||
...currentProduct,
|
||
idProduct: item.idProduct,
|
||
imageUrl: item.imageUrl || currentProduct.imageUrl,
|
||
};
|
||
recordIdElement.textContent = formatRecordId(item.idProduct);
|
||
renderPicture(currentProduct.imageUrl);
|
||
closeSuggestedProducts();
|
||
updateNavigationState();
|
||
}
|
||
function closeSuggestedProducts() {
|
||
suggestedModal.hidden = true;
|
||
suggestedList.replaceChildren();
|
||
}
|
||
async function findSourcesForCurrentProduct() {
|
||
if (!manufacturerSelect.value || !currentProduct) {
|
||
setTableMessage("Select manufacturer and product first.");
|
||
return;
|
||
}
|
||
const savedDraft = readLocalDraft();
|
||
if (savedDraft?.sources) {
|
||
renderSourceCandidates(savedDraft.sources.items || []);
|
||
updateFindSourcesButtonState();
|
||
sourceTitle.textContent = "Source candidates (cached)";
|
||
sourceModal.hidden = false;
|
||
return;
|
||
}
|
||
const supplierColorGroup = getFirstSupplierColorGroup();
|
||
findSourcesButton.disabled = true;
|
||
findSourcesButton.textContent = "Finding sources...";
|
||
try {
|
||
const params = new URLSearchParams({
|
||
manufacturerId: manufacturerSelect.value,
|
||
productName: currentProduct.name || "",
|
||
supplierReference: currentProduct.supplierReference || "",
|
||
ean: supplierColorGroup?.supplierEan || currentCombinations[0]?.ean13 || "",
|
||
eanGroup: (supplierColorGroup?.supplierRows || []).map((row) => row.ean13).filter(Boolean).join(","),
|
||
color: supplierColorGroup?.color || "",
|
||
});
|
||
const response = await fetch(`/api/catalog-maker/find-sources?${params.toString()}`);
|
||
const data = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(data.message || "Source search failed.");
|
||
}
|
||
saveLocalDraft({ sources: data });
|
||
updateFindSourcesButtonState();
|
||
renderSourceCandidates(data.items || []);
|
||
const targetLabel = supplierColorGroup
|
||
? `${supplierColorGroup.color} / EAN ${supplierColorGroup.supplierEan}`
|
||
: currentProduct.name || currentProduct.supplierReference;
|
||
sourceTitle.textContent = `Source candidates for ${targetLabel}`;
|
||
sourceModal.hidden = false;
|
||
}
|
||
catch (error) {
|
||
setTableMessage(error.message || "Source search failed.");
|
||
}
|
||
finally {
|
||
updateFindSourcesButtonState();
|
||
}
|
||
}
|
||
async function getPicturesForCurrentProduct(sourceKey = "") {
|
||
if (!manufacturerSelect.value || !currentProduct) {
|
||
setTableMessage("Select manufacturer and product first.");
|
||
return;
|
||
}
|
||
const savedDraft = readLocalDraft();
|
||
const cachedPictures = savedDraft?.picturesBySource?.[sourceKey || "all"];
|
||
if (cachedPictures) {
|
||
renderPictureCandidates(cachedPictures.items || []);
|
||
sourceTitle.textContent = `Picture candidates (cached${sourceKey ? `: ${sourceKey}` : ""})`;
|
||
sourceModal.hidden = false;
|
||
return;
|
||
}
|
||
const supplierColorGroup = getFirstSupplierColorGroup();
|
||
getPicturesButton.disabled = true;
|
||
getPicturesButton.textContent = "Loading pictures...";
|
||
try {
|
||
const params = new URLSearchParams({
|
||
manufacturerId: manufacturerSelect.value,
|
||
productName: currentProduct.name || "",
|
||
supplierReference: currentProduct.supplierReference || "",
|
||
ean: supplierColorGroup?.supplierEan || currentCombinations[0]?.ean13 || "",
|
||
eanGroup: (supplierColorGroup?.supplierRows || []).map((row) => row.ean13).filter(Boolean).join(","),
|
||
color: supplierColorGroup?.color || "",
|
||
});
|
||
if (sourceKey)
|
||
params.set("sourceKey", sourceKey);
|
||
const response = await fetch(`/api/catalog-maker/get-pictures?${params.toString()}`);
|
||
const data = await response.json();
|
||
if (!response.ok)
|
||
throw new Error(data.message || "Picture lookup failed.");
|
||
const picturesBySource = readLocalDraft()?.picturesBySource || {};
|
||
picturesBySource[sourceKey || "all"] = data;
|
||
saveLocalDraft({ picturesBySource });
|
||
renderPictureCandidates(data.items || []);
|
||
sourceTitle.textContent = `Picture candidates${sourceKey ? `: ${sourceKey}` : ""}`;
|
||
sourceModal.hidden = false;
|
||
}
|
||
catch (error) {
|
||
setTableMessage(error.message || "Picture lookup failed.");
|
||
}
|
||
finally {
|
||
getPicturesButton.disabled = false;
|
||
getPicturesButton.textContent = "Get pictures";
|
||
updateNavigationState();
|
||
}
|
||
}
|
||
function getFirstSupplierColorGroup() {
|
||
const groups = new Map();
|
||
for (const combination of currentCombinations) {
|
||
const color = formatValue(combination.color) || "Unknown color";
|
||
const group = groups.get(color) || {
|
||
color,
|
||
rows: [],
|
||
supplierRows: [],
|
||
supplierEan: "",
|
||
};
|
||
group.rows.push(combination);
|
||
if (Number(combination.supplierStock) === 1) {
|
||
group.supplierRows.push(combination);
|
||
if (!group.supplierEan)
|
||
group.supplierEan = combination.ean13;
|
||
}
|
||
groups.set(color, group);
|
||
}
|
||
return [...groups.values()].find((group) => group.supplierRows.length > 0) || null;
|
||
}
|
||
function renderSourceCandidates(items) {
|
||
if (!items.length) {
|
||
const empty = document.createElement("div");
|
||
empty.className = "source-row";
|
||
empty.textContent = "No source mapping found.";
|
||
sourceList.replaceChildren(empty);
|
||
return;
|
||
}
|
||
sourceList.replaceChildren(...items.map((item) => {
|
||
const section = document.createElement("section");
|
||
section.className = "source-section";
|
||
const header = document.createElement("div");
|
||
header.className = "source-section-header";
|
||
const name = document.createElement("strong");
|
||
name.textContent = formatValue(item.source?.name || item.source?.key || "Source");
|
||
const status = document.createElement("span");
|
||
status.className = `source-status ${item.status === "found" ? "is-ok" : "is-muted"}`;
|
||
status.textContent = item.status === "found" ? `${item.candidates.length} found` : item.status;
|
||
const actions = document.createElement("div");
|
||
actions.className = "source-section-actions";
|
||
const pictures = document.createElement("button");
|
||
pictures.type = "button";
|
||
pictures.className = "source-picture-action";
|
||
pictures.textContent = "Get pictures";
|
||
pictures.addEventListener("click", () => getPicturesForCurrentProduct(item.source?.key || ""));
|
||
actions.append(status, pictures);
|
||
header.append(name, actions);
|
||
const rows = item.candidates.length
|
||
? item.candidates.map(renderSourceCandidateRow)
|
||
: [renderManualSearchRow(item.searchUrl, item.error || "Open search")];
|
||
section.append(header, ...rows);
|
||
return section;
|
||
}));
|
||
}
|
||
function renderPictureCandidates(items) {
|
||
const rows = [];
|
||
for (const item of items) {
|
||
for (const candidate of item.candidates || []) {
|
||
for (const picture of candidate.pictures || []) {
|
||
const row = document.createElement("a");
|
||
row.className = "picture-candidate";
|
||
row.href = picture;
|
||
row.target = "_blank";
|
||
row.rel = "noreferrer";
|
||
const image = document.createElement("img");
|
||
image.src = picture;
|
||
image.alt = candidate.rawTitle || item.source?.name || "Product picture";
|
||
image.loading = "lazy";
|
||
const label = document.createElement("span");
|
||
label.textContent = item.source?.name || item.source?.key || "Source";
|
||
row.append(image, label);
|
||
rows.push(row);
|
||
}
|
||
}
|
||
}
|
||
if (!rows.length) {
|
||
const empty = document.createElement("div");
|
||
empty.className = "source-row";
|
||
empty.textContent = "No pictures found. Open the source manually to inspect it.";
|
||
sourceList.replaceChildren(empty);
|
||
return;
|
||
}
|
||
sourceList.replaceChildren(...rows);
|
||
}
|
||
function collectPictureUrls(items) {
|
||
return items.flatMap((item) => (item.candidates || []).flatMap((candidate) => candidate.pictures || [])).filter((url, index, all) => all.indexOf(url) === index);
|
||
}
|
||
function saveLocalDraft(patch) {
|
||
if (!localDraftProductKey)
|
||
return;
|
||
let draft = {};
|
||
try {
|
||
draft = JSON.parse(localStorage.getItem(localDraftKey) || "{}");
|
||
}
|
||
catch {
|
||
draft = {};
|
||
}
|
||
localStorage.setItem(localDraftKey, JSON.stringify({
|
||
...draft,
|
||
productKey: localDraftProductKey,
|
||
...patch,
|
||
}));
|
||
}
|
||
function readLocalDraft() {
|
||
if (!localDraftProductKey)
|
||
return null;
|
||
try {
|
||
const draft = JSON.parse(localStorage.getItem(localDraftKey) || "null");
|
||
return draft?.productKey === localDraftProductKey ? draft : null;
|
||
}
|
||
catch {
|
||
return null;
|
||
}
|
||
}
|
||
function clearLocalDraft() {
|
||
localStorage.removeItem(localDraftKey);
|
||
localDraftProductKey = "";
|
||
updateFindSourcesButtonState();
|
||
}
|
||
function updateFindSourcesButtonState() {
|
||
const hasSources = Boolean(readLocalDraft()?.sources);
|
||
findSourcesButton.textContent = hasSources ? "Find sources ✓" : "Find sources";
|
||
findSourcesButton.disabled = !manufacturerSelect.value || !currentProduct;
|
||
}
|
||
function renderSourceCandidateRow(candidate) {
|
||
const row = document.createElement("a");
|
||
row.className = "source-row";
|
||
row.href = candidate.url;
|
||
row.target = "_blank";
|
||
row.rel = "noreferrer";
|
||
const title = document.createElement("span");
|
||
title.className = "source-row-title";
|
||
title.textContent = formatValue(candidate.title || candidate.url);
|
||
title.title = formatValue(candidate.rawTitle || candidate.title || candidate.url);
|
||
const action = document.createElement("span");
|
||
action.className = "source-row-action";
|
||
action.textContent = "Open";
|
||
row.append(title, action);
|
||
return row;
|
||
}
|
||
function renderManualSearchRow(url, label) {
|
||
return renderSourceCandidateRow({
|
||
title: label,
|
||
url,
|
||
});
|
||
}
|
||
function closeSourceCandidates() {
|
||
sourceModal.hidden = true;
|
||
sourceList.replaceChildren();
|
||
}
|
||
function openSettings() {
|
||
settingsModal.hidden = false;
|
||
}
|
||
function toggleParserDrawer() {
|
||
const isOpen = !parserDrawer.hidden;
|
||
parserDrawer.hidden = isOpen;
|
||
parserDrawerToggle.setAttribute("aria-expanded", String(!isOpen));
|
||
parserDrawerToggle.querySelector(".drawer-chevron").textContent = isOpen ? "⌄" : "⌃";
|
||
}
|
||
function syncSidebarForViewport() {
|
||
if (window.matchMedia("(min-width: 981px)").matches && !sidebarUserClosed)
|
||
openSidebar();
|
||
else
|
||
closeSidebar();
|
||
}
|
||
function openSidebar() {
|
||
sidebarUserClosed = false;
|
||
sidebarDrawer.classList.add("is-open");
|
||
sidebarOverlay.hidden = false;
|
||
sidebarOpenButton.hidden = true;
|
||
sidebarOpenButton.setAttribute("aria-expanded", "true");
|
||
sidebarDrawer.setAttribute("aria-hidden", "false");
|
||
catalogScreen.classList.remove("sidebar-collapsed");
|
||
}
|
||
function closeSidebar() {
|
||
sidebarUserClosed = true;
|
||
sidebarDrawer.classList.remove("is-open");
|
||
sidebarOverlay.hidden = true;
|
||
sidebarOpenButton.hidden = false;
|
||
sidebarOpenButton.setAttribute("aria-expanded", "false");
|
||
sidebarDrawer.setAttribute("aria-hidden", "true");
|
||
catalogScreen.classList.add("sidebar-collapsed");
|
||
}
|
||
function toggleSecondaryPanel() {
|
||
const isCollapsed = catalogScreen.classList.toggle("secondary-collapsed");
|
||
secondaryPanelToggle.setAttribute("aria-expanded", String(!isCollapsed));
|
||
secondaryPanelToggle.setAttribute("aria-label", isCollapsed ? "Expand product details" : "Collapse product details");
|
||
secondaryPanelToggle.title = isCollapsed ? "Expand product details" : "Collapse product details";
|
||
secondaryPanelToggle.textContent = isCollapsed ? "›" : "‹";
|
||
}
|
||
function toggleMainWorkspace() {
|
||
const isCollapsed = catalogScreen.classList.toggle("workspace-collapsed");
|
||
mainWorkspaceToggle.setAttribute("aria-expanded", String(!isCollapsed));
|
||
mainWorkspaceToggle.setAttribute("aria-label", isCollapsed ? "Expand main workspace" : "Collapse main workspace");
|
||
mainWorkspaceToggle.title = isCollapsed ? "Expand main workspace" : "Collapse main workspace";
|
||
mainWorkspaceToggle.textContent = isCollapsed ? "‹" : "›";
|
||
}
|
||
function loadTheme() {
|
||
const isDark = localStorage.getItem("catalog-maker:theme") === "dark";
|
||
document.documentElement.classList.toggle("dark", isDark);
|
||
themeToggle.querySelector("span").textContent = isDark ? "Light theme" : "Dark theme";
|
||
themeToggle.setAttribute("aria-pressed", String(isDark));
|
||
}
|
||
function loadAppearance() {
|
||
const settings = JSON.parse(localStorage.getItem("catalog-maker:appearance") || "{}");
|
||
lightContrastSelect.value = settings.lightContrast || "medium";
|
||
darkContrastSelect.value = settings.darkContrast || "medium";
|
||
applyAppearance();
|
||
}
|
||
function saveAppearance() {
|
||
localStorage.setItem("catalog-maker:appearance", JSON.stringify({
|
||
lightContrast: lightContrastSelect.value,
|
||
darkContrast: darkContrastSelect.value,
|
||
}));
|
||
applyAppearance();
|
||
}
|
||
function applyAppearance() {
|
||
const root = document.documentElement;
|
||
root.classList.remove("contrast-light-low", "contrast-light-medium", "contrast-light-high", "contrast-dark-low", "contrast-dark-medium", "contrast-dark-high");
|
||
root.classList.add(`contrast-light-${lightContrastSelect.value || "medium"}`);
|
||
root.classList.add(`contrast-dark-${darkContrastSelect.value || "medium"}`);
|
||
}
|
||
function toggleTheme() {
|
||
const isDark = !document.documentElement.classList.contains("dark");
|
||
localStorage.setItem("catalog-maker:theme", isDark ? "dark" : "light");
|
||
loadTheme();
|
||
}
|
||
function closeSettings() {
|
||
settingsModal.hidden = true;
|
||
}
|
||
function loadDatabaseMode() {
|
||
const mode = localStorage.getItem("catalog-maker:database-mode") || "local";
|
||
const input = document.querySelector(`input[name="databaseMode"][value="${mode}"]`);
|
||
if (input)
|
||
input.checked = true;
|
||
const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}");
|
||
for (const input of permissionInputs)
|
||
input.checked = permissions[input.id] === true;
|
||
const transactionMode = localStorage.getItem("catalog-maker:transaction-mode") || "transaction";
|
||
const transactionInput = document.querySelector(`input[name="transactionMode"][value="${transactionMode}"]`);
|
||
if (transactionInput)
|
||
transactionInput.checked = true;
|
||
const localDbSettings = JSON.parse(localStorage.getItem("catalog-maker:local-db-settings") || "{}");
|
||
for (const input of localDbInputs) {
|
||
if (Object.prototype.hasOwnProperty.call(localDbSettings, input.id))
|
||
input.value = localDbSettings[input.id];
|
||
}
|
||
syncDatabasePermissions(mode);
|
||
syncLocalDbFields(mode);
|
||
renderDatabaseModeNote(mode);
|
||
refreshDatabaseStatusLabel();
|
||
}
|
||
function saveDatabaseMode(event) {
|
||
const mode = ["local", "live", "scrape"].includes(event.target.value) ? event.target.value : "local";
|
||
localStorage.setItem("catalog-maker:database-mode", mode);
|
||
syncDatabasePermissions(mode);
|
||
syncLocalDbFields(mode);
|
||
saveSettings();
|
||
renderDatabaseModeNote(mode);
|
||
refreshDatabaseStatusLabel();
|
||
}
|
||
function saveLocalDbSettings() {
|
||
const settings = {};
|
||
for (const input of localDbInputs)
|
||
settings[input.id] = input.value;
|
||
localStorage.setItem("catalog-maker:local-db-settings", JSON.stringify(settings));
|
||
}
|
||
async function testLocalDbConnection() {
|
||
saveLocalDbSettings();
|
||
testLocalDbButton.disabled = true;
|
||
localDbTestResult.textContent = "Testing connection...";
|
||
localDbTestResult.className = "settings-note";
|
||
const payload = Object.fromEntries([...localDbInputs].map((input) => [
|
||
input.id.replace("localDb", "").toLowerCase(),
|
||
input.value,
|
||
]));
|
||
try {
|
||
const response = await fetch("/api/database/test", {
|
||
method: "POST",
|
||
headers: { "content-type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
const data = await response.json();
|
||
if (!response.ok || !data.connected)
|
||
throw new Error(data.message || "Connection failed.");
|
||
localDbTestResult.textContent = `Connected: ${data.version}`;
|
||
localDbTestResult.className = "settings-note connection-ok";
|
||
}
|
||
catch (error) {
|
||
localDbTestResult.textContent = `Connection error: ${error.message}`;
|
||
localDbTestResult.className = "settings-note connection-error";
|
||
}
|
||
finally {
|
||
testLocalDbButton.disabled = false;
|
||
}
|
||
}
|
||
function syncLocalDbFields(mode) {
|
||
localDbFields.hidden = mode !== "local";
|
||
}
|
||
function syncDatabasePermissions(mode) {
|
||
const isReadOnly = mode === "live" || mode === "scrape";
|
||
for (const input of permissionInputs) {
|
||
input.disabled = isReadOnly;
|
||
if (isReadOnly)
|
||
input.checked = false;
|
||
}
|
||
}
|
||
function saveSettings() {
|
||
const permissions = {};
|
||
for (const input of permissionInputs)
|
||
permissions[input.id] = input.checked;
|
||
localStorage.setItem("catalog-maker:db-permissions", JSON.stringify(permissions));
|
||
const transactionMode = [...transactionModeInputs].find((input) => input.checked)?.value || "transaction";
|
||
localStorage.setItem("catalog-maker:transaction-mode", transactionMode);
|
||
refreshDatabaseStatusLabel();
|
||
}
|
||
function refreshDatabaseStatusLabel() {
|
||
const mode = localStorage.getItem("catalog-maker:database-mode") || "local";
|
||
const transactionMode = localStorage.getItem("catalog-maker:transaction-mode") || "transaction";
|
||
const permissions = JSON.parse(localStorage.getItem("catalog-maker:db-permissions") || "{}");
|
||
const writesEnabled = mode === "local" && (permissions.allowInsertToggle || permissions.allowUpdateToggle || permissions.allowDeleteToggle);
|
||
const dbLabel = mode === "live" ? "Live 9bplus DB" : mode === "scrape" ? "Local scrape DB" : "Local 9bplus DB";
|
||
const configuredDriver = databaseStatus.dataset.driver;
|
||
const connectionError = (mode === "local" && configuredDriver !== "mariadb")
|
||
|| (mode === "live" && configuredDriver !== "endpoint");
|
||
const connectionLabel = connectionError ? "connection error" : "connected";
|
||
const accessLabel = writesEnabled ? "writes enabled" : "read-only";
|
||
const executionLabel = applicationMode === "dry-run"
|
||
? "dry-run"
|
||
: (transactionMode === "dry-run" ? "dry-run" : "transaction");
|
||
databaseStatus.textContent = `${dbLabel} / ${connectionLabel} / ${accessLabel} / ${executionLabel}`;
|
||
databaseStatus.classList.toggle("database-error", connectionError);
|
||
}
|
||
function renderDatabaseModeNote(mode) {
|
||
databaseModeNote.textContent = mode === "live"
|
||
? "Live 9bplus DB selected for read-only data."
|
||
: mode === "scrape"
|
||
? "Local scrape DB selected for scraper settings and scraped data. Catalog reads stay on Local 9bplus DB."
|
||
: "Local 9bplus DB selected.";
|
||
}
|
||
function renderProduct(product) {
|
||
const productKey = `${manufacturerSelect.value}:${product.idProductCatalog || product.supplierReference || product.name || ""}`;
|
||
if (localDraftProductKey && localDraftProductKey !== productKey)
|
||
clearLocalDraft();
|
||
localDraftProductKey = productKey;
|
||
currentProduct = product;
|
||
recordIdElement.textContent = formatRecordId(product.idProduct);
|
||
currentPosition = Number(product.position) || currentPosition;
|
||
totalPositions = Number(product.total) || 0;
|
||
const enrichedProduct = {
|
||
...product,
|
||
positionTotal: product.position && product.total ? `${product.position}/${product.total}` : "",
|
||
};
|
||
for (const [label, key] of Object.entries(productFieldMap)) {
|
||
const valueElement = document.querySelector(`[data-field="${CSS.escape(label)}"]`);
|
||
if (!valueElement)
|
||
continue;
|
||
valueElement.textContent = formatValue(enrichedProduct[key]);
|
||
}
|
||
renderPicture(product.imageUrl);
|
||
updateFindSourcesButtonState();
|
||
updateNavigationState();
|
||
}
|
||
function renderCombinations(combinations) {
|
||
if (!combinations.length) {
|
||
setTableMessage("No combinations found for selected product.");
|
||
return;
|
||
}
|
||
tableBody.replaceChildren(...combinations.map((item) => {
|
||
const row = document.createElement("div");
|
||
row.className = "variant-row";
|
||
row.setAttribute("role", "row");
|
||
row.append(...variantColumns.map((column) => {
|
||
const value = item[column.key];
|
||
const text = formatTableValue(value, column);
|
||
const cell = document.createElement("div");
|
||
cell.className = "variant-cell";
|
||
cell.setAttribute("role", "cell");
|
||
cell.textContent = text;
|
||
cell.title = text;
|
||
cell.dataset.column = column.key;
|
||
cell.dataset.label = column.label;
|
||
cell.style.textAlign = column.align || "";
|
||
if ((column.problem && item.hasProblem) || (column.key === "eanStatus" && text)) {
|
||
cell.classList.add("problem-ean");
|
||
}
|
||
return cell;
|
||
}));
|
||
return row;
|
||
}));
|
||
}
|
||
function clearProductState(message) {
|
||
clearLocalDraft();
|
||
recordIdElement.textContent = "";
|
||
currentProduct = null;
|
||
currentCombinations = [];
|
||
currentPosition = 1;
|
||
totalPositions = 0;
|
||
for (const valueElement of document.querySelectorAll(".field-value")) {
|
||
valueElement.textContent = "";
|
||
}
|
||
renderPicture();
|
||
setTableMessage(message);
|
||
updateNavigationState();
|
||
}
|
||
function setTableMessage(message) {
|
||
const row = document.createElement("div");
|
||
row.className = "variant-row variant-message-row";
|
||
row.setAttribute("role", "row");
|
||
const cell = document.createElement("div");
|
||
cell.className = "empty-row";
|
||
cell.setAttribute("role", "cell");
|
||
cell.style.gridColumn = "1 / -1";
|
||
cell.textContent = message;
|
||
row.append(cell);
|
||
tableBody.replaceChildren(row);
|
||
}
|
||
function formatValue(value) {
|
||
if (value === null || value === undefined)
|
||
return "";
|
||
if (typeof value === "number")
|
||
return String(value);
|
||
const text = String(value).trim();
|
||
if (/^\d{4}-\d{2}-\d{2}T/.test(text))
|
||
return formatDate(text);
|
||
if (/^-?\d+\.0+$/.test(text))
|
||
return String(Number.parseInt(text, 10));
|
||
if (/^-?\d+\.\d+$/.test(text))
|
||
return trimDecimal(text);
|
||
return text;
|
||
}
|
||
function formatTableValue(value, column) {
|
||
if (value === null || value === undefined || value === "")
|
||
return "";
|
||
if (column.amount) {
|
||
const amount = Number(value);
|
||
if (Number.isFinite(amount))
|
||
return String(Math.round(amount));
|
||
}
|
||
return formatValue(value);
|
||
}
|
||
function formatRecordId(value) {
|
||
const text = formatValue(value);
|
||
return text || "";
|
||
}
|
||
function formatDate(value) {
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime()))
|
||
return value;
|
||
return new Intl.DateTimeFormat("cs-CZ", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
timeZone: "UTC",
|
||
})
|
||
.format(date)
|
||
.replaceAll(" ", "");
|
||
}
|
||
function trimDecimal(value) {
|
||
const asNumber = Number(value);
|
||
if (!Number.isFinite(asNumber))
|
||
return value;
|
||
return String(asNumber);
|
||
}
|
||
function setNavigationEnabled(enabled) {
|
||
previousProductButton.disabled = !enabled;
|
||
nextProductButton.disabled = !enabled;
|
||
}
|
||
function updateNavigationState() {
|
||
previousProductButton.disabled = !manufacturerSelect.value || currentPosition <= 1;
|
||
nextProductButton.disabled =
|
||
!manufacturerSelect.value || (totalPositions > 0 && currentPosition >= totalPositions);
|
||
loadProductButton.disabled = !currentProduct?.idProduct;
|
||
findSourcesButton.disabled = !manufacturerSelect.value || !currentProduct;
|
||
getPicturesButton.disabled = !manufacturerSelect.value || !currentProduct;
|
||
}
|
||
function normalizeScannedEan(value) {
|
||
const czechKeyboardDigits = new Map([
|
||
["+", "1"],
|
||
["ě", "2"],
|
||
["š", "3"],
|
||
["č", "4"],
|
||
["ř", "5"],
|
||
["ž", "6"],
|
||
["ý", "7"],
|
||
["á", "8"],
|
||
["í", "9"],
|
||
["é", "0"],
|
||
]);
|
||
return String(value ?? "")
|
||
.trim()
|
||
.toLowerCase()
|
||
.split("")
|
||
.map((char) => czechKeyboardDigits.get(char) || char)
|
||
.join("")
|
||
.replace(/\D/g, "");
|
||
}
|
||
function renderPicture(imageUrl) {
|
||
if (!imageUrl) {
|
||
pictureSlot.replaceChildren(Object.assign(document.createElement("span"), { textContent: "No picture" }));
|
||
return;
|
||
}
|
||
const image = document.createElement("img");
|
||
image.src = imageUrl;
|
||
image.alt = currentProduct?.name || "Product preview";
|
||
image.loading = "lazy";
|
||
pictureSlot.replaceChildren(image);
|
||
}
|
||
async function loadProductInfoForCurrentProduct() {
|
||
if (!currentProduct?.idProduct || !manufacturerSelect.value || !currentProduct.supplierReference) {
|
||
return;
|
||
}
|
||
loadProductButton.disabled = true;
|
||
loadProductButton.textContent = "Loading info...";
|
||
try {
|
||
const params = new URLSearchParams({
|
||
productId: currentProduct.idProduct,
|
||
manufacturerId: manufacturerSelect.value,
|
||
supplierReference: currentProduct.supplierReference,
|
||
});
|
||
const response = await fetch(`/api/catalog-maker/product-info?${params.toString()}`);
|
||
const data = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(data.message || "Product info load failed.");
|
||
}
|
||
const infoByCatalogId = new Map(data.items.map((item) => [String(item.idProductCatalog), item]));
|
||
currentCombinations = currentCombinations.map((combination) => ({
|
||
...combination,
|
||
...(infoByCatalogId.get(String(combination.id)) || {}),
|
||
}));
|
||
renderCombinations(currentCombinations);
|
||
}
|
||
catch (error) {
|
||
setTableMessage(error.message);
|
||
}
|
||
finally {
|
||
loadProductButton.textContent = "Get product info - 00";
|
||
updateNavigationState();
|
||
}
|
||
}
|
||
export {};
|