Check that response is ok and files is an array before processing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
712 lines
26 KiB
HTML
712 lines
26 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>H&M EMS Review Tool</title>
|
|
<style>
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
background: #f0f0f0; color: #333; font-size: 13px;
|
|
}
|
|
|
|
.header {
|
|
background: #333; color: #fff; padding: 0 20px; height: 44px;
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
}
|
|
.header-left { display: flex; align-items: center; gap: 16px; }
|
|
.header-brand { font-size: 18px; font-weight: 700; color: #cc0000; letter-spacing: 1px; }
|
|
.header-campaign { font-size: 14px; font-weight: 500; }
|
|
.header-campaign span { color: #aaa; font-weight: 400; margin-left: 6px; }
|
|
.header-right { display: flex; align-items: center; gap: 10px; }
|
|
.header-meta { font-size: 10px; color: #999; text-align: right; line-height: 1.4; }
|
|
|
|
.file-selector {
|
|
padding: 4px 8px; font-size: 12px; border: 1px solid #666;
|
|
border-radius: 3px; background: #555; color: #fff; cursor: pointer;
|
|
}
|
|
.file-selector option { background: #444; color: #fff; }
|
|
|
|
.btn {
|
|
padding: 6px 14px; border: none; border-radius: 3px; cursor: pointer;
|
|
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
|
letter-spacing: 0.5px; transition: background 0.2s;
|
|
}
|
|
.btn-export { background: #cc0000; color: #fff; }
|
|
.btn-export:hover { background: #a30000; }
|
|
.btn-export:disabled { background: #888; cursor: default; }
|
|
|
|
.btn-logout { background: #555; color: #fff; }
|
|
.btn-logout:hover { background: #333; }
|
|
|
|
.toolbar {
|
|
background: #fff; border-bottom: 1px solid #ddd; padding: 0 20px; height: 38px;
|
|
display: flex; align-items: center; gap: 12px;
|
|
}
|
|
.toolbar .stats {
|
|
margin-left: auto; font-size: 11px; color: #888; white-space: nowrap;
|
|
}
|
|
.toolbar .stats strong { color: #cc0000; }
|
|
.approved-count { color: #4caf50; }
|
|
|
|
.sticky-top { position: sticky; top: 0; z-index: 100; }
|
|
.table-container { padding: 0 0 40px; overflow-x: auto; }
|
|
|
|
.loading-overlay {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(255,255,255,0.85); display: flex;
|
|
align-items: center; justify-content: center; z-index: 200;
|
|
font-size: 16px; color: #666;
|
|
}
|
|
.loading-overlay.hidden { display: none; }
|
|
|
|
.product-table {
|
|
border-collapse: collapse; background: #fff;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1); min-width: 100%;
|
|
}
|
|
.product-table thead th {
|
|
background: #333; color: #fff; font-size: 10px; font-weight: 600;
|
|
text-transform: uppercase; letter-spacing: 0.4px; padding: 6px 8px;
|
|
text-align: left; white-space: nowrap; border-right: 1px solid #444;
|
|
}
|
|
.product-table thead th:last-child { border-right: none; }
|
|
.product-table thead th.lang-group-header {
|
|
background: #3a3a3a; text-align: center; padding: 5px 6px;
|
|
border-left: 2px solid #666;
|
|
}
|
|
.product-table thead th.lang-group-header.empty-col { background: #2a2a2a; }
|
|
.product-table thead th.lang-sub-header {
|
|
background: #484848; font-size: 9px; padding: 4px 6px;
|
|
}
|
|
.product-table thead th.lang-sub-header-first { border-left: 2px solid #666; }
|
|
|
|
.product-table tbody td {
|
|
padding: 5px 8px; border-bottom: 1px solid #e5e5e5;
|
|
border-right: 1px solid #e5e5e5; vertical-align: middle; font-size: 12px;
|
|
}
|
|
.product-table tbody td:last-child { border-right: none; }
|
|
.product-table tbody td.lang-group-first { border-left: 2px solid #ddd; }
|
|
.product-table tbody td.empty-cell { background: #fafafa; }
|
|
.product-table tbody tr:nth-child(even) { background: #f5f5f5; }
|
|
.product-table tbody tr:nth-child(even) td.empty-cell { background: #f2f2f2; }
|
|
.product-table tbody tr:hover { background: #eef4ff; }
|
|
|
|
.col-visual { width: 90px; text-align: center; }
|
|
.col-article { width: 90px; font-family: 'SF Mono','Consolas',monospace; font-size: 11px; }
|
|
.col-gb-name { width: 160px; font-size: 12px; }
|
|
|
|
.image-group { display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; }
|
|
.product-image { max-width: 80px; max-height: 80px; object-fit: contain; border-radius: 2px; cursor: pointer; }
|
|
.product-image:hover { opacity: 0.8; }
|
|
.image-placeholder {
|
|
width: 60px; height: 60px; background: #e0e0e0; border-radius: 2px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
color: #aaa; font-size: 9px;
|
|
}
|
|
|
|
.image-modal-overlay {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.7); z-index: 300;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.image-modal-overlay.hidden { display: none; }
|
|
.image-modal {
|
|
background: #fff; border-radius: 8px; padding: 16px;
|
|
max-width: 90vw; max-height: 90vh; display: flex;
|
|
flex-direction: column; align-items: center; gap: 12px;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
|
}
|
|
.image-modal img {
|
|
max-width: 80vw; max-height: 75vh; object-fit: contain; border-radius: 4px;
|
|
}
|
|
.image-modal-close {
|
|
padding: 8px 24px; border: none; border-radius: 4px; cursor: pointer;
|
|
font-size: 13px; font-weight: 600; background: #333; color: #fff;
|
|
transition: background 0.2s;
|
|
}
|
|
.image-modal-close:hover { background: #cc0000; }
|
|
|
|
.edit-input {
|
|
width: 100%; padding: 3px 6px; border: 1px solid #ddd; border-radius: 2px;
|
|
font-size: 12px; font-family: inherit; background: #fff;
|
|
transition: border-color 0.15s, background 0.15s;
|
|
}
|
|
.edit-input:focus {
|
|
outline: none; border-color: #cc0000;
|
|
box-shadow: 0 0 0 2px rgba(204,0,0,0.1);
|
|
}
|
|
.edit-input.changed { background: #fff8e1; border-color: #f5a623; }
|
|
.edit-input.differs-from-gb { background: #fffde7; }
|
|
.edit-input.changed.differs-from-gb { background: #fff3cd; border-color: #f5a623; }
|
|
.edit-input:disabled {
|
|
background: #f0f0f0; color: #999; border-color: #e0e0e0; cursor: not-allowed;
|
|
}
|
|
|
|
.approve-btn {
|
|
width: 24px; height: 24px; border-radius: 50%; border: 2px solid #ccc;
|
|
background: #fff; cursor: pointer; display: inline-flex;
|
|
align-items: center; justify-content: center; transition: all 0.15s;
|
|
font-size: 13px; color: #ccc; padding: 0; line-height: 1;
|
|
}
|
|
.approve-btn:hover { border-color: #4caf50; color: #4caf50; }
|
|
.approve-btn.approved { background: #4caf50; border-color: #4caf50; color: #fff; }
|
|
.approve-btn.saving { opacity: 0.5; pointer-events: none; }
|
|
.col-approve { width: 34px; text-align: center; }
|
|
|
|
.lang-header-controls { display: inline-flex; align-items: center; gap: 6px; }
|
|
.lang-header-controls select {
|
|
padding: 3px 4px; font-size: 11px; border: 1px solid #666;
|
|
border-radius: 2px; background: #555; color: #fff; max-width: 180px;
|
|
}
|
|
.lang-header-controls select option { background: #444; color: #fff; }
|
|
.lang-remove-btn {
|
|
width: 18px; height: 18px; border-radius: 50%; border: 1px solid #888;
|
|
background: transparent; color: #aaa; cursor: pointer; font-size: 14px;
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
padding: 0; line-height: 1; transition: all 0.15s;
|
|
}
|
|
.lang-remove-btn:hover { background: #cc0000; border-color: #cc0000; color: #fff; }
|
|
|
|
.change-count {
|
|
display: inline-block; background: #cc0000; color: #fff; border-radius: 10px;
|
|
padding: 1px 6px; font-size: 10px; font-weight: 600; margin-left: 4px;
|
|
min-width: 16px; text-align: center;
|
|
}
|
|
.change-count.hidden { display: none; }
|
|
|
|
.save-flash { animation: flashGreen 0.6s ease; }
|
|
@keyframes flashGreen {
|
|
0% { box-shadow: 0 0 0 0 rgba(76,175,80,0.6); }
|
|
50% { box-shadow: 0 0 0 6px rgba(76,175,80,0); }
|
|
100% { box-shadow: none; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="loadingOverlay" class="loading-overlay">Loading...</div>
|
|
|
|
<div id="imageModal" class="image-modal-overlay hidden" onclick="closeImageModal(event)">
|
|
<div class="image-modal">
|
|
<img id="imageModalImg" src="" alt="Preview">
|
|
<button class="image-modal-close" onclick="closeImageModal()">Close</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="sticky-top">
|
|
<div class="header">
|
|
<div class="header-left">
|
|
<div class="header-brand">H&M</div>
|
|
<select class="file-selector" id="fileSelector" onchange="onFileChange()"></select>
|
|
<div class="header-campaign" id="campaignInfo"></div>
|
|
</div>
|
|
<div class="header-right">
|
|
<div class="header-meta" id="headerMeta"></div>
|
|
<button class="btn btn-export" id="exportBtn" onclick="exportChanges()" disabled>
|
|
Export Changes <span class="change-count hidden" id="changeBadge">0</span>
|
|
</button>
|
|
<a href="/logout" class="btn btn-logout" style="text-decoration: none;">Logout</a>
|
|
</div>
|
|
</div>
|
|
<div class="toolbar">
|
|
<div class="stats">
|
|
<span id="productCount">0</span> products ·
|
|
<span id="langCount">0</span> languages ·
|
|
<strong><span id="editCount">0</span> edits</strong> ·
|
|
<span class="approved-count"><span id="approvedCount">0</span> approved</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<table class="product-table" id="productTable">
|
|
<thead id="tableHead"></thead>
|
|
<tbody id="tableBody"></tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<script>
|
|
// ========== State ==========
|
|
var RAW_DATA = [];
|
|
var IMAGE_MAP = {};
|
|
var LANG_DISPLAY = {};
|
|
var currentFilename = "";
|
|
var MAX_LANG_COLS = 4;
|
|
|
|
var dataByArticle = {};
|
|
var articleOrder = [];
|
|
var langColumns = [];
|
|
var changes = {};
|
|
var approvals = {};
|
|
|
|
var ALL_LANGS = [];
|
|
|
|
function langOptionsHtml(selectedLang) {
|
|
var h = '<option value="">-- Select Language --</option>';
|
|
ALL_LANGS.forEach(function(l) {
|
|
var sel = (l === selectedLang) ? " selected" : "";
|
|
h += '<option value="' + l + '"' + sel + '>' + LANG_DISPLAY[l] + '</option>';
|
|
});
|
|
return h;
|
|
}
|
|
|
|
function filledCount() {
|
|
var n = 0;
|
|
langColumns.forEach(function(l) { if (l !== "") n++; });
|
|
return n;
|
|
}
|
|
|
|
function ensureTrailingEmpty() {
|
|
while (langColumns.length > 1 && langColumns[langColumns.length - 1] === "" && langColumns[langColumns.length - 2] === "") {
|
|
langColumns.pop();
|
|
}
|
|
if (langColumns.length < MAX_LANG_COLS && (langColumns.length === 0 || langColumns[langColumns.length - 1] !== "")) {
|
|
langColumns.push("");
|
|
}
|
|
}
|
|
|
|
// ========== Init ==========
|
|
async function init() {
|
|
try {
|
|
var res = await fetch("/hm-ems-report/api/files");
|
|
if (res.status === 401) {
|
|
window.location.href = "/login";
|
|
return;
|
|
}
|
|
if (!res.ok) {
|
|
throw new Error("Failed to load files");
|
|
}
|
|
var files = await res.json();
|
|
|
|
if (!Array.isArray(files)) {
|
|
throw new Error("Invalid response format");
|
|
}
|
|
|
|
var sel = document.getElementById("fileSelector");
|
|
sel.innerHTML = "";
|
|
files.forEach(function(f) {
|
|
var opt = document.createElement("option");
|
|
opt.value = f.filename;
|
|
opt.textContent = f.campaign + " (" + f.filename + ")";
|
|
sel.appendChild(opt);
|
|
});
|
|
|
|
if (files.length > 0) {
|
|
await loadCampaign(files[0].filename);
|
|
}
|
|
} catch (err) {
|
|
console.error("Init error:", err);
|
|
document.getElementById("loadingOverlay").textContent = "Error loading data: " + err.message;
|
|
}
|
|
}
|
|
|
|
async function loadCampaign(filename) {
|
|
showLoading(true);
|
|
try {
|
|
var res = await fetch("/hm-ems-report/api/load/" + encodeURIComponent(filename));
|
|
if (res.status === 401) {
|
|
window.location.href = "/login";
|
|
return;
|
|
}
|
|
var payload = await res.json();
|
|
|
|
RAW_DATA = payload.data;
|
|
IMAGE_MAP = payload.image_map;
|
|
LANG_DISPLAY = payload.lang_display;
|
|
currentFilename = payload.filename;
|
|
approvals = payload.approvals || {};
|
|
|
|
ALL_LANGS = Object.keys(LANG_DISPLAY).filter(function(l) { return l !== "en-gb"; }).sort();
|
|
|
|
// Update header
|
|
document.getElementById("campaignInfo").innerHTML =
|
|
"Campaign " + payload.campaign + '<span>Season ' + payload.season + '</span>';
|
|
document.getElementById("headerMeta").innerHTML =
|
|
"Source: " + payload.filename + "<br>Loaded: " + new Date().toLocaleString();
|
|
|
|
// Reset state
|
|
dataByArticle = {};
|
|
articleOrder = [];
|
|
langColumns = [""];
|
|
changes = {};
|
|
|
|
RAW_DATA.forEach(function(rec) {
|
|
var artId = rec["Article id"];
|
|
var lang = (rec["Language"] || "").toLowerCase();
|
|
if (!dataByArticle[artId]) {
|
|
dataByArticle[artId] = {};
|
|
articleOrder.push(artId);
|
|
}
|
|
dataByArticle[artId][lang] = rec;
|
|
});
|
|
|
|
document.getElementById("productCount").textContent = articleOrder.length;
|
|
document.getElementById("langCount").textContent = Object.keys(LANG_DISPLAY).length;
|
|
|
|
rebuildAll();
|
|
updateEditCount();
|
|
} catch (err) {
|
|
console.error("Load error:", err);
|
|
alert("Failed to load campaign: " + err.message);
|
|
}
|
|
showLoading(false);
|
|
}
|
|
|
|
function showLoading(show) {
|
|
document.getElementById("loadingOverlay").classList.toggle("hidden", !show);
|
|
}
|
|
|
|
async function onFileChange() {
|
|
var filename = document.getElementById("fileSelector").value;
|
|
if (filename) await loadCampaign(filename);
|
|
}
|
|
|
|
// ========== Full rebuild ==========
|
|
function rebuildAll() {
|
|
buildHead();
|
|
buildBody();
|
|
populateAllColumns();
|
|
updateApprovedCount();
|
|
}
|
|
|
|
function buildHead() {
|
|
var thead = document.getElementById("tableHead");
|
|
var r1 = '<tr>';
|
|
r1 += '<th class="col-visual" rowspan="2">Visual</th>';
|
|
r1 += '<th class="col-article" rowspan="2">Article ID</th>';
|
|
r1 += '<th class="col-gb-name" rowspan="2">Product Name (English)</th>';
|
|
|
|
langColumns.forEach(function(lang, idx) {
|
|
var isEmpty = (lang === "");
|
|
var cls = "lang-group-header" + (isEmpty ? " empty-col" : "");
|
|
var showRemove = !isEmpty && filledCount() > 1;
|
|
r1 += '<th class="' + cls + '" colspan="3">';
|
|
r1 += '<div class="lang-header-controls">';
|
|
r1 += '<select onchange="onLangColChange(' + idx + ', this.value)">' + langOptionsHtml(lang) + '</select>';
|
|
if (showRemove) {
|
|
r1 += '<button class="lang-remove-btn" onclick="removeLanguageColumn(' + idx + ')" title="Remove">−</button>';
|
|
}
|
|
r1 += '</div></th>';
|
|
});
|
|
r1 += '</tr>';
|
|
|
|
var r2 = '<tr>';
|
|
langColumns.forEach(function(lang) {
|
|
var isEmpty = (lang === "");
|
|
r2 += '<th class="lang-sub-header lang-sub-header-first">' + (isEmpty ? "" : "Product Name") + '</th>';
|
|
r2 += '<th class="lang-sub-header">' + (isEmpty ? "" : "Price") + '</th>';
|
|
r2 += '<th class="lang-sub-header col-approve">' + (isEmpty ? "" : "OK") + '</th>';
|
|
});
|
|
r2 += '</tr>';
|
|
|
|
thead.innerHTML = r1 + r2;
|
|
}
|
|
|
|
function buildBody() {
|
|
var tbody = document.getElementById("tableBody");
|
|
var html = "";
|
|
|
|
articleOrder.forEach(function(artId) {
|
|
var gbRec = dataByArticle[artId]["en-gb"];
|
|
var gbName = gbRec ? (gbRec["Product name"] || "") : "";
|
|
var imgPaths = IMAGE_MAP[artId];
|
|
|
|
var imgHtml;
|
|
if (imgPaths && imgPaths.length > 0) {
|
|
imgHtml = '<div class="image-group">';
|
|
imgPaths.forEach(function(p) {
|
|
imgHtml += '<img class="product-image" src="' + p + '" alt="' + artId + '" loading="lazy" onclick="openImageModal(this.src)">';
|
|
});
|
|
imgHtml += '</div>';
|
|
} else {
|
|
imgHtml = '<div class="image-placeholder">No Image</div>';
|
|
}
|
|
|
|
html += '<tr data-article="' + artId + '" id="row_' + artId + '">';
|
|
html += '<td class="col-visual">' + imgHtml + '</td>';
|
|
html += '<td class="col-article">' + artId + '</td>';
|
|
html += '<td class="col-gb-name">' + escapeHtml(gbName) + '</td>';
|
|
|
|
langColumns.forEach(function(lang, idx) {
|
|
var cid = idx + '_' + artId;
|
|
if (lang === "") {
|
|
html += '<td class="lang-group-first empty-cell"></td>';
|
|
html += '<td class="empty-cell"></td>';
|
|
html += '<td class="col-approve empty-cell"></td>';
|
|
} else {
|
|
html += '<td class="lang-group-first"><input type="text" class="edit-input" id="name_' + cid + '" data-article="' + artId + '" data-field="Product name" data-colidx="' + idx + '" onchange="onEdit(this)" oninput="onInputChange(this)"></td>';
|
|
html += '<td><input type="text" class="edit-input" id="price_' + cid + '" data-article="' + artId + '" data-field="Price" data-colidx="' + idx + '" onchange="onEdit(this)" oninput="onInputChange(this)"></td>';
|
|
html += '<td class="col-approve"><button class="approve-btn" id="approve_' + cid + '" onclick="toggleApprove(\x27' + artId + '\x27,' + idx + ')" title="Approve">✓</button></td>';
|
|
}
|
|
});
|
|
|
|
html += '</tr>';
|
|
});
|
|
|
|
tbody.innerHTML = html;
|
|
}
|
|
|
|
// ========== Populate ==========
|
|
function populateAllColumns() {
|
|
langColumns.forEach(function(lang, idx) {
|
|
if (lang !== "") populateColumn(idx);
|
|
});
|
|
}
|
|
|
|
function populateColumn(idx) {
|
|
var lang = langColumns[idx];
|
|
if (!lang) return;
|
|
|
|
articleOrder.forEach(function(artId) {
|
|
var rec = dataByArticle[artId][lang];
|
|
var gbRec = dataByArticle[artId]["en-gb"];
|
|
var cid = idx + '_' + artId;
|
|
|
|
var nameInput = document.getElementById("name_" + cid);
|
|
var priceInput = document.getElementById("price_" + cid);
|
|
var approveBtn = document.getElementById("approve_" + cid);
|
|
if (!nameInput || !priceInput) return;
|
|
|
|
var targetName = rec ? (rec["Product name"] || "") : "";
|
|
var targetPrice = rec ? (rec["Price"] || "") : "";
|
|
var gbName = gbRec ? (gbRec["Product name"] || "") : "";
|
|
|
|
var nameChangeKey = artId + "::" + lang + "::Product name";
|
|
var priceChangeKey = artId + "::" + lang + "::Price";
|
|
var approveKey = artId + "::" + lang;
|
|
var isApproved = !!approvals[approveKey];
|
|
|
|
nameInput.value = changes[nameChangeKey] ? changes[nameChangeKey]["New value"] : targetName;
|
|
priceInput.value = changes[priceChangeKey] ? changes[priceChangeKey]["New value"] : targetPrice;
|
|
|
|
nameInput.classList.toggle("changed", !!changes[nameChangeKey]);
|
|
priceInput.classList.toggle("changed", !!changes[priceChangeKey]);
|
|
|
|
nameInput.setAttribute("data-original", targetName);
|
|
priceInput.setAttribute("data-original", targetPrice);
|
|
nameInput.setAttribute("data-lang", lang);
|
|
priceInput.setAttribute("data-lang", lang);
|
|
|
|
var differs = targetName !== "" && targetName.toUpperCase() !== gbName.toUpperCase();
|
|
nameInput.classList.toggle("differs-from-gb", differs);
|
|
|
|
nameInput.disabled = isApproved;
|
|
priceInput.disabled = isApproved;
|
|
if (approveBtn) approveBtn.classList.toggle("approved", isApproved);
|
|
});
|
|
}
|
|
|
|
// ========== Language columns ==========
|
|
function onLangColChange(idx, newLang) {
|
|
langColumns[idx] = newLang;
|
|
ensureTrailingEmpty();
|
|
rebuildAll();
|
|
}
|
|
|
|
function removeLanguageColumn(idx) {
|
|
langColumns.splice(idx, 1);
|
|
if (langColumns.length === 0) langColumns.push("");
|
|
ensureTrailingEmpty();
|
|
rebuildAll();
|
|
}
|
|
|
|
// ========== Editing ==========
|
|
function onInputChange(el) {
|
|
var original = el.getAttribute("data-original");
|
|
el.classList.toggle("changed", el.value !== original);
|
|
}
|
|
|
|
function onEdit(el) {
|
|
var artId = el.getAttribute("data-article");
|
|
var field = el.getAttribute("data-field");
|
|
var lang = el.getAttribute("data-lang");
|
|
var original = el.getAttribute("data-original");
|
|
var newVal = el.value;
|
|
var changeKey = artId + "::" + lang + "::" + field;
|
|
|
|
if (newVal !== original) {
|
|
changes[changeKey] = {
|
|
"Article id": artId,
|
|
"Language": lang,
|
|
"Field": field,
|
|
"Original value": original,
|
|
"New value": newVal,
|
|
"Country": getCountryForArticleLang(artId, lang),
|
|
"Product id": getProductIdForArticle(artId),
|
|
};
|
|
el.classList.add("changed");
|
|
} else {
|
|
delete changes[changeKey];
|
|
el.classList.remove("changed");
|
|
}
|
|
updateEditCount();
|
|
}
|
|
|
|
// ========== Approve with server save ==========
|
|
async function toggleApprove(artId, colIdx) {
|
|
var lang = langColumns[colIdx];
|
|
if (!lang) return;
|
|
|
|
var approveKey = artId + "::" + lang;
|
|
var cid = colIdx + '_' + artId;
|
|
var btn = document.getElementById("approve_" + cid);
|
|
var nameInput = document.getElementById("name_" + cid);
|
|
var priceInput = document.getElementById("price_" + cid);
|
|
|
|
if (approvals[approveKey]) {
|
|
// Un-approve
|
|
btn.classList.add("saving");
|
|
try {
|
|
var res = await fetch("/hm-ems-report/api/approve", {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({
|
|
filename: currentFilename,
|
|
article_id: artId,
|
|
language: lang,
|
|
action: "unapprove",
|
|
edits: {}
|
|
})
|
|
});
|
|
if (!res.ok) throw new Error("Server error");
|
|
|
|
delete approvals[approveKey];
|
|
btn.classList.remove("approved");
|
|
if (nameInput) nameInput.disabled = false;
|
|
if (priceInput) priceInput.disabled = false;
|
|
} catch (err) {
|
|
alert("Failed to save: " + err.message);
|
|
}
|
|
btn.classList.remove("saving");
|
|
} else {
|
|
// Approve — collect any pending edits for this article+lang
|
|
var edits = {};
|
|
var nameChangeKey = artId + "::" + lang + "::Product name";
|
|
var priceChangeKey = artId + "::" + lang + "::Price";
|
|
|
|
if (nameInput) edits["Product name"] = nameInput.value;
|
|
if (priceInput) edits["Price"] = priceInput.value;
|
|
|
|
btn.classList.add("saving");
|
|
try {
|
|
var res = await fetch("/hm-ems-report/api/approve", {
|
|
method: "POST",
|
|
headers: {"Content-Type": "application/json"},
|
|
body: JSON.stringify({
|
|
filename: currentFilename,
|
|
article_id: artId,
|
|
language: lang,
|
|
action: "approve",
|
|
edits: edits
|
|
})
|
|
});
|
|
if (!res.ok) throw new Error("Server error");
|
|
|
|
approvals[approveKey] = true;
|
|
btn.classList.add("approved");
|
|
if (nameInput) nameInput.disabled = true;
|
|
if (priceInput) priceInput.disabled = true;
|
|
|
|
// Update the local data so the "original" reflects saved state
|
|
var rec = dataByArticle[artId] && dataByArticle[artId][lang];
|
|
if (rec) {
|
|
if (edits["Product name"] != null) rec["Product name"] = edits["Product name"];
|
|
if (edits["Price"] != null) rec["Price"] = edits["Price"];
|
|
}
|
|
if (nameInput) {
|
|
nameInput.setAttribute("data-original", nameInput.value);
|
|
nameInput.classList.remove("changed");
|
|
}
|
|
if (priceInput) {
|
|
priceInput.setAttribute("data-original", priceInput.value);
|
|
priceInput.classList.remove("changed");
|
|
}
|
|
|
|
// Clear changes for this article+lang since they're now saved
|
|
delete changes[nameChangeKey];
|
|
delete changes[priceChangeKey];
|
|
updateEditCount();
|
|
|
|
// Visual flash
|
|
btn.classList.add("save-flash");
|
|
setTimeout(function() { btn.classList.remove("save-flash"); }, 600);
|
|
} catch (err) {
|
|
alert("Failed to save: " + err.message);
|
|
}
|
|
btn.classList.remove("saving");
|
|
}
|
|
updateApprovedCount();
|
|
}
|
|
|
|
// ========== Helpers ==========
|
|
function getCountryForArticleLang(artId, lang) {
|
|
var rec = dataByArticle[artId] && dataByArticle[artId][lang];
|
|
return rec ? (rec["Country"] || "") : "";
|
|
}
|
|
function getProductIdForArticle(artId) {
|
|
var rec = dataByArticle[artId] && dataByArticle[artId]["en-gb"];
|
|
return rec ? (rec["Product id"] || "") : "";
|
|
}
|
|
|
|
function updateEditCount() {
|
|
var count = Object.keys(changes).length;
|
|
document.getElementById("editCount").textContent = count;
|
|
var badge = document.getElementById("changeBadge");
|
|
var btn = document.getElementById("exportBtn");
|
|
if (count > 0) {
|
|
badge.textContent = count;
|
|
badge.classList.remove("hidden");
|
|
btn.disabled = false;
|
|
} else {
|
|
badge.classList.add("hidden");
|
|
btn.disabled = true;
|
|
}
|
|
}
|
|
|
|
function updateApprovedCount() {
|
|
document.getElementById("approvedCount").textContent = Object.keys(approvals).length;
|
|
}
|
|
|
|
function exportChanges() {
|
|
var changeList = Object.values(changes);
|
|
if (changeList.length === 0) return;
|
|
changeList.forEach(function(c) {
|
|
c["Approved"] = !!approvals[c["Article id"] + "::" + c["Language"]];
|
|
});
|
|
var exportData = {
|
|
"export_date": new Date().toISOString(),
|
|
"source_file": currentFilename,
|
|
"total_changes": changeList.length,
|
|
"changes": changeList
|
|
};
|
|
var blob = new Blob([JSON.stringify(exportData, null, 2)], {type: "application/json"});
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "ems_changes_" + new Date().toISOString().slice(0,19).replace(/[:T]/g,"_") + ".json";
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function openImageModal(src) {
|
|
document.getElementById("imageModalImg").src = src;
|
|
document.getElementById("imageModal").classList.remove("hidden");
|
|
}
|
|
function closeImageModal(event) {
|
|
if (event && event.target !== document.getElementById("imageModal") && event.type === "click") return;
|
|
document.getElementById("imageModal").classList.add("hidden");
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return "";
|
|
return str.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", init);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|