attr-column-mapping-setting.html 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. {% load static %}
  2. <!DOCTYPE html>
  3. <html lang="en">
  4. <head>
  5. <meta charset="UTF-8">
  6. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  7. <title>Column Mapping</title>
  8. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
  9. <script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
  10. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/source-sans-3@5.0.12/index.css"
  11. integrity="sha256-tXJfXfp6Ewt1ilPzLDtQnJV4hclT9XuaZUKyUvmyr+Q=" crossorigin="anonymous">
  12. <!--end::Fonts--><!--begin::Third Party Plugin(OverlayScrollbars)-->
  13. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/styles/overlayscrollbars.min.css"
  14. integrity="sha256-dSokZseQNT08wYEWiz5iLI8QPlKxG+TswNRD8k35cpg=" crossorigin="anonymous">
  15. <!--end::Third Party Plugin(OverlayScrollbars)--><!--begin::Third Party Plugin(Bootstrap Icons)-->
  16. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.min.css"
  17. integrity="sha256-Qsx5lrStHZyR9REqhUF8iQt73X06c8LGIUPzpOhwRrI=" crossorigin="anonymous">
  18. <!--end::Third Party Plugin(Bootstrap Icons)--><!--begin::Required Plugin(AdminLTE)-->
  19. <link rel="stylesheet" href="{% static './css/adminlte.css' %}"><!--end::Required Plugin(AdminLTE)--><!-- apexcharts -->
  20. <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/apexcharts@3.37.1/dist/apexcharts.css"
  21. integrity="sha256-4MX+61mt9NVvvuPjUWdUdyfZfxSB1/Rf9WtqRHgG5S0=" crossorigin="anonymous">
  22. <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
  23. <link rel="stylesheet" href="{% static './css/select2-bootstrap4.min.css' %}">
  24. <link rel="stylesheet" href="{% static './css/custom.css' %}">
  25. <style>
  26. /* General Container and Layout */
  27. body {
  28. background-color: #f8f9fa; /* Light grey background */
  29. }
  30. .mapping-container {
  31. max-width: 1200px;
  32. margin: 40px auto;
  33. background: #ffffff;
  34. padding: 30px;
  35. border-radius: 12px;
  36. box-shadow: 0 8px 25px rgba(0,0,0,0.08);
  37. }
  38. /* Uploaded Columns (Source) */
  39. #uploaded-columns-wrapper {
  40. border: 1px solid #dee2e6;
  41. border-radius: 6px;
  42. padding: 10px;
  43. min-height: 200px;
  44. background-color: #e9ecef; /* Slightly darker background for source */
  45. }
  46. .drag-item {
  47. padding: 8px 15px;
  48. margin: 8px 0;
  49. background: #007bff; /* Primary blue for drag items */
  50. color: white;
  51. border-radius: 4px;
  52. cursor: grab;
  53. transition: transform 0.1s, box-shadow 0.1s;
  54. font-weight: 500;
  55. display: inline-block;
  56. width: 100%;
  57. }
  58. .drag-item:hover {
  59. background: #0056b3;
  60. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  61. }
  62. /* System Columns (Destination/Drop Zones) */
  63. .drop-zone-container {
  64. padding: 15px;
  65. border: 1px solid #dee2e6;
  66. border-radius: 6px;
  67. min-height: 200px;
  68. }
  69. .drop-zone {
  70. min-height: 45px;
  71. padding: 10px;
  72. margin-bottom: 10px;
  73. border: 2px dashed #adb5bd; /* Subtler dashed border */
  74. border-radius: 6px;
  75. background: #f1f3f5; /* Light grey background for drop zone */
  76. font-weight: bold;
  77. color: #495057;
  78. display: flex;
  79. justify-content: space-between;
  80. align-items: center;
  81. transition: background-color 0.2s, border-color 0.2s;
  82. }
  83. .drop-zone.hover {
  84. border-color: #28a745; /* Green on hover */
  85. background: #e6ffed; /* Light green background on hover */
  86. }
  87. .drop-zone strong {
  88. color: #dc3545; /* Red for the mapped column name */
  89. font-weight: 600;
  90. padding: 4px 8px;
  91. background-color: #ffe8e8;
  92. border-radius: 4px;
  93. }
  94. .drop-zone .system-label {
  95. color: #495057;
  96. }
  97. .drop-zone .mapped-content {
  98. display: flex;
  99. align-items: center;
  100. }
  101. .drop-zone .mapped-content .reset-map {
  102. margin-left: 10px;
  103. cursor: pointer;
  104. color: #6c757d;
  105. font-size: 1.2rem;
  106. transition: color 0.2s;
  107. }
  108. .drop-zone .mapped-content .reset-map:hover {
  109. color: #dc3545;
  110. }
  111. /* Preview Table */
  112. .preview-table {
  113. margin-top: 30px;
  114. border-top: 2px solid #007bff;
  115. padding-top: 20px;
  116. }
  117. .preview-table th {
  118. background-color: #007bff;
  119. color: white;
  120. }
  121. </style>
  122. </head>
  123. <!-- <body> -->
  124. <body class="layout-fixed sidebar-expand-lg sidebar-mini app-loaded sidebar-collapse">
  125. <!--begin::App Wrapper-->
  126. <div class="app-wrapper"> <!--begin::Header-->
  127. {% include 'header.html' %}
  128. {% include 'sidebar.html' %}
  129. <main class="app-main"> <!--begin::App Content Header-->
  130. <div class="app-content-header"> <!--begin::Container-->
  131. <div class="container-fluid"> <!--begin::Row-->
  132. <div class="row">
  133. <div class="col-sm-6">
  134. <h3 class="mb-0">⚙️ Attribute Extraction File Column Mapping</h3>
  135. </div>
  136. <div class="col-sm-6">
  137. <ol class="breadcrumb float-sm-end">
  138. <li class="breadcrumb-item"><a href="{% url 'file-upload' %}">Home</a></li>
  139. <li class="breadcrumb-item active" aria-current="page"><a href="{% url 'content-scorecard' %}"></a>
  140. ⚙️ Attribute Extraction File Column Mapping</a>
  141. </li>
  142. </ol>
  143. </div>
  144. </div> <!--end::Row-->
  145. </div> <!--end::Container-->
  146. </div>
  147. <div class="app-content"> <!--begin::Container-->
  148. <div class="container-fluid"> <!-- Info boxes -->
  149. <div id="full-page-loader" style="display: none;">
  150. <div class="loader-overlay">
  151. <div class="spinner-border text-light" role="status">
  152. <!-- <span class="sr-only">Loading...</span> -->
  153. </div>
  154. </div>
  155. </div>
  156. <div class="mapping-container">
  157. <!-- <h2 class="text-center mb-4 text-primary">⚙️ Data Attribute Mapping Tool</h2> -->
  158. <p class="text-center text-muted mb-4"><strong>Step 1:</strong> Upload your file. <strong>Step 2:</strong> Drag columns from the left to their corresponding system fields on the right. <strong>Step 3:</strong> Save Mapping.</p>
  159. <div class="mb-4 p-3 bg-light rounded border">
  160. <label for="fileUpload" class="form-label fs-5 mb-2">📥 Upload Source File (CSV/Excel)</label>
  161. <input type="file" class="form-control" id="fileUpload" accept=".csv, .xls, .xlsx">
  162. </div>
  163. <div class="row">
  164. <div class="col-md-5">
  165. <h5 class="mb-3 text-secondary">Source File Columns (Drag)</h5>
  166. <div id="uploaded-columns-wrapper">
  167. <div id="uploaded-columns" class="list-group">
  168. <p class="text-center text-muted" id="upload-placeholder">Upload a file to see columns here.</p>
  169. </div>
  170. </div>
  171. </div>
  172. <div class="col-md-7">
  173. <h5 class="mb-3 text-success">Target System Attributes (Drop)</h5>
  174. <div id="system-columns" class="drop-zone-container">
  175. </div>
  176. </div>
  177. </div>
  178. <!-- <div class="preview-table">
  179. <h5 class="text-primary">👀 Data Preview (First 5 Rows)</h5>
  180. <div class="table-responsive">
  181. <table class="table table-striped table-bordered">
  182. <thead>
  183. <tr id="preview-header">
  184. <th colspan="100%" class="text-center text-white bg-secondary">No data loaded for preview.</th>
  185. </tr>
  186. </thead>
  187. <tbody id="preview-body">
  188. <tr>
  189. <td colspan="100%" class="text-center text-muted">A sample of your file data will appear here upon upload.</td>
  190. </tr>
  191. </tbody>
  192. </table>
  193. </div>
  194. </div> -->
  195. <div class="text-end mt-4 pt-3 border-top">
  196. <button class="btn btn-outline-secondary me-2" id="resetBtn">↩️ Reset Mapping</button>
  197. <button class="btn btn-success" id="saveBtn">✅ Save & Confirm Mapping</button>
  198. </div>
  199. </div>
  200. </div> <!--end::Container-->
  201. </div> <!--end::App Content-->
  202. </main> <!--end::App Main--> <!--begin::Footer-->
  203. {% include 'footer.html' %}
  204. </div>
  205. <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  206. <script src="https://cdn.jsdelivr.net/npm/overlayscrollbars@2.3.0/browser/overlayscrollbars.browser.es6.min.js"
  207. integrity="sha256-H2VM7BKda+v2Z4+DRy69uknwxjyDRhszjXFhsL4gD3w=" crossorigin="anonymous"></script>
  208. <!--end::Third Party Plugin(OverlayScrollbars)--><!--begin::Required Plugin(popperjs for Bootstrap 5)-->
  209. <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
  210. integrity="sha256-whL0tQWoY1Ku1iskqPFvmZ+CHsvmRWx/PIoEvIeWh4I=" crossorigin="anonymous"></script>
  211. <!--end::Required Plugin(popperjs for Bootstrap 5)--><!--begin::Required Plugin(Bootstrap 5)-->
  212. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.min.js"
  213. integrity="sha256-YMa+wAM6QkVyz999odX7lPRxkoYAan8suedu4k2Zur8=" crossorigin="anonymous"></script>
  214. <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
  215. <!--end::Required Plugin(Bootstrap 5)--><!--begin::Required Plugin(AdminLTE)-->
  216. <script src="{% static './js/adminlte.js' %}"></script>
  217. <!--end::Required Plugin(AdminLTE)--><!--begin::OverlayScrollbars Configure-->
  218. <script>
  219. const SELECTOR_SIDEBAR_WRAPPER = ".sidebar-wrapper";
  220. const Default = {
  221. scrollbarTheme: "os-theme-light",
  222. scrollbarAutoHide: "leave",
  223. scrollbarClickScroll: true,
  224. };
  225. document.addEventListener("DOMContentLoaded", function () {
  226. const sidebarWrapper = document.querySelector(SELECTOR_SIDEBAR_WRAPPER);
  227. if (
  228. sidebarWrapper &&
  229. typeof OverlayScrollbarsGlobal?.OverlayScrollbars !== "undefined"
  230. ) {
  231. OverlayScrollbarsGlobal.OverlayScrollbars(sidebarWrapper, {
  232. scrollbars: {
  233. theme: Default.scrollbarTheme,
  234. autoHide: Default.scrollbarAutoHide,
  235. clickScroll: Default.scrollbarClickScroll,
  236. },
  237. });
  238. }
  239. });
  240. </script>
  241. <script>
  242. const uploadedColumns = [];
  243. const systemColumns = ["PRODUCT NAME", "ITEM ID", "PRODUCT TYPE", "Product Short Description", "Product Long Description", "image_path"];
  244. let globalHeaders = []; // To store headers for preview
  245. $(document).ready(function() {
  246. // --- Initial Drop Zone Setup ---
  247. systemColumns.forEach(col => {
  248. $("#system-columns").append(`
  249. <div class="drop-zone" data-system="${col}">
  250. <span class="system-label">${col}</span>
  251. <span class="mapped-content">
  252. <span class="text-muted">No column mapped</span>
  253. </span>
  254. </div>
  255. `);
  256. });
  257. // --- File Upload Handler (Supports CSV/Excel) ---
  258. $("#fileUpload").on("change", function(e) {
  259. const file = e.target.files[0];
  260. if (!file) return;
  261. const reader = new FileReader();
  262. reader.onload = function(event) {
  263. let jsonData;
  264. try {
  265. const data = new Uint8Array(event.target.result);
  266. const workbook = XLSX.read(data, { type: "array" });
  267. const sheetName = workbook.SheetNames[0];
  268. const sheet = workbook.Sheets[sheetName];
  269. jsonData = XLSX.utils.sheet_to_json(sheet, { header: 1 });
  270. } catch (err) {
  271. alert("Error reading file. Ensure it is a valid CSV or Excel file.");
  272. console.error(err);
  273. return;
  274. }
  275. const headers = jsonData[0];
  276. const sampleRows = jsonData.slice(1, 6);
  277. globalHeaders = headers;
  278. // Populate uploaded columns
  279. $("#uploaded-columns").empty();
  280. headers.forEach(col => {
  281. if (col && String(col).trim() !== "") {
  282. $("#uploaded-columns").append(`<div class="drag-item" draggable="true" data-col="${col}">${col}</div>`);
  283. }
  284. });
  285. // Reset mappings and remove placeholder text
  286. $("#upload-placeholder").hide();
  287. $("#resetBtn").click(); // Reset any previous mappings upon new file upload
  288. // Populate preview table
  289. $("#preview-header").html(headers.map(h => `<th scope="col">${h}</th>`).join(''));
  290. $("#preview-body").empty();
  291. if (sampleRows.length > 0) {
  292. sampleRows.forEach(row => {
  293. let tr = "<tr>";
  294. headers.forEach((h, i) => tr += `<td>${row[i] || "-"}</td>`);
  295. tr += "</tr>";
  296. $("#preview-body").append(tr);
  297. });
  298. } else {
  299. $("#preview-body").html('<tr><td colspan="100%" class="text-center text-warning">File uploaded, but no data rows found.</td></tr>');
  300. }
  301. // Re-enable drag-and-drop for new items
  302. enableDragDrop();
  303. };
  304. reader.readAsArrayBuffer(file);
  305. });
  306. // --- Drag and Drop Logic ---
  307. function enableDragDrop() {
  308. // Drag Start
  309. $(".drag-item").off("dragstart").on("dragstart", function(e) {
  310. e.originalEvent.dataTransfer.setData("text/plain", $(this).data("col"));
  311. $(this).addClass("dragging-active");
  312. });
  313. // Drag End (optional, for cleanup)
  314. $(".drag-item").off("dragend").on("dragend", function() {
  315. $(this).removeClass("dragging-active");
  316. });
  317. // Drop Zone Events
  318. $(".drop-zone").off("dragover").on("dragover", function(e) {
  319. e.preventDefault();
  320. $(this).addClass("hover");
  321. e.originalEvent.dataTransfer.dropEffect = "move";
  322. }).off("dragleave").on("dragleave", function() {
  323. $(this).removeClass("hover");
  324. }).off("drop").on("drop", function(e) {
  325. e.preventDefault();
  326. $(this).removeClass("hover");
  327. const col = e.originalEvent.dataTransfer.getData("text/plain");
  328. const systemCol = $(this).data("system");
  329. // 1. ONE-TO-ONE MAPPING CHECK: Is this uploaded column already mapped?
  330. let isAlreadyMapped = false;
  331. $(".drop-zone").each(function() {
  332. if ($(this).data("mapped") === col) {
  333. isAlreadyMapped = true;
  334. return false; // Exit the loop early
  335. }
  336. });
  337. if (isAlreadyMapped) {
  338. alert(`The column "${col}" is already mapped to another system attribute. Please unmap it first.`);
  339. return; // Stop the drop action
  340. }
  341. // 2. UNMAP PREVIOUS COLUMN (if the drop zone already has a value)
  342. const previousMappedCol = $(this).data("mapped");
  343. if (previousMappedCol) {
  344. // Show the previously mapped column item again in the source list
  345. $(`.drag-item[data-col="${previousMappedCol}"]`).show();
  346. }
  347. // 3. APPLY NEW MAPPING
  348. $(this).find('.mapped-content').html(`
  349. <strong>${col}</strong>
  350. <span class="reset-map" title="Unmap Column" data-system="${systemCol}">✖️</span>
  351. `);
  352. $(this).data("mapped", col);
  353. $(this).removeClass("border-danger").addClass("border-success");
  354. // 4. Hide the newly mapped column item from the uploaded list
  355. $(`.drag-item[data-col="${col}"]`).hide();
  356. });
  357. }
  358. // --- Reset Single Mapping ---
  359. $(document).on("click", ".reset-map", function() {
  360. const dropZone = $(this).closest(".drop-zone");
  361. const mappedCol = dropZone.data("mapped");
  362. // Reset drop zone appearance and data
  363. dropZone.find('.mapped-content').html(`<span class="text-muted">No column mapped</span>`);
  364. dropZone.removeData("mapped");
  365. dropZone.removeClass("border-success border-danger");
  366. // Show the uploaded column item again
  367. if (mappedCol) {
  368. $(`.drag-item[data-col="${mappedCol}"]`).show();
  369. }
  370. });
  371. // --- Reset All Button ---
  372. $("#resetBtn").click(function() {
  373. $(".drop-zone").each(function() {
  374. $(this).find('.mapped-content').html(`<span class="text-muted">No column mapped</span>`);
  375. $(this).removeData("mapped");
  376. $(this).removeClass("border-success border-danger");
  377. });
  378. // Show all uploaded columns again
  379. $(".drag-item").show();
  380. });
  381. // --- Save Button with Validation ---
  382. $("#saveBtn").click(function() {
  383. let mapping = {};
  384. let missingMaps = [];
  385. let allMapped = true;
  386. $(".drop-zone").each(function() {
  387. const systemCol = $(this).data("system");
  388. const mappedCol = $(this).data("mapped");
  389. mapping[systemCol] = mappedCol || null;
  390. if (!mappedCol) {
  391. allMapped = false;
  392. missingMaps.push(systemCol);
  393. $(this).addClass("border-danger");
  394. } else {
  395. $(this).removeClass("border-danger");
  396. }
  397. });
  398. if (!allMapped) {
  399. alert(`Please map all system columns before saving. Missing: ${missingMaps.join(', ')}`);
  400. return;
  401. }
  402. console.log("Final Mapping:", mapping);
  403. alert(`Mapping saved successfully! System Columns Mapped: ${Object.keys(mapping).length}`);
  404. });
  405. // Initial call to enable drag/drop
  406. enableDragDrop();
  407. });
  408. </script>
  409. </body>
  410. </html>