attr-extraction.js 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144
  1. jQuery.noConflict(); // Release $ to other libraries
  2. // console.log(typeof jQuery);
  3. // $ = jQuery;
  4. // --- Config ---
  5. const UPLOAD_API_URL = '/attr/products/upload-excel/'; // TODO: set to your upload endpoint
  6. const ACCEPT_TYPES = '*'; // e.g., 'image/*,.csv,.xlsx'
  7. const thresholdInput = document.getElementById('thresholdRange');
  8. const thresholdValueDisplay = document.getElementById('thresholdValue');
  9. var PRODUCT_BASE = [
  10. // { 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' },
  11. // { 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' },
  12. // { 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' },
  13. // { 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' },
  14. // { 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' }
  15. ];
  16. // --- Data ---
  17. const mediaUrl = "./../";
  18. document.addEventListener('DOMContentLoaded', () => {
  19. jQuery('#full-page-loader').show();
  20. fetch('/attr/products', {
  21. method: 'GET', // or 'POST' if your API expects POST
  22. headers: {
  23. 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
  24. }
  25. })
  26. .then(response => response.json())
  27. .then(data => {
  28. // console.log("data",data);
  29. // --- Wire up ---
  30. PRODUCT_BASE = data;
  31. PRODUCT_BASE = PRODUCT_BASE.map((d)=>{return {...d,mandatoryAttributes:["color","size"]}});
  32. // console.log("PRODUCT_BASE",PRODUCT_BASE);
  33. if(PRODUCT_BASE.length > 0){
  34. $('#paginationBar').style.display = 'block';
  35. }
  36. renderProducts();
  37. getAtributeList();
  38. document.getElementById('btnSubmit').addEventListener('click', submitAttributes);
  39. document.getElementById('btnReset').addEventListener('click', resetAll);
  40. // document.getElementById('btnSelectAll').addEventListener('click', () => {
  41. // if (selectedIds.size === PRODUCT_BASE.length) { selectedIds.clear(); } else { selectedIds = new Set(PRODUCT_BASE.map(p => p.id)); }
  42. // // renderProducts();
  43. // });
  44. // Replace your existing Select All listener with this:
  45. document.getElementById('btnSelectAll').addEventListener('click', () => {
  46. // Use the container for the active layout
  47. const container = (layoutMode === 'cards')
  48. ? document.getElementById('cardsContainer')
  49. : document.getElementById('tableContainer');
  50. // Collect all visible checkboxes
  51. const boxes = Array.from(container.querySelectorAll('input[type="checkbox"]'));
  52. // If every visible checkbox is already checked, we'll deselect; otherwise select all
  53. const allChecked = boxes.length > 0 && boxes.every(cb => cb.checked);
  54. boxes.forEach(cb => {
  55. const target = !allChecked; // true to select, false to deselect
  56. if (cb.checked !== target) {
  57. cb.checked = target;
  58. // Trigger your existing "change" handler so selectedIds & row .selected class update
  59. cb.dispatchEvent(new Event('change', { bubbles: true }));
  60. }
  61. });
  62. // Update the selection pill text (doesn't re-render the list)
  63. updateSelectionInfo();
  64. });
  65. document.getElementById('btnCards').addEventListener('click', () => setLayout('cards'));
  66. document.getElementById('btnTable').addEventListener('click', () => setLayout('table'));
  67. jQuery('#full-page-loader').hide();
  68. // if (data.success) {
  69. // }
  70. });
  71. });
  72. var FAKE_API_RESPONSE = {
  73. // results: [
  74. // { 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' } },
  75. // { 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' } },
  76. // { 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' } },
  77. // { 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' } },
  78. // { 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' } }
  79. // ],
  80. // total_products: 5,
  81. // successful: 5,
  82. // failed: 0
  83. };
  84. // --- State ---
  85. let selectedIds = new Set();
  86. // NEW: Array of objects { item_id: string, mandatory_attrs: { [attribute_name]: string[] } }
  87. let selectedProductsWithAttributes = [];
  88. let selectedAttributes = new Array();
  89. const lastSeen = new Map(); // per-product memory for NEW highlighting (product_id -> maps)
  90. let layoutMode = 'table'; // 'cards' | 'table'
  91. // --- Helpers ---
  92. const $ = (sel) => document.querySelector(sel);
  93. const el = (tag, cls) => { const e = document.createElement(tag); if (cls) e.className = cls; return e; }
  94. function updateSelectionInfo() {
  95. const pill = $('#selectionInfo');
  96. const total = PRODUCT_BASE.length;
  97. // const count = selectedIds.size;
  98. const count = selectedProductsWithAttributes.length;
  99. pill.textContent = count === 0 ? 'No products selected' : `${count} of ${total} selected`;
  100. }
  101. function setChecked(id, checked) { if (checked) selectedIds.add(id); else selectedIds.delete(id); updateSelectionInfo(); }
  102. // function setCheckedAttributes(id,attribute, checked) { if (checked) selectedAttributes.add({id: [attribute]}); else selectedIds.delete({id:[attribute]}); updateSelectionInfo(); }
  103. // --- Chips rendering ---
  104. function renderChips(container, obj, memoryMap) {
  105. container.innerHTML = '';
  106. let count = 0;
  107. Object.entries(obj || {}).forEach(([k, v]) => {
  108. const chip = el('span', 'chip');
  109. const kEl = el('span', 'k'); kEl.textContent = k + ':';
  110. // console.log("v",v);
  111. const vEl = el('span', 'v'); vEl.textContent = ' ' + String(v[0].value) +' (' +String(v[0].source) + ')';
  112. chip.appendChild(kEl); chip.appendChild(vEl);
  113. const was = memoryMap.get(k);
  114. if (was === undefined || was !== v) chip.classList.add('new');
  115. container.appendChild(chip);
  116. memoryMap.set(k, v);
  117. count++;
  118. });
  119. return count;
  120. }
  121. function findApiResultForProduct(p, index, api) { return api.results?.find(r => r.product_id === p.item_id) || api.results?.[index] || null; }
  122. // --- Cards layout ---
  123. function createProductCard(p) {
  124. const row = el('div', 'product');
  125. // Check selection using the new helper
  126. if (isProductSelected(p.item_id)) row.classList.add('selected');
  127. // if (selectedIds.has(p.item_id)) row.classList.add('selected');
  128. const left = el('div', 'thumb');
  129. const img = new Image(); img.src = mediaUrl+p.image_path || p.image || '';
  130. img.alt = `${p.product_name} image`;
  131. // console.log("img",img);
  132. // 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); };
  133. img.onerror = () => { img.src = mediaUrl+"media/images/no-product.png" };
  134. left.appendChild(img);
  135. const mid = el('div', 'meta');
  136. const name = el('div', 'name'); name.textContent = p.product_name || '—';
  137. const desc = el('div', 'desc'); desc.innerHTML = p.product_short_description || '';
  138. const badges = el('div', 'badges');
  139. const sku = el('span', 'pill'); sku.textContent = `SKU: ${p.item_id || '—'}`; badges.appendChild(sku);
  140. const type = el('span', 'pill'); type.textContent = p.product_type || '—'; badges.appendChild(type);
  141. const long = el('div', 'desc'); long.innerHTML = p.product_long_description || ''; long.style.marginTop = '4px';
  142. mid.appendChild(name); mid.appendChild(desc); mid.appendChild(badges); mid.appendChild(long);
  143. // Helper function to create the chip UI for attributes
  144. function createAttributeChips(p, attr, initialSelected, isMandatory, updateCallback) {
  145. const wrapper = el('div', 'attribute-chip-group');
  146. wrapper.dataset.attrName = attr.attribute_name;
  147. wrapper.innerHTML = `<p class="attribute-header">${attr.attribute_name} (${isMandatory ? 'Mandatory' : 'Optional'}):</p>`;
  148. const chipContainer = el('div', 'chips-container');
  149. attr.possible_values.forEach(value => {
  150. const chip = el('label', 'attribute-chip');
  151. // Checkbox input is hidden, but drives the selection state
  152. const checkbox = document.createElement('input');
  153. checkbox.type = 'checkbox';
  154. checkbox.value = value;
  155. checkbox.name = `${p.item_id}-${attr.attribute_name}`;
  156. // Set initial state
  157. checkbox.checked = initialSelected.includes(value);
  158. // The visual part of the chip
  159. const span = el('span');
  160. span.textContent = value;
  161. chip.appendChild(checkbox);
  162. chip.appendChild(span);
  163. chipContainer.appendChild(chip);
  164. });
  165. // Use event delegation on the container for performance
  166. chipContainer.addEventListener('change', updateCallback);
  167. wrapper.appendChild(chipContainer);
  168. return wrapper;
  169. }
  170. // --- Main Select Checkbox (Product Selection) ---
  171. const right = el('label', 'select');
  172. const cb = document.createElement('input'); cb.type = 'checkbox';
  173. cb.checked = isProductSelected(p.item_id);
  174. const lbl = el('span'); lbl.textContent = 'Select Product';
  175. right.appendChild(cb); right.appendChild(lbl);
  176. // --- Dynamic Attribute Selects ---
  177. const attrContainer = el('div', 'attribute-selectors');
  178. // Find all mandatory and non-mandatory attributes for this product
  179. const mandatoryAttributes = p.product_type_details?.filter(a => a.is_mandatory === 'Yes') || [];
  180. const optionalAttributes = p.product_type_details?.filter(a => a.is_mandatory !== 'Yes') || [];
  181. // Helper to update the main state object with all current selections
  182. const updateProductState = () => {
  183. const isSelected = cb.checked;
  184. const currentSelections = {};
  185. if (isSelected) {
  186. // Iterate over all attribute groups (Mandatory and Optional)
  187. attrContainer.querySelectorAll('.attribute-chip-group').forEach(group => {
  188. const attrName = group.dataset.attrName;
  189. // Collect selected chip values
  190. const selectedOptions = Array.from(group.querySelectorAll('input[type="checkbox"]:checked'))
  191. .map(checkbox => checkbox.value);
  192. if (selectedOptions.length > 0) {
  193. currentSelections[attrName] = selectedOptions;
  194. }
  195. });
  196. }
  197. toggleProductSelection(p.item_id, isSelected, currentSelections);
  198. row.classList.toggle('selected', isSelected);
  199. };
  200. // Attach listener to main checkbox
  201. cb.addEventListener('change', () => {
  202. attrContainer.classList.toggle('disabled', !cb.checked);
  203. updateProductState();
  204. });
  205. // --- Render Mandatory Attributes ---
  206. if (mandatoryAttributes.length > 0) {
  207. const manTitle = el('p', "pSelectRight mandatory-title");
  208. manTitle.innerHTML = "Mandatory Attributes:";
  209. attrContainer.appendChild(manTitle);
  210. mandatoryAttributes.forEach(attr => {
  211. const initialSelected = getSelectedAttributes(p.item_id)[attr.attribute_name] || attr.possible_values;
  212. const chipGroup = createAttributeChips(p, attr, initialSelected, true, updateProductState);
  213. attrContainer.appendChild(chipGroup);
  214. });
  215. }
  216. // --- Render Optional Attributes ---
  217. if (optionalAttributes.length > 0) {
  218. const br = el('br');
  219. const optTitle = el('p', "pSelectRight optional-title");
  220. optTitle.innerHTML = "Additional Attributes:";
  221. attrContainer.appendChild(br);
  222. attrContainer.appendChild(optTitle);
  223. optionalAttributes.forEach(attr => {
  224. const initialSelected = getSelectedAttributes(p.item_id)[attr.attribute_name] || attr.possible_values;
  225. const chipGroup = createAttributeChips(p, attr, initialSelected, false, updateProductState);
  226. attrContainer.appendChild(chipGroup);
  227. });
  228. }
  229. // Initialize attribute selectors' enabled state and state data
  230. attrContainer.classList.toggle('disabled', !cb.checked);
  231. // Initial state setup if the product was already selected (e.g., after a re-render)
  232. if (cb.checked) {
  233. // This is important to set the initial state correctly on load
  234. // We defer this until all selects are mounted, or ensure the initial state is correct.
  235. // For simplicity, we assume the data from PRODUCT_BASE already includes selected attributes if a selection exists
  236. // (which it won't in this case, so they default to all/empty)
  237. }
  238. const inline = el('div', 'attr-inline');
  239. inline.dataset.pid = p.item_id; // use item_id for mapping
  240. row.appendChild(left); row.appendChild(mid);
  241. row.appendChild(attrContainer); // Append the new attribute selectors container
  242. row.appendChild(right);
  243. // if (p.mandatoryAttributes && p.mandatoryAttributes.length > 0) {
  244. // const hr = el('hr');
  245. // row.appendChild(hr);
  246. // row.appendChild(attri);
  247. // row.appendChild(secondRight);
  248. // }
  249. row.appendChild(inline);
  250. return row;
  251. }
  252. // Cards layout
  253. function renderProductsCards(items = getCurrentSlice()) {
  254. const cards = document.getElementById('cardsContainer');
  255. cards.innerHTML = '';
  256. if(items.length > 0){
  257. items.forEach(p => cards.appendChild(createProductCard(p)));
  258. }else{
  259. cards.innerHTML = "<p>No Products Found.</p>"
  260. }
  261. }
  262. // --- Table layout ---
  263. function createMiniThumb(p) {
  264. const mt = el('div', 'mini-thumb');
  265. const img = new Image(); img.src = mediaUrl+p.image_path || p.image || ''; img.alt = `${p.product_name} image`;
  266. // console.log("img",img);
  267. img.onerror = () => { img.src = mediaUrl+"media/images/no-product.png" };
  268. // 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); };
  269. mt.appendChild(img);
  270. return mt;
  271. }
  272. // Table layout
  273. // function renderProductsTable(items = getCurrentSlice()) {
  274. // const wrap = document.getElementById('tableContainer');
  275. // wrap.innerHTML = '';
  276. // const table = document.createElement('table');
  277. // const thead = document.createElement('thead'); const trh = document.createElement('tr');
  278. // ['Select', 'Image', 'Product', 'SKU', 'Type', 'Short Description'].forEach(h => {
  279. // const th = document.createElement('th'); th.textContent = h; trh.appendChild(th);
  280. // });
  281. // thead.appendChild(trh); table.appendChild(thead);
  282. // const tbody = document.createElement('tbody');
  283. // if(items.length > 0 ){
  284. // items.forEach(p => {
  285. // const tr = document.createElement('tr'); tr.id = `row-${p.id}`;
  286. // const tdSel = document.createElement('td'); tdSel.className = 'select-cell';
  287. // const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = selectedIds.has(p.item_id);
  288. // cb.addEventListener('change', () => { setChecked(p.item_id, cb.checked); tr.classList.toggle('selected', cb.checked); });
  289. // tdSel.appendChild(cb); tr.appendChild(tdSel);
  290. // const tdImg = document.createElement('td'); tdImg.className = 'thumb-cell'; tdImg.appendChild(createMiniThumb(p)); tr.appendChild(tdImg);
  291. // const tdName = document.createElement('td'); tdName.textContent = p.product_name || '—'; tr.appendChild(tdName);
  292. // const tdSku = document.createElement('td'); tdSku.textContent = p.item_id || '—'; tr.appendChild(tdSku);
  293. // const tdType = document.createElement('td'); const b = document.createElement('span'); b.className = 'badge'; b.textContent = p.product_type || '—'; tdType.appendChild(b); tr.appendChild(tdType);
  294. // const tdDesc = document.createElement('td'); tdDesc.textContent = p.product_short_description || ''; tr.appendChild(tdDesc);
  295. // tr.addEventListener('click', (e) => { if (e.target.tagName.toLowerCase() !== 'input') { cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); } });
  296. // tbody.appendChild(tr);
  297. // });
  298. // }else{
  299. // const tr = el('tr');
  300. // // tr.id = `row-${p.id}`;
  301. // const tdName = el('td');
  302. // tdName.colSpan = 6;
  303. // tdName.innerHTML = "No Products Found."
  304. // tr.appendChild(tdName);
  305. // // tr.colspan = 6;
  306. // // tr.innerHTML
  307. // tbody.appendChild(tr);
  308. // }
  309. // table.appendChild(tbody);
  310. // wrap.appendChild(table);
  311. // }
  312. // NOTE: Ensure getProductStateUpdater and generateAttributeUI functions are defined globally or accessible here.
  313. /**
  314. * Returns a closure function that updates the global selectedProductsWithAttributes state
  315. * based on the current selections (chips) found in the DOM for a specific product.
  316. * This is used for both card and table views.
  317. * * @param {Object} p - The product object.
  318. * @param {HTMLElement} cb - The main product selection checkbox element.
  319. * @param {HTMLElement} tr - The main row/card element (used for toggling 'selected' class).
  320. * @returns {function} A function to be used as the attribute change handler.
  321. */
  322. const getProductStateUpdater = (p, cb, tr) => () => {
  323. const isSelected = cb.checked;
  324. const currentSelections = {};
  325. // Find the attribute container using its unique ID, which is the same structure
  326. // used in both card and table detail views (e.g., 'attr-container-124353498' or just the main card element).
  327. // For card view, the container is often the attrContainer element itself.
  328. // For table view, we use the explicit ID.
  329. const attrContainer = document.getElementById(`attr-container-${p.item_id}`) || tr.querySelector('.attribute-selectors');
  330. if (isSelected && attrContainer) {
  331. // Iterate over all attribute groups (Mandatory and Optional) within the container
  332. attrContainer.querySelectorAll('.attribute-chip-group').forEach(group => {
  333. const attrName = group.dataset.attrName;
  334. // Collect selected chip values
  335. const selectedOptions = Array.from(group.querySelectorAll('input[type="checkbox"]:checked'))
  336. .map(checkbox => checkbox.value);
  337. // Only add to the selection if at least one option is selected
  338. if (selectedOptions.length > 0) {
  339. currentSelections[attrName] = selectedOptions;
  340. }
  341. });
  342. }
  343. // Update the global state array (selectedProductsWithAttributes)
  344. toggleProductSelection(p.item_id, isSelected, currentSelections);
  345. // Update the visual status of the row/card
  346. tr.classList.toggle('selected', isSelected);
  347. };
  348. /**
  349. * Generates the full attribute selection UI (chips) for a given product.
  350. * NOTE: Assumes el(), createAttributeChips(), and getSelectedAttributes() are defined globally.
  351. * @param {Object} p - The product object from PRODUCT_BASE.
  352. * @param {function} updateProductState - The callback to run on chip changes.
  353. * @param {HTMLElement} attrContainer - The container to append the UI to.
  354. */
  355. function generateAttributeUI(p, updateProductState, attrContainer) {
  356. // Clear the container first, just in case
  357. attrContainer.innerHTML = '';
  358. const mandatoryAttributes = p.product_type_details?.filter(a => a.is_mandatory === 'Yes') || [];
  359. const optionalAttributes = p.product_type_details?.filter(a => a.is_mandatory !== 'Yes') || [];
  360. // --- Render Mandatory Attributes ---
  361. if (mandatoryAttributes.length > 0) {
  362. // Use a general title for the section header
  363. const manTitle = el('p', "pSelectRight mandatory-title");
  364. manTitle.innerHTML = "Mandatory Attributes:";
  365. attrContainer.appendChild(manTitle);
  366. mandatoryAttributes.forEach(attr => {
  367. const initialSelected = getSelectedAttributes(p.item_id)[attr.attribute_name] || attr.possible_values;
  368. // The createAttributeChips function must be globally available
  369. const chipGroup = createAttributeChips(p, attr, initialSelected, true, updateProductState);
  370. attrContainer.appendChild(chipGroup);
  371. });
  372. }
  373. // --- Render Optional Attributes ---
  374. if (optionalAttributes.length > 0) {
  375. // Add visual separation using the optional-title class
  376. const optTitle = el('p', "pSelectRight optional-title");
  377. optTitle.innerHTML = "Additional Attributes:";
  378. // Append the title for separation
  379. attrContainer.appendChild(optTitle);
  380. optionalAttributes.forEach(attr => {
  381. const initialSelected = getSelectedAttributes(p.item_id)[attr.attribute_name] || attr.possible_values;
  382. const chipGroup = createAttributeChips(p, attr, initialSelected, false, updateProductState);
  383. attrContainer.appendChild(chipGroup);
  384. });
  385. }
  386. }
  387. /**
  388. * Creates the HTML structure for a single attribute group using chip/checkbox labels.
  389. * Assumes the helper function 'el' is available.
  390. * * @param {Object} p - The product object.
  391. * @param {Object} attr - The specific attribute detail object.
  392. * @param {string[]} initialSelected - Array of values that should be pre-checked.
  393. * @param {boolean} isMandatory - True if the attribute is mandatory.
  394. * @param {function} updateCallback - The function to call when a chip selection changes.
  395. * @returns {HTMLElement} The attribute chip group container (div).
  396. */
  397. function createAttributeChips(p, attr, initialSelected, isMandatory, updateCallback) {
  398. const wrapper = el('div', 'attribute-chip-group');
  399. wrapper.dataset.attrName = attr.attribute_name;
  400. // Determine the header text based on structure preference (e.g., just the name)
  401. const statusText = isMandatory ? ' (Mandatory)' : ' (Optional)';
  402. wrapper.innerHTML = `<p class="attribute-header">${attr.attribute_name}${statusText}:</p>`;
  403. const chipContainer = el('div', 'chips-container');
  404. attr.possible_values.forEach(value => {
  405. const chip = el('label', 'attribute-chip');
  406. // Checkbox input is hidden, but drives the selection state
  407. const checkbox = document.createElement('input');
  408. checkbox.type = 'checkbox';
  409. checkbox.value = value;
  410. // Ensure the name is unique per product/attribute group
  411. checkbox.name = `${p.item_id}-${attr.attribute_name}`;
  412. // Set initial state
  413. checkbox.checked = initialSelected.includes(value);
  414. // The visual part of the chip
  415. const span = el('span');
  416. span.textContent = value;
  417. chip.appendChild(checkbox);
  418. chip.appendChild(span);
  419. chipContainer.appendChild(chip);
  420. });
  421. // Attach listener to the container using event delegation
  422. chipContainer.addEventListener('change', updateCallback);
  423. wrapper.appendChild(chipContainer);
  424. return wrapper;
  425. }
  426. function renderProductsTable(items = getCurrentSlice()) {
  427. const wrap = document.getElementById('tableContainer');
  428. wrap.innerHTML = '';
  429. const table = document.createElement('table');
  430. table.classList.add('table', 'table-striped', 'table-bordered','table-responsive');
  431. const thead = document.createElement('thead');
  432. const trh = document.createElement('tr');
  433. // Table Headers
  434. ['Select', 'Image', 'Product', 'SKU', 'Type', 'Short Description', 'Attributes'].forEach(h => {
  435. const th = document.createElement('th'); th.textContent = h; trh.appendChild(th);
  436. });
  437. thead.appendChild(trh); table.appendChild(thead);
  438. const tbody = document.createElement('tbody');
  439. if (items.length > 0) {
  440. items.forEach(p => {
  441. const tr = document.createElement('tr');
  442. tr.id = `row-${p.id}`;
  443. if (isProductSelected(p.item_id)) tr.classList.add('selected');
  444. // --- Define Checkbox (cb) and State Updater ---
  445. const cb = document.createElement('input');
  446. cb.type = 'checkbox';
  447. cb.checked = isProductSelected(p.item_id);
  448. // The state updater function is bound to this specific row/checkbox
  449. const updateProductState = getProductStateUpdater(p, cb, tr);
  450. // --- Select Cell ---
  451. const tdSel = document.createElement('td');
  452. tdSel.className = 'select-cell';
  453. tdSel.appendChild(cb);
  454. tr.appendChild(tdSel);
  455. // --- Other Cells ---
  456. const tdImg = document.createElement('td'); tdImg.className = 'thumb-cell'; tdImg.appendChild(createMiniThumb(p)); tr.appendChild(tdImg);
  457. const tdName = document.createElement('td'); tdName.textContent = p.product_name || '—'; tr.appendChild(tdName);
  458. const tdSku  = document.createElement('td'); tdSku.textContent = p.item_id || '—'; tr.appendChild(tdSku);
  459. const tdType = document.createElement('td'); const b = document.createElement('span'); b.className = 'badge'; b.textContent = p.product_type || '—'; tdType.appendChild(b); tr.appendChild(tdType);
  460. const tdDesc = document.createElement('td'); tdDesc.textContent = p.product_short_description || ''; tr.appendChild(tdDesc);
  461. // ---------------------------------------------
  462. // --- ATTRIBUTE SELECTION IMPLEMENTATION ---
  463. // ---------------------------------------------
  464. // 1. DETAIL ROW STRUCTURE
  465. const detailRow = document.createElement('tr');
  466. detailRow.classList.add('attribute-detail-row'); // Custom class for styling
  467. detailRow.style.display = 'none'; // Initially hidden
  468. detailRow.id = `detail-row-${p.id}`;
  469. const detailCell = document.createElement('td');
  470. detailCell.colSpan = 7; // Must span all columns
  471. const attrContainer = document.createElement('div');
  472. attrContainer.id = `attr-container-${p.item_id}`; // Unique ID for targeting by updateProductState
  473. attrContainer.classList.add('attribute-selectors', 'table-selectors');
  474. // 2. GENERATE CHIPS UI
  475. generateAttributeUI(p, updateProductState, attrContainer);
  476. // Initially disable the chips if the product is not selected
  477. attrContainer.classList.toggle('disabled', !cb.checked);
  478. detailCell.appendChild(attrContainer);
  479. detailRow.appendChild(detailCell);
  480. // 3. TOGGLE BUTTON (in the main row)
  481. const tdAttr = document.createElement('td');
  482. const toggleButton = document.createElement('button');
  483. toggleButton.textContent = 'Configure';
  484. toggleButton.classList.add('btn', 'btn-sm', 'btn-info', 'attribute-toggle-btn');
  485. tdAttr.appendChild(toggleButton);
  486. tr.appendChild(tdAttr);
  487. // 4. EVENT LISTENERS
  488. // a) Toggle Button Logic
  489. toggleButton.addEventListener('click', (e) => {
  490. e.stopPropagation(); // Stop row click event
  491. const isHidden = detailRow.style.display === 'none';
  492. detailRow.style.display = isHidden ? '' : 'none'; // Toggle visibility
  493. toggleButton.textContent = isHidden ? 'Hide Attributes' : 'Configure';
  494. toggleButton.classList.toggle('btn-info', !isHidden);
  495. toggleButton.classList.toggle('btn-secondary', isHidden);
  496. });
  497. // b) Main Checkbox Change Logic
  498. cb.addEventListener('change', () => {
  499. updateProductState(); // Update state on check/uncheck
  500. attrContainer.classList.toggle('disabled', !cb.checked); // Enable/Disable chips
  501. });
  502. // c) Row Click Listener (Updated to ignore button clicks)
  503. tr.addEventListener('click', (e) => {
  504. const tag = e.target.tagName.toLowerCase();
  505. if (tag !== 'input' && tag !== 'button') {
  506. cb.checked = !cb.checked;
  507. cb.dispatchEvent(new Event('change'));
  508. }
  509. });
  510. // 5. Append Rows to TBODY
  511. tbody.appendChild(tr);
  512. tbody.appendChild(detailRow); // Append the detail row right after the main row
  513. });
  514. } else {
  515. const tr = el('tr');
  516. const tdName = el('td');
  517. tdName.colSpan = 7;
  518. tdName.innerHTML = "No Products Found.";
  519. tr.appendChild(tdName);
  520. tbody.appendChild(tr);
  521. }
  522. table.appendChild(tbody);
  523. wrap.appendChild(table);
  524. }
  525. function renderInlineForCards() {
  526. const api = FAKE_API_RESPONSE;
  527. // Clear all inline sections first
  528. document.querySelectorAll('.attr-inline').forEach(div => div.innerHTML = '');
  529. PRODUCT_BASE.forEach((p, idx) => {
  530. const inline = document.querySelector(`.attr-inline[data-pid="${p.item_id}"]`);
  531. if (!inline) return;
  532. // --- CHANGE HERE: Use the new helper function ---
  533. if (!isProductSelected(p.item_id)) return; // only show for selected
  534. const res = findApiResultForProduct(p, idx, api);
  535. const pid = p.item_id;
  536. if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map() });
  537. const mem = lastSeen.get(pid);
  538. // Build sections
  539. const manTitle = el('div', 'section-title'); manTitle.innerHTML = '<strong>Mandatory</strong>';
  540. const manChips = el('div', 'chips');
  541. const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
  542. const addChips = el('div', 'chips');
  543. const mandCount = renderChips(manChips, res?.mandatory || {}, mem.mandatory);
  544. const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
  545. const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
  546. const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
  547. const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
  548. counts.appendChild(c1); counts.appendChild(c2);
  549. inline.appendChild(manTitle); inline.appendChild(manChips);
  550. inline.appendChild(addTitle); inline.appendChild(addChips);
  551. inline.appendChild(counts);
  552. });
  553. // Update summary
  554. $('#statTotal').textContent = api.total_products ?? 0;
  555. $('#statOk').textContent = api.successful ?? 0;
  556. $('#statKo').textContent = api.failed ?? 0;
  557. $('#api-summary').style.display = 'block';
  558. }
  559. // -----------------------------------------------------------
  560. function renderInlineForTable() {
  561. const api = FAKE_API_RESPONSE;
  562. const table = $('#tableContainer');
  563. if (!table) return;
  564. // Remove existing detail rows
  565. table.querySelectorAll('tr.detail-row').forEach(r => r.remove());
  566. PRODUCT_BASE.forEach((p, idx) => {
  567. // --- CHANGE HERE: Use the new helper function ---
  568. if (!isProductSelected(p.item_id)) return;
  569. const res = findApiResultForProduct(p, idx, api);
  570. const pid = p.item_id;
  571. if (!lastSeen.has(pid)) lastSeen.set(pid, { mandatory: new Map(), additional: new Map() });
  572. const mem = lastSeen.get(pid);
  573. const tbody = table.querySelector('tbody');
  574. // NOTE: The table rendering uses p.id for the row ID: `row-${p.id}`.
  575. // Assuming p.id is still valid for finding the base row, as your original code used it.
  576. const baseRow = tbody.querySelector(`#row-${p.id}`);
  577. if (!baseRow) return;
  578. const detail = el('tr', 'detail-row');
  579. const td = el('td'); td.colSpan = 6; // number of columns
  580. const content = el('div', 'detail-content');
  581. const manTitle = el('div', 'section-title'); manTitle.innerHTML = '<strong>Mandatory</strong>';
  582. const manChips = el('div', 'chips');
  583. const addTitle = el('div', 'section-title'); addTitle.innerHTML = '<strong>Additional</strong>';
  584. const addChips = el('div', 'chips');
  585. const mandCount = renderChips(manChips, res?.mandatory || {}, mem.mandatory);
  586. const addCount = renderChips(addChips, res?.additional || {}, mem.additional);
  587. const counts = el('div'); counts.style.display = 'flex'; counts.style.gap = '8px'; counts.style.margin = '8px 0 0';
  588. const c1 = el('span', 'pill'); c1.textContent = `Mandatory: ${mandCount}`;
  589. const c2 = el('span', 'pill'); c2.textContent = `Additional: ${addCount}`;
  590. counts.appendChild(c1); counts.appendChild(c2);
  591. content.appendChild(manTitle); content.appendChild(manChips);
  592. content.appendChild(addTitle); content.appendChild(addChips);
  593. content.appendChild(counts);
  594. td.appendChild(content); detail.appendChild(td);
  595. // insert after base row
  596. baseRow.insertAdjacentElement('afterend', detail);
  597. });
  598. // Update summary
  599. $('#statTotal').textContent = api.total_products ?? 0;
  600. $('#statOk').textContent = api.successful ?? 0;
  601. $('#statKo').textContent = api.failed ?? 0;
  602. $('#api-summary').style.display = 'block';
  603. }
  604. function renderInlineAttributes() {
  605. if (layoutMode === 'cards') renderInlineForCards(); else renderInlineForTable();
  606. }
  607. // --- Main rendering ---
  608. function renderProducts() {
  609. if (layoutMode === 'cards') {
  610. $('#cardsContainer').style.display = '';
  611. $('#tableContainer').style.display = 'none';
  612. // console.log("PRODUCT_BASE",PRODUCT_BASE);
  613. renderProductsCards();
  614. } else {
  615. $('#cardsContainer').style.display = 'none';
  616. $('#tableContainer').style.display = '';
  617. renderProductsTable();
  618. }
  619. updateSelectionInfo();
  620. renderPagination();
  621. // If there is a selection, re-render inline attributes (persist across toggle)
  622. if (selectedIds.size > 0) renderInlineAttributes();
  623. }
  624. // --- Submit & Reset ---
  625. function submitAttributes() {
  626. // Check the length of the new array
  627. if (selectedProductsWithAttributes.length === 0) {
  628. alert('Please select at least one product.');
  629. return;
  630. }
  631. // if (selectedIds.size === 0) { alert('Please select at least one product.'); return; }
  632. // console.log("selectedIds",selectedIds);
  633. jQuery('#full-page-loader').show();
  634. // let inputArray = {
  635. // "product_ids" : [...selectedIds]
  636. // }
  637. const extractAdditional = document.getElementById('extract_additional').checked;
  638. const processImage = document.getElementById('process_image').checked;
  639. // const selectedMultiples = document.getElementById('#mandatory-attributes');
  640. // const selectedValues = Array.from(selectedMultiples.selectedOptions).map(option => option.value);
  641. const selectElement = document.getElementById('mandatory-attributes');
  642. const selectedValues = Array.from(selectElement.selectedOptions).map(option => option.value);
  643. // console.log(selectedValues); // Logs an array of selected values
  644. // console.log("thresholdValueDisplay",thresholdValueDisplay.value);
  645. const threshold = parseFloat(document.getElementById('thresholdRange').value);
  646. // Transform the new state array into the required API format
  647. const itemIds = selectedProductsWithAttributes.map(p => p.item_id);
  648. // Create the mandatory_attrs map: { item_id: { attr_name: [values] } }
  649. // NOTE: The backend API you showed expects a flattened list of "mandatory_attrs"
  650. // like: { "color": ["color", "shade"], "size": ["size", "fit"] }
  651. // It seems to ignore the selected product-specific values and uses a general list of synonyms.
  652. // Assuming the request needs a general map of *all unique* selected attributes across all selected products:
  653. let mandatoryAttrsMap = {};
  654. selectedProductsWithAttributes.forEach(product => {
  655. // Merge attributes from all selected products
  656. Object.assign(mandatoryAttrsMap, product.mandatory_attrs);
  657. });
  658. // If the API expects the complex, product-specific payload from your Q1 example:
  659. const payloadForQ1 = selectedProductsWithAttributes.map(p => ({
  660. item_id: p.item_id,
  661. mandatory_attrs: p.mandatory_attrs
  662. }));
  663. let inputArray = {
  664. "products": payloadForQ1,
  665. "model": "llama-3.1-8b-instant",
  666. "extract_additional": extractAdditional,
  667. "process_image": processImage,
  668. "multiple": selectedValues,
  669. "threshold_abs": threshold, // Lower threshold to be more permissive
  670. // "margin": 0.3, // Larger margin to include more candidates
  671. // "use_adaptive_margin": true,
  672. // "use_semantic_clustering": true
  673. }
  674. let raw = JSON.stringify(inputArray);
  675. fetch('/attr/batch-extract/', {
  676. method: 'POST', // or 'POST' if your API expects POST
  677. headers: {
  678. 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || '',
  679. 'Content-Type': "application/json"
  680. },
  681. body: raw
  682. })
  683. .then(response => response.json())
  684. .then(data => {
  685. // console.log("response data",data);
  686. FAKE_API_RESPONSE = data;
  687. renderInlineAttributes();
  688. jQuery('#full-page-loader').hide();
  689. });
  690. }
  691. function resetAll() {
  692. selectedProductsWithAttributes = []; // Reset the main array
  693. // selectedIds.clear();
  694. lastSeen.clear();
  695. renderProducts();
  696. // Clear summary
  697. document.getElementById('statTotal').textContent = '0';
  698. document.getElementById('statOk').textContent = '0';
  699. document.getElementById('statKo').textContent = '0';
  700. $('#api-summary').style.display = 'none';
  701. // ✅ Clear Select2 selections
  702. jQuery('#mandatory-attributes').val(null).trigger('change');
  703. // ✅ Reset threshold input (and display)
  704. const thresholdInput = document.getElementById('thresholdRange');
  705. const thresholdDisplay = document.getElementById('thresholdValue');
  706. thresholdInput.value = '0.2'; // or any default value you prefer
  707. if (thresholdDisplay) {
  708. thresholdDisplay.textContent = '0.2';
  709. }
  710. }
  711. function setLayout(mode) {
  712. layoutMode = mode;
  713. const btnCards = document.getElementById('btnCards');
  714. const btnTable = document.getElementById('btnTable');
  715. if (mode === 'cards') { btnCards.classList.add('active'); btnCards.setAttribute('aria-selected', 'true'); btnTable.classList.remove('active'); btnTable.setAttribute('aria-selected', 'false'); }
  716. else { btnTable.classList.add('active'); btnTable.setAttribute('aria-selected', 'true'); btnCards.classList.remove('active'); btnCards.setAttribute('aria-selected', 'false'); }
  717. renderProducts();
  718. }
  719. // Upload elements (Bootstrap modal version)
  720. const uploadModalEl = document.getElementById('uploadModal');
  721. const dropzone = document.getElementById('dropzone');
  722. const uploadFiles = document.getElementById('uploadFiles');
  723. const fileInfo = document.getElementById('fileInfo');
  724. const uploadBar = document.getElementById('uploadBar');
  725. const uploadStatus = document.getElementById('uploadStatus');
  726. // Reset modal on show
  727. uploadModalEl.addEventListener('shown.bs.modal', () => {
  728. uploadStatus.textContent = '';
  729. uploadStatus.className = ''; // clear success/error class
  730. uploadBar.style.width = '0%';
  731. uploadBar.setAttribute('aria-valuenow', '0');
  732. uploadFiles.value = '';
  733. uploadFiles.setAttribute('accept', ACCEPT_TYPES);
  734. fileInfo.textContent = 'No files selected.';
  735. });
  736. function describeFiles(list) {
  737. if (!list || list.length === 0) { fileInfo.textContent = 'No files selected.'; return; }
  738. const names = Array.from(list).map(f => `${f.name} (${Math.round(f.size/1024)} KB)`);
  739. fileInfo.textContent = names.join(', ');
  740. }
  741. // Drag & drop feedback
  742. ['dragenter','dragover'].forEach(evt => {
  743. dropzone.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); dropzone.classList.add('drag'); });
  744. });
  745. ['dragleave','drop'].forEach(evt => {
  746. dropzone.addEventListener(evt, e => { e.preventDefault(); e.stopPropagation(); dropzone.classList.remove('drag'); });
  747. });
  748. // Handle drop
  749. dropzone.addEventListener('drop', e => {
  750. uploadFiles.files = e.dataTransfer.files;
  751. describeFiles(uploadFiles.files);
  752. });
  753. // Click to browse
  754. // dropzone.addEventListener('click', () => uploadFiles.click());
  755. // Picker change
  756. uploadFiles.addEventListener('change', () => describeFiles(uploadFiles.files));
  757. function startUpload() {
  758. const files = uploadFiles.files;
  759. if (!files || files.length === 0) { alert('Please select file(s) to upload.'); return; }
  760. jQuery('#full-page-loader').show();
  761. uploadStatus.textContent = 'Uploading...';
  762. uploadStatus.className = ''; // neutral
  763. uploadBar.style.width = '0%';
  764. uploadBar.setAttribute('aria-valuenow', '0');
  765. const form = new FormData();
  766. Array.from(files).forEach(f => form.append('file', f));
  767. // form.append('uploaded_by', 'Vishal'); // example extra field
  768. const xhr = new XMLHttpRequest();
  769. xhr.open('POST', UPLOAD_API_URL, true);
  770. // If you need auth:
  771. // xhr.setRequestHeader('Authorization', 'Bearer <token>');
  772. xhr.upload.onprogress = (e) => {
  773. if (e.lengthComputable) {
  774. const pct = Math.round((e.loaded / e.total) * 100);
  775. uploadBar.style.width = pct + '%';
  776. uploadBar.setAttribute('aria-valuenow', String(pct));
  777. }
  778. };
  779. xhr.onreadystatechange = () => {
  780. if (xhr.readyState === 4) {
  781. const ok = (xhr.status >= 200 && xhr.status < 300);
  782. try {
  783. const resp = JSON.parse(xhr.responseText || '{}');
  784. uploadStatus.textContent = ok ? (resp.message || 'Upload successful') : (resp.error || `Upload failed (${xhr.status})`);
  785. } catch {
  786. uploadStatus.textContent = ok ? 'Upload successful' : `Upload failed (${xhr.status})`;
  787. }
  788. uploadStatus.className = ok ? 'success' : 'error';
  789. // Optional: auto-close the modal on success after 1.2s:
  790. // if (ok) setTimeout(() => bootstrap.Modal.getInstance(uploadModalEl).hide(), 1200);
  791. }
  792. };
  793. xhr.onerror = () => {
  794. uploadStatus.textContent = 'Network error during upload.';
  795. uploadStatus.className = 'error';
  796. };
  797. xhr.send(form);
  798. setTimeout(()=>{
  799. jQuery('#uploadModal').modal('hide');
  800. },3000)
  801. jQuery('#full-page-loader').hide();
  802. }
  803. // Wire Start button
  804. document.getElementById('uploadStart').addEventListener('click', startUpload);
  805. // Cancel button already closes the modal via data-bs-dismiss
  806. // --- Pagination state ---
  807. let page = 1;
  808. let pageSize = 50; // default rows per page
  809. function totalPages() {
  810. return Math.max(1, Math.ceil(PRODUCT_BASE.length / pageSize));
  811. }
  812. function clampPage() {
  813. page = Math.min(Math.max(1, page), totalPages());
  814. }
  815. function getCurrentSlice() {
  816. clampPage();
  817. const start = (page - 1) * pageSize;
  818. return PRODUCT_BASE.slice(start, start + pageSize);
  819. }
  820. function renderPagination() {
  821. const bar = document.getElementById('paginationBar');
  822. if (!bar) return;
  823. const tp = totalPages();
  824. clampPage();
  825. bar.innerHTML = `
  826. <div class="page-size">
  827. <label for="pageSizeSelect">Rows per page</label>
  828. <select id="pageSizeSelect">
  829. <option value="5" ${pageSize===5 ? 'selected' : ''}>5</option>
  830. <option value="10" ${pageSize===10 ? 'selected' : ''}>10</option>
  831. <option value="20" ${pageSize===20 ? 'selected' : ''}>20</option>
  832. <option value="50" ${pageSize===50 ? 'selected' : ''}>50</option>
  833. <option value="all" ${pageSize>=PRODUCT_BASE.length ? 'selected' : ''}>All</option>
  834. </select>
  835. </div>
  836. <div class="pager">
  837. <button class="pager-btn" id="prevPage" ${page<=1 ? 'disabled' : ''} aria-label="Previous page">‹</button>
  838. <span class="page-info">Page ${page} of ${tp}</span>
  839. <button class="pager-btn" id="nextPage" ${page>=tp ? 'disabled' : ''} aria-label="Next page">›</button>
  840. </div>
  841. `;
  842. // wire events
  843. document.getElementById('prevPage')?.addEventListener('click', () => { if (page > 1) { page--; renderProducts(); } });
  844. document.getElementById('nextPage')?.addEventListener('click', () => { if (page < tp) { page++; renderProducts(); } });
  845. const sel = document.getElementById('pageSizeSelect');
  846. if (sel) {
  847. sel.addEventListener('change', () => {
  848. const val = sel.value;
  849. pageSize = (val === 'all') ? PRODUCT_BASE.length : parseInt(val, 10);
  850. page = 1; // reset to first page when size changes
  851. renderProducts();
  852. });
  853. }
  854. }
  855. // Function to add/remove product from the state and manage its attributes
  856. function toggleProductSelection(itemId, isChecked, attributes = {}) {
  857. const index = selectedProductsWithAttributes.findIndex(p => p.item_id === itemId);
  858. if (isChecked) {
  859. // If selecting, ensure the product object exists in the array
  860. if (index === -1) {
  861. selectedProductsWithAttributes.push({
  862. item_id: itemId,
  863. mandatory_attrs: attributes
  864. });
  865. } else {
  866. // Update attributes if the product is already selected
  867. selectedProductsWithAttributes[index].mandatory_attrs = attributes;
  868. }
  869. } else {
  870. // If deselecting, remove the product object from the array
  871. if (index !== -1) {
  872. selectedProductsWithAttributes.splice(index, 1);
  873. }
  874. }
  875. updateSelectionInfo();
  876. }
  877. // Function to get the current mandatory attributes for a selected item
  878. function getSelectedAttributes(itemId) {
  879. const productEntry = selectedProductsWithAttributes.find(p => p.item_id === itemId);
  880. return productEntry ? productEntry.mandatory_attrs : {};
  881. }
  882. // Helper to check if a product is selected
  883. function isProductSelected(itemId) {
  884. return selectedProductsWithAttributes.some(p => p.item_id === itemId);
  885. }
  886. // Helper to check if a specific attribute/value is selected
  887. function isAttributeValueSelected(itemId, attrName, value) {
  888. const attrs = getSelectedAttributes(itemId);
  889. const values = attrs[attrName];
  890. return values ? values.includes(value) : false; // Default all selected when first loaded
  891. }
  892. // $('.attribute-select').select2({
  893. // placeholder: 'Select product attributes'
  894. // });
  895. function getAtributeList(){
  896. jQuery('#full-page-loader').show();
  897. try{
  898. fetch('/attr/products/attributes', {
  899. method: 'GET', // or 'POST' if your API expects POST
  900. headers: {
  901. 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
  902. }
  903. })
  904. .then(response => response.json())
  905. .then(data => {
  906. // console.log("data",data);
  907. let attributesData = data;
  908. // Step 1: Extract unique mandatory attribute names
  909. const mandatoryAttributes = [...new Set(
  910. attributesData
  911. .filter(attr => attr.is_mandatory === "Yes")
  912. .map(attr => attr.attribute_name)
  913. )];
  914. // Step 2: Populate the select element
  915. const $select = jQuery('#mandatory-attributes');
  916. $select.append(new Option("Select All", "select_all")); // Add "Select All" option first
  917. mandatoryAttributes.forEach(attr => {
  918. $select.append(new Option(attr, attr));
  919. });
  920. // Step 3: Initialize Select2 with placeholder
  921. // $select.select2({
  922. // placeholder: "Select mandatory attributes",
  923. // allowClear: true
  924. // });
  925. // Step 4: Handle 'Select All' logic
  926. $select.on('select2:select', function (e) {
  927. if (e.params.data.id === "select_all") {
  928. // Select all real options except "Select All"
  929. const allOptions = mandatoryAttributes;
  930. $select.val(allOptions).trigger('change');
  931. }
  932. });
  933. jQuery('#full-page-loader').hide();
  934. });
  935. }catch(err){
  936. console.log("err",err);
  937. jQuery('#full-page-loader').hide();
  938. }
  939. }
  940. document.addEventListener("DOMContentLoaded", function () {
  941. // Update span when range changes
  942. thresholdInput.addEventListener('input', function () {
  943. // console.log("this.value",this.value);
  944. thresholdValueDisplay.textContent = this.value;
  945. });
  946. });
  947. // Get threshold value when needed
  948. function getThreshold() {
  949. // console.log("parseFloat(thresholdInput.value)",parseFloat(thresholdInput.value));
  950. return parseFloat(thresholdInput.value);
  951. }