attr-extraction.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. // --- Data ---
  2. const mediaUrl = "./../";
  3. const PRODUCT_BASE = [
  4. { id: 1, item_id: 'SKU001', product_name: "Levi's Jeans", product_long_description: 'Classic blue denim jeans with straight fit.', product_short_description: 'Blue denim jeans.', product_type: 'Clothing', image_path: 'media/products/jeans.jpg', image: 'http://127.0.0.1:8000/media/products/jeans.png' },
  5. { id: 2, item_id: 'SKU002', product_name: 'Adidas Running Shoes', product_long_description: 'Lightweight running shoes with breathable mesh and cushioned sole.', product_short_description: "Men's running shoes.", product_type: 'Footwear', image_path: 'media/products/shoes.png', image: 'http://127.0.0.1:8000/media/products/shoes.png' },
  6. { id: 3, item_id: 'SKU003', product_name: 'Nike Sports T-Shirt', product_long_description: 'Moisture-wicking sports tee ideal for training and outdoor activities.', product_short_description: 'Performance t-shirt.', product_type: 'Clothing', image_path: 'media/products/tshirt.png', image: 'http://127.0.0.1:8000/media/products/tshirt.png' },
  7. { id: 4, item_id: 'SKU004', product_name: 'Puma Hoodie', product_long_description: 'Soft fleece hoodie with kangaroo pocket and adjustable drawstring.', product_short_description: 'Casual hoodie.', product_type: 'Clothing', image_path: 'media/products/hoodie.png', image: 'http://127.0.0.1:8000/media/products/hoodie.png' },
  8. { id: 5, item_id: 'SKU005', product_name: 'Ray-Ban Sunglasses', product_long_description: 'Classic aviator sunglasses with UV protection lenses.', product_short_description: 'Aviator sunglasses.', product_type: 'Accessories', image_path: 'media/products/sunglasses.png', image: 'http://127.0.0.1:8000/media/products/sunglasses.png' }
  9. ];
  10. const FAKE_API_RESPONSE = {
  11. results: [
  12. { product_id: 'SKU001', mandatory: { 'Clothing Neck Style': 'V-Neck', 'Clothing Top Style': 'Pullover', 'Condition': 'New', 'T-Shirt Type': 'Classic T-Shirt' }, additional: { 'Material': 'Turkish Pima Cotton', 'Size': 'Large', 'Color': 'Blue', 'Brand': 'Sierra', 'Fabric Type': 'Soft & Breathable', 'Fabric Composition': '95% Turkish Pima cotton', 'Care Instructions': 'Machine Washable', 'Sizes Available': 'S-XL' } },
  13. { product_id: 'SKU002', mandatory: { 'Shoe Type': 'Running', 'Closure': 'Lace-Up', 'Condition': 'New', 'Gender': 'Men' }, additional: { 'Upper Material': 'Engineered Mesh', 'Midsole': 'EVA Foam', 'Outsole': 'Rubber', 'Color': 'Black/White', 'Brand': 'Adidas', 'Size': 'UK 9', 'Care Instructions': 'Surface Clean' } },
  14. { product_id: 'SKU003', mandatory: { 'Clothing Neck Style': 'Crew Neck', 'Sleeve Length': 'Short Sleeve', 'Condition': 'New', 'T-Shirt Type': 'Performance' }, additional: { 'Material': 'Polyester Blend', 'Color': 'Red', 'Brand': 'Nike', 'Size': 'Medium', 'Fabric Technology': 'Dri-FIT', 'Care Instructions': 'Machine Wash Cold' } },
  15. { product_id: 'SKU004', mandatory: { 'Clothing Top Style': 'Hoodie', 'Closure': 'Pullover', 'Condition': 'New', 'Fit': 'Relaxed' }, additional: { 'Material': 'Cotton Fleece', 'Color': 'Charcoal', 'Brand': 'Puma', 'Size': 'Large', 'Care Instructions': 'Machine Wash Warm' } },
  16. { product_id: 'SKU005', mandatory: { 'Accessory Type': 'Sunglasses', 'Frame Style': 'Aviator', 'Condition': 'New', 'Lens Protection': 'UV 400' }, additional: { 'Frame Material': 'Metal', 'Lens Color': 'Green', 'Brand': 'Ray-Ban', 'Size': 'Standard', 'Case Included': 'Yes', 'Care Instructions': 'Clean with microfiber' } }
  17. ],
  18. total_products: 5,
  19. successful: 5,
  20. failed: 0
  21. };
  22. // --- State ---
  23. let selectedIds = new Set();
  24. const lastSeen = new Map(); // per-product memory for NEW highlighting (product_id -> maps)
  25. let layoutMode = 'cards'; // 'cards' | 'table'
  26. // --- Helpers ---
  27. const $ = (sel) => document.querySelector(sel);
  28. const el = (tag, cls) => { const e = document.createElement(tag); if (cls) e.className = cls; return e; }
  29. function updateSelectionInfo() {
  30. const pill = $('#selectionInfo');
  31. const total = PRODUCT_BASE.length;
  32. const count = selectedIds.size;
  33. pill.textContent = count === 0 ? 'No products selected' : `${count} of ${total} selected`;
  34. }
  35. function setChecked(id, checked) { if (checked) selectedIds.add(id); else selectedIds.delete(id); updateSelectionInfo(); }
  36. // --- Chips rendering ---
  37. function renderChips(container, obj, memoryMap) {
  38. container.innerHTML = '';
  39. let count = 0;
  40. Object.entries(obj || {}).forEach(([k, v]) => {
  41. const chip = el('span', 'chip');
  42. const kEl = el('span', 'k'); kEl.textContent = k + ':';
  43. const vEl = el('span', 'v'); vEl.textContent = ' ' + String(v);
  44. chip.appendChild(kEl); chip.appendChild(vEl);
  45. const was = memoryMap.get(k);
  46. if (was === undefined || was !== v) chip.classList.add('new');
  47. container.appendChild(chip);
  48. memoryMap.set(k, v);
  49. count++;
  50. });
  51. return count;
  52. }
  53. function findApiResultForProduct(p, index, api) { return api.results?.find(r => r.product_id === p.item_id) || api.results?.[index] || null; }
  54. // --- Cards layout ---
  55. function createProductCard(p) {
  56. const row = el('div', 'product');
  57. if (selectedIds.has(p.id)) row.classList.add('selected');
  58. const left = el('div', 'thumb');
  59. const img = new Image(); img.src = mediaUrl+p.image_path || p.image || '';
  60. img.alt = `${p.product_name} image`;
  61. console.log("img",img);
  62. img.onerror = () => { img.remove(); const fb = el('div', 'fallback'); fb.textContent = (p.product_name || 'Product').split(' ').map(w => w[0]).slice(0,2).join('').toUpperCase(); left.appendChild(fb); };
  63. left.appendChild(img);
  64. const mid = el('div', 'meta');
  65. const name = el('div', 'name'); name.textContent = p.product_name || '—';
  66. const desc = el('div', 'desc'); desc.textContent = p.product_short_description || '';
  67. const badges = el('div', 'badges');
  68. const sku = el('span', 'pill'); sku.textContent = `SKU: ${p.item_id || '—'}`; badges.appendChild(sku);
  69. const type = el('span', 'pill'); type.textContent = p.product_type || '—'; badges.appendChild(type);
  70. const long = el('div', 'desc'); long.textContent = p.product_long_description || ''; long.style.marginTop = '4px';
  71. mid.appendChild(name); mid.appendChild(desc); mid.appendChild(badges); mid.appendChild(long);
  72. const right = el('label', 'select');
  73. const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = selectedIds.has(p.id);
  74. cb.addEventListener('change', () => { setChecked(p.id, cb.checked); row.classList.toggle('selected', cb.checked); });
  75. const lbl = el('span'); lbl.textContent = 'Select';
  76. right.appendChild(cb); right.appendChild(lbl);
  77. // Inline attributes container (rendered on Submit)
  78. const inline = el('div', 'attr-inline');
  79. inline.dataset.pid = p.item_id; // use item_id for mapping
  80. row.addEventListener('click', (e) => { if (e.target.tagName.toLowerCase() !== 'input') { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); } });
  81. row.appendChild(left); row.appendChild(mid); row.appendChild(right);
  82. row.appendChild(inline);
  83. return row;
  84. }
  85. function renderProductsCards() {
  86. const cards = $('#cardsContainer');
  87. cards.innerHTML = '';
  88. PRODUCT_BASE.forEach(p => cards.appendChild(createProductCard(p)));
  89. }
  90. // --- Table layout ---
  91. function createMiniThumb(p) {
  92. const mt = el('div', 'mini-thumb');
  93. const img = new Image(); img.src = mediaUrl+p.image_path || p.image || ''; img.alt = `${p.product_name} image`;
  94. console.log("img",img);
  95. img.onerror = () => { img.remove(); const fb = el('div', 'fallback'); fb.textContent = (p.product_name || 'Product').split(' ').map(w => w[0]).slice(0,2).join('').toUpperCase(); mt.appendChild(fb); };
  96. mt.appendChild(img);
  97. return mt;
  98. }
  99. function renderProductsTable() {
  100. const wrap = $('#tableContainer');
  101. wrap.innerHTML = '';
  102. const table = el('table');
  103. const thead = el('thead'); const trh = el('tr');
  104. const headers = ['Select', 'Image', 'Product', 'SKU', 'Type', 'Short Description'];
  105. headers.forEach(h => { const th = el('th'); th.textContent = h; trh.appendChild(th); });
  106. thead.appendChild(trh); table.appendChild(thead);
  107. const tbody = el('tbody');
  108. PRODUCT_BASE.forEach(p => {
  109. const tr = el('tr'); tr.id = `row-${p.id}`;
  110. // Select cell
  111. const tdSel = el('td', 'select-cell'); const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = selectedIds.has(p.id);
  112. cb.addEventListener('change', () => { setChecked(p.id, cb.checked); tr.classList.toggle('selected', cb.checked); }); tdSel.appendChild(cb); tr.appendChild(tdSel);
  113. // Image
  114. const tdImg = el('td', 'thumb-cell'); tdImg.appendChild(createMiniThumb(p)); tr.appendChild(tdImg);
  115. // Product name
  116. const tdName = el('td'); tdName.textContent = p.product_name || '—'; tr.appendChild(tdName);
  117. // SKU
  118. const tdSku = el('td'); tdSku.textContent = p.item_id || '—'; tr.appendChild(tdSku);
  119. // Type
  120. const tdType = el('td'); const b = el('span', 'badge'); b.textContent = p.product_type || '—'; tdType.appendChild(b); tr.appendChild(tdType);
  121. // Short description
  122. const tdDesc = el('td'); tdDesc.textContent = p.product_short_description || ''; tr.appendChild(tdDesc);
  123. tr.addEventListener('click', (e) => { if (e.target.tagName.toLowerCase() !== 'input') { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); } });
  124. tbody.appendChild(tr);
  125. });
  126. table.appendChild(tbody);
  127. wrap.appendChild(table);
  128. }
  129. // --- Inline attributes rendering ---
  130. function renderInlineForCards() {
  131. const api = FAKE_API_RESPONSE;
  132. // Clear all inline sections first
  133. document.querySelectorAll('.attr-inline').forEach(div => div.innerHTML = '');
  134. PRODUCT_BASE.forEach((p, idx) => {
  135. const inline = document.querySelector(`.attr-inline[data-pid="${p.item_id}"]`);
  136. if (!inline) return;
  137. if (!selectedIds.has(p.id)) return; // only show for selected
  138. const res = findApiResultForProduct(p, idx, api);
  139. const pid = p.item_id;
  140. if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map() });
  141. const mem = lastSeen.get(pid);
  142. // Build sections
  143. const manTitle = el('div', 'section-title'); manTitle.innerHTML = '<strong>Mandatory</strong>';
  144. const manChips = el('div', 'chips');
  145. const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
  146. const addChips = el('div', 'chips');
  147. const mandCount = renderChips(manChips, res?.mandatory || {}, mem.mandatory);
  148. const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
  149. const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
  150. const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
  151. const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
  152. counts.appendChild(c1); counts.appendChild(c2);
  153. inline.appendChild(manTitle); inline.appendChild(manChips);
  154. inline.appendChild(addTitle); inline.appendChild(addChips);
  155. inline.appendChild(counts);
  156. });
  157. // Update summary
  158. $('#statTotal').textContent = api.total_products ?? 0;
  159. $('#statOk').textContent = api.successful ?? 0;
  160. $('#statKo').textContent = api.failed ?? 0;
  161. }
  162. function renderInlineForTable() {
  163. const api = FAKE_API_RESPONSE;
  164. const table = $('#tableContainer');
  165. if (!table) return;
  166. // Remove existing detail rows
  167. table.querySelectorAll('tr.detail-row').forEach(r => r.remove());
  168. PRODUCT_BASE.forEach((p, idx) => {
  169. if (!selectedIds.has(p.id)) return;
  170. const res = findApiResultForProduct(p, idx, api);
  171. const pid = p.item_id;
  172. if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map() });
  173. const mem = lastSeen.get(pid);
  174. const tbody = table.querySelector('tbody');
  175. const baseRow = tbody.querySelector(`#row-${p.id}`);
  176. if (!baseRow) return;
  177. const detail = el('tr', 'detail-row');
  178. const td = el('td'); td.colSpan = 6; // number of columns
  179. const content = el('div', 'detail-content');
  180. const manTitle = el('div', 'section-title'); manTitle.innerHTML = '<strong>Mandatory</strong>';
  181. const manChips = el('div', 'chips');
  182. const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
  183. const addChips = el('div', 'chips');
  184. const mandCount = renderChips(manChips, res?.mandatory || {}, mem.mandatory);
  185. const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
  186. const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
  187. const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
  188. const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
  189. counts.appendChild(c1); counts.appendChild(c2);
  190. content.appendChild(manTitle); content.appendChild(manChips);
  191. content.appendChild(addTitle); content.appendChild(addChips);
  192. content.appendChild(counts);
  193. td.appendChild(content); detail.appendChild(td);
  194. // insert after base row
  195. baseRow.insertAdjacentElement('afterend', detail);
  196. });
  197. // Update summary
  198. $('#statTotal').textContent = api.total_products ?? 0;
  199. $('#statOk').textContent = api.successful ?? 0;
  200. $('#statKo').textContent = api.failed ?? 0;
  201. }
  202. function renderInlineAttributes() {
  203. if (layoutMode === 'cards') renderInlineForCards(); else renderInlineForTable();
  204. }
  205. // --- Main rendering ---
  206. function renderProducts() {
  207. if (layoutMode === 'cards') {
  208. $('#cardsContainer').style.display = '';
  209. $('#tableContainer').style.display = 'none';
  210. renderProductsCards();
  211. } else {
  212. $('#cardsContainer').style.display = 'none';
  213. $('#tableContainer').style.display = '';
  214. renderProductsTable();
  215. }
  216. updateSelectionInfo();
  217. // If there is a selection, re-render inline attributes (persist across toggle)
  218. if (selectedIds.size > 0) renderInlineAttributes();
  219. }
  220. // --- Submit & Reset ---
  221. function submitAttributes() {
  222. if (selectedIds.size === 0) { alert('Please select at least one product.'); return; }
  223. renderInlineAttributes();
  224. }
  225. function resetAll() {
  226. selectedIds.clear();
  227. lastSeen.clear();
  228. renderProducts();
  229. // Clear summary
  230. document.getElementById('statTotal').textContent = '0';
  231. document.getElementById('statOk').textContent = '0';
  232. document.getElementById('statKo').textContent = '0';
  233. }
  234. function setLayout(mode) {
  235. layoutMode = mode;
  236. const btnCards = document.getElementById('btnCards');
  237. const btnTable = document.getElementById('btnTable');
  238. if (mode === 'cards') { btnCards.classList.add('active'); btnCards.setAttribute('aria-selected', 'true'); btnTable.classList.remove('active'); btnTable.setAttribute('aria-selected', 'false'); }
  239. else { btnTable.classList.add('active'); btnTable.setAttribute('aria-selected', 'true'); btnCards.classList.remove('active'); btnCards.setAttribute('aria-selected', 'false'); }
  240. renderProducts();
  241. }
  242. // --- Wire up ---
  243. document.addEventListener('DOMContentLoaded', () => {
  244. renderProducts();
  245. document.getElementById('btnSubmit').addEventListener('click', submitAttributes);
  246. document.getElementById('btnReset').addEventListener('click', resetAll);
  247. document.getElementById('btnSelectAll').addEventListener('click', () => {
  248. if (selectedIds.size === PRODUCT_BASE.length) { selectedIds.clear(); } else { selectedIds = new Set(PRODUCT_BASE.map(p => p.id)); }
  249. renderProducts();
  250. });
  251. document.getElementById('btnCards').addEventListener('click', () => setLayout('cards'));
  252. document.getElementById('btnTable').addEventListener('click', () => setLayout('table'));
  253. });