Last active
January 1, 2026 09:52
-
-
Save manfred-hinsch/0f49c882928fe985af4f9ccc78dd09c3 to your computer and use it in GitHub Desktop.
smart-trantypes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* smart-trantype.css - visual styles */ | |
| #main.col620 { | |
| width: 100% !important; | |
| } | |
| /* table */ | |
| #transaction-table { | |
| border-collapse: collapse; | |
| width: 100%; | |
| background: #fff; | |
| font-family: inherit; | |
| } | |
| #transaction-table th:not(.numeric), | |
| #transaction-table td:not(.numeric) { | |
| border: 1px solid #ccc; | |
| padding: 8px; | |
| text-align: left; | |
| font-size: 1.3rem; | |
| } | |
| #transaction-table th.numeric, | |
| #transaction-table td.numeric { | |
| border: 1px solid #ccc; | |
| padding: 8px; | |
| text-align: right; | |
| font-size: 1.3rem; | |
| } | |
| /* row states */ | |
| #transaction-table tbody tr.blank-row { | |
| background: #f5f5f5; | |
| transition: background-color .2s ease; | |
| } | |
| #transaction-table tbody tr.assigned-row { | |
| background: #e8f8ee; | |
| transition: background-color .25s ease; | |
| } | |
| #transaction-table tbody tr.changed-row { | |
| background: #fff6e4; | |
| transition: background-color .25s ease; | |
| } | |
| #transaction-table tbody tr.pre-assigned { | |
| box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.03); | |
| } | |
| /* small icons/buttons */ | |
| .save-tran-type-btn { | |
| border: none; | |
| background: transparent; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| } | |
| .save-tran-type-btn[disabled] { | |
| opacity: 0.6; | |
| cursor: default; | |
| } | |
| .undo-tran-type-btn { | |
| border: none; | |
| background: transparent; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| margin-left: 6px; | |
| } | |
| #save-all-btn { | |
| padding: 0px 0px; | |
| background: transparent; | |
| border: none; | |
| cursor: pointer; | |
| font-size: 17px; | |
| transition: background-color 0.3s ease; | |
| } | |
| /* ============================================================ | |
| SMARTTRANTYPE POPUPS — CLEAN + ISOLATED | |
| ============================================================ */ | |
| /* ---------- OVERLAYS ---------- */ | |
| #pattern-popup-overlay, | |
| #stt-tran-type-popup-overlay { | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0, 0, 0, 0.45); | |
| z-index: 99990; | |
| display: none; | |
| } | |
| /* ---------- BASE POPUP CONTAINER ---------- */ | |
| #pattern-popup, | |
| #stt-tran-type-popup { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| z-index: 99991; | |
| background: #fff; | |
| border: 1px solid #888; | |
| border-radius: 8px; | |
| box-shadow: 0 8px 25px rgba(0, 0, 0, 0.35); | |
| padding: 12px; | |
| /* width: 25%; */ | |
| max-width: 760px; | |
| max-height: 80vh; | |
| display: none; | |
| overflow: hidden; | |
| font-family: system-ui, -apple-system, "Segoe UI", Roboto, Arial; | |
| } | |
| /* ---------- HEADER ---------- */ | |
| #pattern-popup .popup-header, | |
| #stt-tran-type-popup .popup-header { | |
| padding: 12px 16px; | |
| background: #f5f5f5; | |
| border-bottom: 1px solid #e0e0e0; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .popup-title { | |
| font-size: 18px; | |
| font-weight: 600; | |
| } | |
| /* Close button */ | |
| .popup-close { | |
| background: none; | |
| border: none; | |
| font-size: 22px; | |
| cursor: pointer; | |
| } | |
| /* ---------- BODY ---------- */ | |
| #pattern-popup .popup-body, | |
| #stt-tran-type-popup .popup-body { | |
| padding: 14px; | |
| overflow-y: auto; | |
| flex: 1; | |
| } | |
| /* Highlighted pattern text */ | |
| #popup-description .highlighted { | |
| background: yellow; | |
| padding: 2px 4px; | |
| border-radius: 3px; | |
| font-weight: 600; | |
| } | |
| /* ---------- FOOTER ---------- */ | |
| /* #pattern-popup .popup-footer, */ | |
| #stt-tran-type-popup .popup-footer { | |
| padding: 12px 14px; | |
| border-top: 1px solid #e0e0e0; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| /* Buttons */ | |
| .btn-primary { | |
| background: #0073aa; | |
| border: none; | |
| padding: 7px 12px; | |
| color: white; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| .btn-secondary { | |
| background: #e0e0e0; | |
| border: none; | |
| padding: 7px 12px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| color: #222; | |
| } | |
| .btn-danger { | |
| background: #c0392b; | |
| border: none; | |
| padding: 7px 12px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| color: white; | |
| } | |
| /* Pattern popup footer: two-column layout */ | |
| /* .pattern-popup-footer { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| gap: 1rem; | |
| } */ | |
| /* Left side (font size control) */ | |
| /* .pattern-popup-footer .font-size-box { | |
| flex: 1; | |
| } */ | |
| /* Right side (Accept / Cancel / Clear buttons) */ | |
| /* .pattern-popup-footer .popup-buttons { */ | |
| .stt-popup-buttons { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| } | |
| /* Pattern popup should use vertical stacking layout */ | |
| /* #pattern-popup .popup-body { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.2rem; | |
| } */ | |
| /* Each row in the pattern popup */ | |
| #pattern-popup .pattern-row { | |
| width: 100%; | |
| } | |
| /* Pattern Popup Buttons - horizontal layout */ | |
| .pattern-button-row { | |
| display: inline-flex; | |
| justify-content: flex-start; | |
| /* or center if you prefer */ | |
| gap: 0.5rem; | |
| /* spacing between buttons */ | |
| margin-top: 1rem; | |
| } | |
| /* pattern-row pattern-button-row */ | |
| .pattern-button-row button { | |
| display: flex; | |
| /* ensure buttons do not take full width */ | |
| margin: 0; | |
| /* reset any previous margin */ | |
| } | |
| /* Ensure pattern popup buttons appear horizontally */ | |
| #pattern-popup .popup-footer .popup-buttons, | |
| #pattern-popup .pattern-popup-footer .popup-buttons, | |
| .pattern-popup .popup-buttons { | |
| display: flex !important; | |
| flex-direction: row !important; | |
| gap: 8px; | |
| align-items: center; | |
| justify-content: flex-end; | |
| } | |
| /* Make sure the font-size control is on its own line and smaller */ | |
| #pattern-popup .font-size-box { | |
| display: block; | |
| margin-right: 12px; | |
| } | |
| /* Slight spacing cleanup for the footer */ | |
| #pattern-popup .popup-footer { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 12px; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| @media (max-width: 500px) { | |
| #pattern-popup, | |
| #stt-tran-type-popup { | |
| width: 95%; | |
| max-width: 95%; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* smart-trantype.js | |
| Single namespace architecture: SmartTrantype | |
| Advanced pattern_sets engine (matches description -> pattern_sets) | |
| Popup Type 1 for tran-type management | |
| Popup Type 2 for pattern management | |
| */ | |
| var SmartTrantype = SmartTrantype || {}; | |
| // ------------------------------ | |
| // SmartTrantype.Cache — central cache store | |
| // ------------------------------ | |
| SmartTrantype.Cache = { | |
| rows: [], | |
| map: {}, | |
| get: function (rowId) { | |
| return this.map[rowId] || null; | |
| }, | |
| set: function (rowId, data) { | |
| if (!this.map[rowId]) { | |
| this.map[rowId] = data; | |
| this.rows.push(data); | |
| } else { | |
| Object.assign(this.map[rowId], data); | |
| } | |
| } | |
| }; | |
| (function (window, $) { | |
| 'use strict'; | |
| // Row states (single source of truth) | |
| const STT_STATE = { | |
| BLANK: 'blank', | |
| CHANGED: 'changed', | |
| ASSIGNED: 'assigned' | |
| }; | |
| // internal locks | |
| let _refreshLockActive = false; | |
| let _initTranTypeInputsLock = false; | |
| // small session cache (client-side) | |
| window.sttClientCache = window.sttClientCache || { | |
| tran_types: [], | |
| pattern_sets: [] | |
| }; | |
| if (!window._stt_inputs_listener_attached) { | |
| window._stt_inputs_listener_attached = true; | |
| $(document).on('change', '#transaction-table .tran-type-input', function () { | |
| SmartTrantype.handleInputChange(this); | |
| }); | |
| } | |
| // ---------------------------- | |
| // Utils | |
| // ---------------------------- | |
| SmartTrantype.Utils = { | |
| escapeRegExp: function (s) { | |
| return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| }, | |
| highlightPatterns: function (container, patterns, color) { | |
| if (!container) return; | |
| const text = container.innerText || ''; | |
| if (!patterns || patterns.length === 0) { | |
| container.innerHTML = $('<div/>').text(text).html(); | |
| return; | |
| } | |
| const sorted = [...patterns].filter(Boolean).sort((a, b) => b.length - a.length); | |
| const regex = new RegExp("(" + sorted.map(this.escapeRegExp).join("|") + ")", "gi"); | |
| const html = text.replace(regex, match => `<span class="stt-highlighted" style="background-color:${color};">${match}</span>`); | |
| container.innerHTML = html; | |
| }, | |
| highlightDuplicates: function (container, patterns) { | |
| if (!container || !container.parentNode) return; | |
| const parent = container.parentNode; | |
| // remove any previous warning | |
| Array.from(parent.querySelectorAll(".duplicate-warning")).forEach(n => n.remove()); | |
| if (!patterns || patterns.length === 0) return; | |
| // Normalize and compute counts | |
| const normalized = patterns.map(p => (p || '').toString().trim()).filter(Boolean); | |
| const counts = normalized.reduce((acc, p) => { | |
| const k = p.toLowerCase(); | |
| acc[k] = (acc[k] || 0) + 1; | |
| return acc; | |
| }, {}); | |
| // exact duplicates | |
| const exactRemoved = Object.keys(counts).filter(k => counts[k] > 1); | |
| // subset/contained duplicates (present in normalized but omitted by dedupe) | |
| const lowerClean = [...new Set(normalized.map(p => p.toLowerCase()))]; | |
| const subsetRemoved = normalized | |
| .map(p => p.toLowerCase()) | |
| .filter(p => lowerClean.indexOf(p) === -1 && exactRemoved.indexOf(p) === -1); | |
| if (exactRemoved.length || subsetRemoved.length) { | |
| const warn = document.createElement("div"); | |
| warn.className = "duplicate-warning"; | |
| let parts = []; | |
| if (exactRemoved.length) parts.push("duplicates: " + exactRemoved.join(", ")); | |
| if (subsetRemoved.length) parts.push("contained within longer ones: " + subsetRemoved.join(", ")); | |
| warn.innerText = "⚠ These patterns will be ignored — " + parts.join("; "); | |
| parent.insertBefore(warn, container.nextSibling); | |
| } | |
| }, | |
| getHighlightedSelections: function (container) { | |
| if (!container) return []; | |
| return Array.from(container.querySelectorAll(".stt-highlighted")).map(el => el.textContent.trim()).filter(Boolean); | |
| }, | |
| clearSelections: function (container) { | |
| if (!container) return; | |
| container.textContent = container.textContent; // removes spans | |
| }, | |
| dedupeAndClean: function (arr) { | |
| return [...new Set((arr || []).map(s => (s || '').toString().trim()).filter(Boolean))]; | |
| }, | |
| // ------------------------------------------------------ | |
| // Highlight one or more patterns inside an element | |
| // ------------------------------------------------------ | |
| highlightPatterns(container, patterns, color = 'yellow') { | |
| if (!container || !patterns || !patterns.length) return; | |
| const text = container.innerText; | |
| let html = text; | |
| patterns.forEach(p => { | |
| if (!p) return; | |
| const esc = p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| const re = new RegExp(esc, 'gi'); | |
| html = html.replace(re, match => `<span class="stt-highlighted" style="background:${color};">${match}</span>`); | |
| }); | |
| container.innerHTML = html; | |
| }, | |
| // ------------------------------------------------------ | |
| // Remove all highlights (reset to plain text) | |
| // ------------------------------------------------------ | |
| clearSelections(container) { | |
| if (!container) return; | |
| const text = container.innerText; | |
| container.innerHTML = text; | |
| }, | |
| // ------------------------------------------------------ | |
| // Extract highlighted selections as array of strings | |
| // ------------------------------------------------------ | |
| getHighlightedSelections(container) { | |
| if (!container) return []; | |
| const nodes = container.querySelectorAll('.stt-highlighted'); | |
| return Array.from(nodes).map(n => n.innerText.trim()); | |
| }, | |
| // ------------------------------------------------------ | |
| // Remove duplicates + trim + normalize | |
| // ------------------------------------------------------ | |
| dedupeAndClean(arr) { | |
| if (!Array.isArray(arr)) return []; | |
| return [...new Set(arr.map(a => a.trim()).filter(a => a.length))]; | |
| }, | |
| // ------------------------------------------------------ | |
| // Highlight duplicates differently (orange) | |
| // ------------------------------------------------------ | |
| highlightDuplicates(container, arr) { | |
| if (!container || !arr || !arr.length) return; | |
| // Calculate duplicates | |
| const counts = arr.reduce((a, c) => { | |
| a[c] = (a[c] || 0) + 1; | |
| return a; | |
| }, {}); | |
| const duplicates = Object.keys(counts).filter(k => counts[k] > 1); | |
| if (!duplicates.length) return; | |
| duplicates.forEach(d => { | |
| const esc = d.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| const re = new RegExp(esc, 'gi'); | |
| container.innerHTML = container.innerHTML.replace( | |
| re, | |
| match => `<span class="stt-highlighted" style="background:orange;">${match}</span>` | |
| ); | |
| }); | |
| }, | |
| }; | |
| SmartTrantype.Row = { | |
| applyStateToRow: function ($row, state) { | |
| $row.removeClass('blank-row changed-row assigned-row'); | |
| switch (state) { | |
| case STT_STATE.BLANK: | |
| $row.addClass('blank-row'); | |
| $row.find('.save-tran-type-btn, .undo-tran-type-btn').hide(); | |
| break; | |
| case STT_STATE.CHANGED: | |
| $row.addClass('changed-row'); | |
| $row.find('.save-tran-type-btn').show(); | |
| $row.find('.undo-tran-type-btn').show(); | |
| break; | |
| case STT_STATE.ASSIGNED: | |
| $row.addClass('assigned-row'); | |
| $row.find('.save-tran-type-btn, .undo-tran-type-btn').hide(); | |
| break; | |
| } | |
| }, | |
| markAsChanged: function ($row) { | |
| if (!$row || !$row.length) return; | |
| const rowId = $row.data('txn-id'); | |
| const entry = window.sttCache.transactions.find(r => r.id === rowId); | |
| if (!entry) return; | |
| // Cache flags only | |
| entry.changed = true; | |
| entry.assigned = false; | |
| entry.blank = false; | |
| // DOM only | |
| $row.removeClass('assigned-row blank-row').addClass('changed-row'); | |
| $row.find('.save-tran-type-btn').show(); | |
| $row.find('.undo-tran-type-btn').show(); | |
| }, | |
| markAsAssigned: function ($row) { | |
| if (!$row || !$row.length) return; | |
| const rowId = $row.data('txn-id'); | |
| const entry = window.sttCache.transactions.find(r => r.id === rowId); | |
| if (!entry) return; | |
| entry.changed = false; | |
| entry.assigned = true; | |
| entry.blank = false; | |
| $row.removeClass('changed-row blank-row').addClass('assigned-row'); | |
| $row.find('.save-tran-type-btn').hide(); | |
| $row.find('.undo-tran-type-btn').hide(); | |
| }, | |
| markAsBlank: function ($row) { | |
| if (!$row || !$row.length) return; | |
| const rowId = $row.data('txn-id'); | |
| const entry = window.sttCache.transactions.find(r => r.id === rowId); | |
| if (!entry) return; | |
| // Update cache flags | |
| entry.changed = false; | |
| entry.assigned = false; | |
| entry.blank = true; | |
| // Update DOM | |
| $row.removeClass('changed-row assigned-row').addClass('blank-row'); | |
| $row.find('.save-tran-type-btn').hide(); | |
| $row.find('.undo-tran-type-btn').hide(); | |
| } | |
| }; | |
| // ------------------------------ | |
| // Handle tran-type input changes | |
| // ------------------------------ | |
| SmartTrantype.handleInputChange = function (inputEl) { | |
| const $input = $(inputEl); | |
| if (!$input.length) return; | |
| const $row = $input.closest('tr'); | |
| const rowId = $row.data('txn-id'); | |
| if (!rowId) return; | |
| const entry = window.sttCache.transactions.find(r => r.id === rowId); | |
| if (!entry) return; | |
| // New values | |
| const newText = $input.val(); | |
| const newId = $input.data('tran-type-id') || null; | |
| // Update cache | |
| entry.tran_type = newText; | |
| entry.tran_type_id = newId; | |
| // Determine state | |
| const origText = entry.original_tran_type; | |
| const origId = entry.original_tran_type_id; | |
| const isBlank = (!newText || newText.trim() === ''); | |
| const isChanged = (newText !== origText) || (newId !== origId); | |
| if (isBlank) { | |
| SmartTrantype.Row.markAsBlank($row); | |
| } else if (isChanged) { | |
| SmartTrantype.Row.markAsChanged($row); | |
| } else { | |
| SmartTrantype.Row.markAsAssigned($row); | |
| } | |
| }; | |
| // ------------------------------ | |
| // Attach input listeners (blur only) | |
| // ------------------------------ | |
| if (!window._stt_inputs_listener_attached) { | |
| window._stt_inputs_listener_attached = true; | |
| // Trigger only on blur (user leaves input) | |
| $(document).on('blur', '#transaction-table .tran-type-input', function () { | |
| const $input = $(this); | |
| const $row = $input.closest('tr'); | |
| const rowId = $row.data('txn-id'); | |
| if (!rowId) return; | |
| const entry = window.sttCache.transactions.find(r => r.id === rowId); | |
| if (!entry) return; | |
| SmartTrantype.handleInputChange($input[0]); | |
| // Show popup only if meaningful change occurred | |
| if (entry.tran_type !== entry.original_tran_type || | |
| entry.tran_type_id !== entry.original_tran_type_id) { | |
| if (SmartTrantype.Popup?.showTranTypePopupForRow) { | |
| SmartTrantype.Popup.showTranTypePopupForRow($row, $input); | |
| } | |
| } | |
| }); | |
| } | |
| // ------------------------------ | |
| // Popup management | |
| // ------------------------------ | |
| SmartTrantype.Popup = { | |
| activeRow: null, | |
| activeEntry: null, | |
| showTranTypePopupForRow: function ($row, $input) { | |
| if (!($row instanceof jQuery)) $row = $($row); | |
| if (!($input instanceof jQuery)) $input = $($input); | |
| // Remove any existing popup/overlay | |
| $('.tran-popup-overlay, #tran-type-popup').remove(); | |
| const overlay = $('<div class="tran-popup-overlay"></div>'); | |
| const popup = $(` | |
| <div id="tran-type-popup" role="dialog" aria-modal="true" class="pattern-popup" style="display:none;"> | |
| <div class="popup-header"> | |
| <strong>Transaction Type Options</strong> | |
| <button class="popup-close" aria-label="Close">×</button> | |
| </div> | |
| <p class="popup-body" style="margin-bottom:8px;">Choose what to do with this transaction:</p> | |
| <div style="display:flex;flex-direction:column;gap:8px;"> | |
| <button class="popup-btn btn-secondary" data-action="create">Create New Transaction Type</button> | |
| <button class="popup-btn btn-secondary" data-action="keep">Keep / Adjust This Transaction Type</button> | |
| <button class="popup-btn btn-secondary" data-action="remove">Clear Type For This Transaction</button> | |
| </div> | |
| <div style="text-align:center; margin-top:8px"> | |
| <button class="popup-close btn-primary">Cancel</button> | |
| </div> | |
| </div> | |
| `); | |
| // Store references | |
| SmartTrantype.Popup.activeRow = $row; | |
| SmartTrantype.Popup.activeEntry = window.sttCache.transactions.find(r => r.id === $row.data('txn-id')); | |
| $('body').append(overlay, popup); | |
| // Show popup | |
| overlay.fadeIn(120); | |
| popup.fadeIn(120); | |
| // Bind popup buttons | |
| popup.find('.popup-btn').off('click').on('click', function () { | |
| const action = $(this).data('action'); | |
| if (action === 'create') SmartTrantype.Popup.applyCreate(); | |
| else if (action === 'keep') SmartTrantype.Popup.applyKeep(); | |
| else if (action === 'remove') SmartTrantype.Popup.applyRemove(); | |
| }); | |
| // Bind close | |
| popup.find('.popup-close, .tran-popup-overlay').off('click').on('click', function () { | |
| SmartTrantype.Popup.close(); | |
| }); | |
| }, | |
| close: function () { | |
| $('#tran-type-popup, .tran-popup-overlay').fadeOut(120, function () { $(this).remove(); }); | |
| SmartTrantype.Popup.activeRow = null; | |
| SmartTrantype.Popup.activeEntry = null; | |
| }, | |
| applyCreate: function () { | |
| SmartTrantype.Popup.close(); | |
| }, | |
| applyKeep: function () { | |
| const entry = SmartTrantype.Popup.activeEntry; | |
| const $row = SmartTrantype.Popup.activeRow; | |
| if (!entry || !$row) return; | |
| entry.tran_type = entry.original_tran_type; | |
| entry.tran_type_id = entry.original_tran_type_id ?? null; | |
| const $input = $row.find('.tran-type-input'); | |
| $input.val(entry.tran_type).data('tran-type-id', entry.tran_type_id); | |
| if (entry.tran_type) SmartTrantype.Row.markAsAssigned($row); | |
| else SmartTrantype.Row.markAsBlank($row); | |
| SmartTrantype.Popup.close(); | |
| if (SmartTrantype.Autocomplete?.refresh) SmartTrantype.Autocomplete.refresh($row, entry.tran_type); | |
| }, | |
| applyRemove: function () { | |
| const entry = SmartTrantype.Popup.activeEntry; | |
| const $row = SmartTrantype.Popup.activeRow; | |
| if (!entry || !$row) return; | |
| entry.tran_type = ''; | |
| entry.tran_type_id = null; | |
| const $input = $row.find('.tran-type-input'); | |
| $input.val('').removeData('tran-type-id'); | |
| SmartTrantype.Row.markAsBlank($row); | |
| SmartTrantype.Popup.close(); | |
| if (SmartTrantype.Autocomplete?.refresh) SmartTrantype.Autocomplete.refresh($row, ''); | |
| } | |
| }; | |
| /* ---------------------------- | |
| * SmartTrantype.Pattern | |
| * ---------------------------- */ | |
| SmartTrantype.Pattern = (function () { | |
| // Local safe fallbacks if SmartTrantype.Utils is missing | |
| const Utils = (SmartTrantype && SmartTrantype.Utils) ? SmartTrantype.Utils : { | |
| // dedupe and clean: trim, remove empties, unique | |
| dedupeAndClean(arr) { | |
| if (!Array.isArray(arr)) return []; | |
| return [...new Set(arr.map(a => (a || '').toString().trim()).filter(Boolean))]; | |
| }, | |
| // highlightPatterns: replace text with spans (orange/yellow) | |
| highlightPatterns(container, patterns = [], color = 'yellow') { | |
| if (!container) return; | |
| const text = container.innerText || ''; | |
| if (!patterns || patterns.length === 0) { container.textContent = text; return; } | |
| const sorted = [...patterns].filter(Boolean).sort((a, b) => b.length - a.length); | |
| const esc = s => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| const regex = new RegExp("(" + sorted.map(esc).join("|") + ")", "gi"); | |
| const html = (text || '').replace(regex, match => | |
| `<span class="stt-highlighted highlighted" style="background-color:${color};">${match}</span>` | |
| ); | |
| container.innerHTML = html; | |
| }, | |
| // highlightDuplicates: shows a brief warning about duplicates (keeps simple) | |
| highlightDuplicates(container, patterns = []) { | |
| if (!container || !container.parentNode) return; | |
| const parent = container.parentNode; | |
| Array.from(parent.querySelectorAll(".duplicate-warning")).forEach(n => n.remove()); | |
| if (!patterns || patterns.length === 0) return; | |
| const normalized = patterns.map(p => (p || '').toString().trim()).filter(Boolean); | |
| const cleaned = [...new Set(normalized.map(p => p.toLowerCase()))]; | |
| const exactRemoved = []; | |
| const counts = {}; | |
| normalized.forEach(p => { const k = p.toLowerCase(); counts[k] = (counts[k] || 0) + 1; }); | |
| Object.keys(counts).forEach(k => { if (counts[k] > 1) exactRemoved.push(k); }); | |
| const subsetRemoved = normalized | |
| .map(p => p.toLowerCase()) | |
| .filter(p => cleaned.indexOf(p) === -1 && exactRemoved.indexOf(p) === -1); | |
| if (exactRemoved.length || subsetRemoved.length) { | |
| const warn = document.createElement("div"); | |
| warn.className = "duplicate-warning"; | |
| let parts = []; | |
| if (exactRemoved.length) parts.push("duplicates: " + exactRemoved.join(", ")); | |
| if (subsetRemoved.length) parts.push("contained within longer ones: " + subsetRemoved.join(", ")); | |
| warn.innerText = "⚠ These patterns will be ignored — " + parts.join("; "); | |
| parent.insertBefore(warn, container.nextSibling); | |
| } | |
| }, | |
| clearSelections(container) { | |
| if (!container) return; | |
| container.textContent = container.textContent; // remove spans | |
| }, | |
| getHighlightedSelections(container) { | |
| if (!container) return []; | |
| return Array.from(container.querySelectorAll(".stt-highlighted, .highlighted")) | |
| .map(s => s.textContent.trim()) | |
| .filter(Boolean); | |
| } | |
| }; | |
| // Helper to create popup HTML (keeps ids/classes used by your CSS) | |
| function ensurePopupHtml() { | |
| let $overlay = $("#pattern-popup-overlay"); | |
| if (!$overlay.length) $overlay = $('<div id="pattern-popup-overlay" style="display:none;"></div>').appendTo('body'); | |
| let $popup = $("#pattern-popup"); | |
| if (!$popup.length) { | |
| $popup = $(` | |
| <div id="pattern-popup" role="dialog" aria-modal="true" class="pattern-popup" style="display:none;"> | |
| <div class="popup-header"><strong>Pattern selector</strong><button class="popup-close" aria-label="Close">×</button></div> | |
| <div class="popup-body"> | |
| <div id="popup-description" style="white-space:pre-wrap; line-height:1.4; cursor:text; font-size: 28px";"></div> | |
| <div style="margin-top:10px;"> | |
| <label>Amount sign: | |
| <select id="popup-amount-sign"> | |
| <option value="any">Any</option> | |
| <option value="positive">Positive</option> | |
| <option value="negative">Negative</option> | |
| </select> | |
| </label> | |
| </div> | |
| </div> | |
| <div class="popup-footer"> | |
| <div>Font size: <input type="number" id="popup-font-size" value="20" min="10" max="36" style="width:64px;"></div> | |
| <div style="display:flex; gap:8px; margin-top:15px;"> | |
| <button id="popup-accept" class="btn-primary">Accept</button> | |
| <button id="popup-clear" class="btn-secondary">Clear</button> | |
| <button id="popup-cancel" class="btn-secondary">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| `).appendTo('body'); | |
| } | |
| return { $overlay: $("#pattern-popup-overlay"), $popup: $("#pattern-popup") }; | |
| } | |
| // enable interactive selection inside popup body | |
| function enablePopupHighlighting(containerEl, $row, txnId) { | |
| if (!containerEl) return; | |
| if (!Array.isArray(containerEl._highlightedSelections)) containerEl._highlightedSelections = []; | |
| function persist(merged) { | |
| containerEl._highlightedSelections = merged; | |
| try { $row.data('current-patterns', merged); } catch (e) { /* ignore */ } | |
| } | |
| // mouseup -> capture text selection | |
| containerEl.onmouseup = function () { | |
| const sel = window.getSelection(); | |
| if (!sel || sel.rangeCount === 0) return; | |
| const text = sel.toString().trim(); | |
| if (!text) { sel.removeAllRanges(); return; } | |
| const existing = Utils.getHighlightedSelections(containerEl); | |
| const merged = Utils.dedupeAndClean(existing.concat([text])); | |
| Utils.highlightPatterns(containerEl, merged, 'yellow'); | |
| Utils.highlightDuplicates(containerEl, merged); | |
| persist(merged); | |
| sel.removeAllRanges(); | |
| }; | |
| // dblclick behavior | |
| containerEl.addEventListener('dblclick', function () { | |
| const sel = window.getSelection(); | |
| if (!sel) return; | |
| const text = sel.toString().trim(); | |
| if (!text) return; | |
| const existing = Utils.getHighlightedSelections(containerEl); | |
| const merged = Utils.dedupeAndClean(existing.concat([text])); | |
| Utils.highlightPatterns(containerEl, merged, 'yellow'); | |
| Utils.highlightDuplicates(containerEl, merged); | |
| persist(merged); | |
| sel.removeAllRanges(); | |
| }); | |
| // click on existing highlighted span removes it | |
| containerEl.addEventListener('click', function (e) { | |
| const targ = e.target; | |
| if (targ && targ.classList && (targ.classList.contains('stt-highlighted') || targ.classList.contains('highlighted'))) { | |
| const text = targ.innerText; | |
| targ.replaceWith(document.createTextNode(text)); | |
| const remaining = Utils.getHighlightedSelections(containerEl); | |
| const cleaned = Utils.dedupeAndClean(remaining); | |
| Utils.highlightPatterns(containerEl, cleaned, 'yellow'); | |
| Utils.highlightDuplicates(containerEl, cleaned); | |
| persist(cleaned); | |
| } | |
| }, true); | |
| } | |
| // Fetch server patterns for a given tranTypeId (visual only) | |
| function fetchServerPatterns(accountId, tranTypeId) { | |
| return new Promise((resolve) => { | |
| if (!tranTypeId || !accountId) { resolve([]); return; } | |
| $.post(stt_ajax_obj.ajaxurl, { | |
| action: 'stt_get_patterns_for_tran_type', | |
| account_id: accountId, | |
| tran_type_id: tranTypeId, | |
| nonce: stt_ajax_obj.nonce | |
| }).done(function (res) { | |
| if (res && res.success && Array.isArray(res.data.patterns)) { | |
| // server returns rows with patterns column (JSON array) maybe | |
| const serverPatterns = res.data.patterns.map(p => { | |
| try { | |
| // patterns might already be array or JSON | |
| return (typeof p.patterns === 'string') ? JSON.parse(p.patterns) : p.patterns; | |
| } catch (e) { | |
| // fallback: if p is a stringified array | |
| try { return JSON.parse(p); } catch (ee) { return []; } | |
| } | |
| }).flat(1).filter(Boolean); | |
| resolve(Utils.dedupeAndClean(serverPatterns)); | |
| } else { | |
| resolve([]); | |
| } | |
| }).fail(function () { resolve([]); }); | |
| }); | |
| } | |
| // Main show popup | |
| async function openPatternPopup($row) { | |
| // remove existing popup/overlay if present | |
| $("#pattern-popup, #pattern-popup-overlay").remove(); | |
| const desc = $row.find('td:nth-child(2)').text().trim(); | |
| const txnId = $row.data('txn-id') || ''; | |
| const accountId = $('#stt_account_id').val(); | |
| const tranTypeId = $row.find('.tran-type-input').data('tran-type-id') || 0; | |
| // create DOM | |
| const { $overlay, $popup } = ensurePopupHtml(); | |
| // Fill description | |
| const descEl = document.getElementById('popup-description'); | |
| descEl.innerText = desc; | |
| // restore font size if user saved | |
| const savedSize = localStorage.getItem('patternPopupFontSize') || 28; | |
| $('#popup-font-size').val(savedSize).trigger('input'); | |
| $('#popup-font-size').on('input', function () { | |
| $(descEl).css('font-size', $(this).val() + 'px'); | |
| localStorage.setItem('patternPopupFontSize', $(this).val()); | |
| }); | |
| // hydrate selections from row (client-side) | |
| const initialArr = $row.data('current-patterns') || []; | |
| if (initialArr && Array.isArray(initialArr) && initialArr.length) { | |
| Utils.highlightPatterns(descEl, initialArr, 'yellow'); | |
| Utils.highlightDuplicates(descEl, initialArr); | |
| descEl._highlightedSelections = initialArr.slice(); | |
| } else { | |
| descEl._highlightedSelections = []; | |
| Utils.clearSelections(descEl); | |
| } | |
| // show server patterns visually (orange) but keep user yellow selections | |
| const serverPatterns = await fetchServerPatterns(accountId, tranTypeId); | |
| if (serverPatterns && serverPatterns.length) { | |
| // render server-provided orange highlights first, then overlay user yellow selections | |
| Utils.highlightPatterns(descEl, serverPatterns, 'orange'); | |
| if (descEl._highlightedSelections && descEl._highlightedSelections.length) { | |
| Utils.highlightPatterns(descEl, descEl._highlightedSelections, 'yellow'); | |
| } | |
| } | |
| // enable user interactions for selecting / removing patterns | |
| enablePopupHighlighting(descEl, $row, txnId); | |
| // amount_sign prefill | |
| const existingSign = ($row.data('current-amount-sign') || 'any'); | |
| $('#popup-amount-sign').val(existingSign); | |
| // position + show | |
| $popup.css({ position: 'fixed', left: '50%', top: '50%', transform: 'translate(-50%,-50%)', zIndex: 99999 }); | |
| $overlay.fadeIn(120); $popup.fadeIn(180); | |
| // cleanup helper | |
| function closePatternPopup() { | |
| $popup.fadeOut(120, function () { $(this).remove(); }); | |
| $overlay.fadeOut(120, function () { $(this).remove(); }); | |
| $('#popup-font-size').off('input'); | |
| $('#popup-accept').off('click'); | |
| $('#popup-clear').off('click'); | |
| $('#popup-cancel').off('click'); | |
| $popup.find('.popup-close').off('click'); | |
| } | |
| // handlers | |
| $popup.find('.popup-close').on('click', closePatternPopup); | |
| $overlay.on('click', closePatternPopup); | |
| $('#popup-cancel').on('click', closePatternPopup); | |
| $('#popup-clear').on('click', function () { | |
| Utils.clearSelections(descEl); | |
| descEl._highlightedSelections = []; | |
| $row.data('current-patterns', []); | |
| Utils.highlightDuplicates(descEl, []); | |
| }); | |
| $('#popup-accept').on('click', function () { | |
| const selections = Utils.getHighlightedSelections(descEl); | |
| const cleaned = Utils.dedupeAndClean(selections); | |
| $row.data('current-patterns', cleaned); | |
| // record amount_sign | |
| const chosenSign = $('#popup-amount-sign').val() || 'any'; | |
| $row.data('current-amount-sign', chosenSign); | |
| // visually update the table cell to show persistent (orange) patterns | |
| const descCell = $row.find('td:nth-child(2)')[0]; | |
| if (descCell) Utils.highlightPatterns(descCell, cleaned, 'orange'); | |
| // mark row changed so Save button is enabled | |
| if (typeof SmartTrantype.Row !== 'undefined' && typeof SmartTrantype.Row.markAsChanged === 'function') { | |
| SmartTrantype.Row.markAsChanged($row); | |
| } | |
| closePatternPopup(); | |
| }); | |
| } | |
| // Public init function - binds to description cell (2nd td) | |
| function init() { | |
| $(document).on('click', '#transaction-table tbody tr td:nth-child(2)', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const $cell = $(this); | |
| const $row = $cell.closest('tr'); | |
| if (!$row || !$row.length) return; | |
| openPatternPopup($row); | |
| }); | |
| } | |
| // Return public interface | |
| return { | |
| init, | |
| openPatternPopup, // backward-compat (old name) | |
| showPatternPopup: openPatternPopup, | |
| enablePopupHighlighting, // exported if needed elsewhere | |
| ensurePopupHtml | |
| }; | |
| }()); | |
| // ---------------------------- | |
| // Matching algorithm (scoring) | |
| // ---------------------------- | |
| SmartTrantype.Matcher = { | |
| scorePatternSet: function (description, patternSet) { | |
| // description: string | |
| // patternSet: { patterns: [p1,p2], amount_sign: 'any'|'positive'|'negative' } | |
| if (!patternSet || !Array.isArray(patternSet.patterns)) return { score: 0, matchedCount: 0, totalLen: 0 }; | |
| const descLower = (description || '').toLowerCase(); | |
| let matchedCount = 0; | |
| let totalLen = 0; | |
| for (const p of patternSet.patterns) { | |
| if (!p) continue; | |
| const pl = p.toString().toLowerCase(); | |
| if (descLower.includes(pl)) { | |
| matchedCount++; | |
| totalLen += pl.length; | |
| } | |
| } | |
| // Complexity heuristics: | |
| // - A single pattern that equals or contains almost all description gets priority (long totalLen) | |
| // - Higher matchedCount preferred | |
| // - Then totalLen | |
| const score = (matchedCount * 1000) + totalLen; | |
| return { score: score, matchedCount: matchedCount, totalLen: totalLen }; | |
| }, | |
| findBestPatternSetForDescription: function (description, patternSets, amount) { | |
| // patternSets: array of pattern_set objects | |
| if (!Array.isArray(patternSets) || patternSets.length === 0) return null; | |
| let best = null; | |
| let bestScore = -1; | |
| for (const ps of patternSets) { | |
| const { score, matchedCount, totalLen } = this.scorePatternSet(description, ps); | |
| if (matchedCount === 0) continue; | |
| // amount_sign constraint check | |
| if (ps.amount_sign === 'positive' && Number(amount) < 0) continue; | |
| if (ps.amount_sign === 'negative' && Number(amount) > 0) continue; | |
| // prefer higher score | |
| if (score > bestScore) { | |
| bestScore = score; | |
| best = ps; | |
| } else if (score === bestScore) { | |
| // tie-break: prefer higher usage_count | |
| if ((ps.usage_count || 0) > (best.usage_count || 0)) { | |
| best = ps; | |
| } | |
| } | |
| } | |
| return best; | |
| } | |
| }; | |
| SmartTrantype.Autocomplete = { | |
| initTranTypeInputs: function () { | |
| const $inputs = $('.tran-type-input'); | |
| $inputs.each(function () { | |
| const $input = $(this); | |
| if ($input.data('ui-autocomplete')) return; | |
| try { | |
| $input.autocomplete({ | |
| source: function (request, response) { | |
| const list = (sttCache && sttCache.tran_types) ? sttCache.tran_types : []; | |
| const term = request.term.toLowerCase(); | |
| const matches = list | |
| .filter(item => | |
| item.description && | |
| item.description.toLowerCase().includes(term) | |
| ) | |
| .map(item => ({ | |
| label: item.description, // shown in dropdown | |
| value: item.description, // inserted into input field | |
| id: item.id // stored for reference | |
| })); | |
| response(matches); | |
| }, | |
| minLength: 1, | |
| // select: function (event, ui) { | |
| // $input.val(ui.item.value); // the description | |
| // $input.data('selected-id', ui.item.id); // store the id | |
| // $input.trigger('change'); | |
| // } | |
| select: function (event, ui) { | |
| $input.val(ui.item.label); | |
| $input.data('selected-id', ui.item.id); | |
| $input.trigger('change'); | |
| } | |
| }); | |
| } catch (e) { console.error('Autocomplete init failed', e); } | |
| }); | |
| } | |
| }; | |
| // --------------------------------------------------------- | |
| // SmartTrantype.Cache — central cache + DOM initialization | |
| // --------------------------------------------------------- | |
| SmartTrantype.Cache = { | |
| initCacheFromDOM: function () { | |
| window.sttCache = window.sttCache || {}; | |
| // if (!window.sttCache.transactions) { | |
| window.sttCache.transactions = []; | |
| // } | |
| $('#transaction-table tbody tr').each(function () { | |
| const $row = $(this); | |
| const rowId = $row.data('txn-id'); | |
| const tranType = $row.find('.tran-type-input').val() || ''; | |
| // const tranTypeId = $row.find('.tran-type-input').data('selected-id') || ''; | |
| const tranTypeId = $row.find('.tran-type-input').data('tran-type-id') || ''; | |
| // FIX | |
| const entry = { | |
| id: rowId, | |
| tran_type: tranType, | |
| tran_type_id: tranTypeId, | |
| // assigned: !!tranType, | |
| assigned: $row.hasClass('assigned-row'), | |
| // changed: false, | |
| changed: $row.hasClass('changed-row'), | |
| blank: !tranType, | |
| pre_assigned: $row.hasClass('pre-assigned'), | |
| // saved: false | |
| saved: $row.hasClass('assigned-row') | |
| }; | |
| window.sttCache.transactions.push(entry); | |
| }); | |
| }, | |
| get: function (rowId) { | |
| return window.sttCache.transactions.find(e => e.id == rowId) || null; | |
| }, | |
| setFlags: function (rowId, flags) { | |
| const entry = this.get(rowId); | |
| if (!entry) return; | |
| Object.assign(entry, flags); | |
| } | |
| }; | |
| SmartTrantype.Save = { | |
| getChangedRows: function () { | |
| return window.sttCache.transactions.filter(entry => entry.changed); | |
| }, | |
| saveSingleRow: function ($row, done) { | |
| if (!$row || !$row.length) return; | |
| const rowId = $row.data('txn-id'); | |
| const entry = window.sttCache.transactions.find(r => r.id == rowId); | |
| if (!entry) { done?.(); return; } | |
| // Cannot save if row is blank or unchanged | |
| if (!entry.changed) { done?.(); return; } | |
| const $saveBtn = $row.find('.save-tran-type-btn').first(); | |
| $saveBtn.prop('disabled', true); | |
| const payload = { | |
| action: 'save_tran_type', | |
| nonce: stt_ajax_obj.nonce, | |
| account_id: $('#stt_account_id').val(), | |
| txn_id: entry.id, | |
| tran_type: entry.tran_type, | |
| tran_type_id: entry.tran_type_id, | |
| current_patterns: JSON.stringify($row.data('current-patterns') || []), | |
| original_patterns: JSON.stringify($row.data('original-patterns') || []), | |
| pattern_text: $row.find('td:nth-child(2)').text().trim() | |
| }; | |
| $.ajax({ | |
| url: stt_ajax_obj.ajaxurl, | |
| method: 'POST', | |
| data: payload | |
| }).done((resp) => { | |
| if (resp.success) { | |
| // Update cache | |
| entry.assigned = true; | |
| entry.changed = false; | |
| entry.blank = false; | |
| entry.saved = true; | |
| // Update row DOM | |
| SmartTrantype.Row.markAsAssigned($row); | |
| } else { | |
| console.error('Save failed for row', entry, resp); | |
| } | |
| done?.(); | |
| }).fail((jqXHR, textStatus, errorThrown) => { | |
| console.error('AJAX error saving row', entry, textStatus, errorThrown); | |
| done?.(); | |
| }); | |
| } | |
| } | |
| // ---------------------------- | |
| // Refresh module | |
| // ---------------------------- | |
| SmartTrantype.Refresh = { | |
| refreshRowStatesAndAutocomplete: function () { | |
| // const $rows = $('#transaction-table tbody tr'); | |
| // $rows.each(function () { | |
| // const $row = $(this); | |
| // const id = $row.data('txn-id'); | |
| // const entry = window.sttCache.transactions.find(r => r.id == id); | |
| // if (!entry) return; | |
| // // Determine current | |
| // const hasValue = (entry.tran_type !== '' || entry.tran_type_id !== ''); | |
| // const hasChanged = (entry.tran_type !== entry.original_tran_type || entry.tran_type_id !== entry.original_tran_type_id); | |
| // if (!hasValue) { | |
| // entry.state = STT_STATE.BLANK; | |
| // } else if (hasChanged) { | |
| // entry.state = STT_STATE.CHANGED; | |
| // } else { | |
| // entry.state = STT_STATE.ASSIGNED; | |
| // } | |
| // // SmartTrantype.Row.applyStateToRow($row, entry.state); | |
| // }); | |
| // Autocomplete stable init | |
| SmartTrantype.Autocomplete.initTranTypeInputs(); | |
| } | |
| }; | |
| /* ---------------------------- | |
| * Save module init | |
| * ---------------------------- */ | |
| SmartTrantype.Save.init = function () { | |
| // Save single row | |
| $(document).on('click', '.save-tran-type-btn', function () { | |
| const $row = $(this).closest('tr'); | |
| SmartTrantype.Save.saveSingleRow($row); | |
| }); | |
| // Undo / reset row | |
| $(document).on('click', '.undo-tran-type-btn', function () { | |
| const $row = $(this).closest('tr'); | |
| SmartTrantype.Row.restoreOriginal($row); | |
| }); | |
| // Save all rows | |
| $(document).on('click', '#save-all-btn', function () { | |
| const $rows = $('#transaction-table tbody tr'); | |
| let remaining = $rows.length; | |
| $rows.each(function () { | |
| const $r = $(this); | |
| SmartTrantype.Save.saveSingleRow($r, function () { | |
| remaining--; | |
| if (remaining === 0) { | |
| console.log('All rows saved.'); | |
| } | |
| }); | |
| }); | |
| }); | |
| }; | |
| /* ---------------------------- | |
| * Row module: restore original | |
| * ---------------------------- */ | |
| SmartTrantype.Row.restoreOriginal = function ($row) { | |
| if (!$row || !$row.length) return; | |
| const rowId = $row.data('txn-id'); | |
| const entry = window.sttCache.transactions.find(r => r.id === rowId); | |
| if (!entry) return; | |
| const $input = $row.find('.tran-type-input'); | |
| // Restore cache | |
| entry.tran_type = entry.original_tran_type || ''; | |
| entry.tran_type_id = entry.original_tran_type_id ?? null; | |
| // Restore input field | |
| $input | |
| .val(entry.tran_type) | |
| .data('tran-type-id', entry.tran_type_id); | |
| // Re-evaluate state centrally | |
| SmartTrantype.handleInputChange($input[0]); | |
| }; | |
| /* ----------------------------------------------------------------------------- | |
| SmartTrantype.Init — safe orchestrator (non-invasive) | |
| Calls any module.init() that exists and sets minimal fallbacks. | |
| Place this near the end of smart-trantype.js (after modules). | |
| ----------------------------------------------------------------------------- */ | |
| SmartTrantype.Init = (function () { | |
| function safeCall(name, fn) { | |
| try { | |
| fn(); | |
| console.log("SmartTrantype.Init: initialized ->", name); | |
| } catch (err) { | |
| console.error("SmartTrantype.Init: error initializing", name, err); | |
| } | |
| } | |
| function run() { | |
| const candidates = [ | |
| 'Autocomplete', | |
| 'Pattern', | |
| 'Popup', | |
| 'Events', | |
| 'Save', | |
| 'Row', | |
| 'Refresh', | |
| 'Ajax', | |
| 'Utils' | |
| ]; | |
| const initialized = []; | |
| candidates.forEach(name => { | |
| const module = SmartTrantype[name]; | |
| if (module && typeof module.init === 'function') { | |
| safeCall(name, () => module.init()); | |
| initialized.push(name); | |
| } | |
| }); | |
| // Fallback: ensure basic save/undo event binding exists | |
| // If an Events.init already handled this, the duplicate handler won't do harm | |
| $(document).off('click.smarttrantype.save').on('click.smarttrantype.save', '.save-tran-type-btn', function (e) { | |
| e.preventDefault(); | |
| const $row = $(this).closest('tr'); | |
| if (SmartTrantype.Save && typeof SmartTrantype.Save.saveSingleRow === 'function') { | |
| SmartTrantype.Save.saveSingleRow($row); | |
| } else if (SmartTrantype.Save && typeof SmartTrantype.Save.saveRow === 'function') { | |
| SmartTrantype.Save.saveRow($row); | |
| } else { | |
| console.warn("SmartTrantype.Init: no Save.saveSingleRow found for .save-tran-type-btn"); | |
| } | |
| }); | |
| $(document).off('click.smarttrantype.undo').on('click.smarttrantype.undo', '.undo-tran-type-btn', function (e) { | |
| e.preventDefault(); | |
| const $row = $(this).closest('tr'); | |
| if (SmartTrantype.Row && typeof SmartTrantype.Row.undo === 'function') { | |
| SmartTrantype.Row.undo($row); | |
| } else if (SmartTrantype.Row && typeof SmartTrantype.Row.reset === 'function') { | |
| SmartTrantype.Row.reset($row); | |
| } else { | |
| // Best-effort DOM restore from data-* attributes | |
| const origType = $row.data('original-tran-type') || ''; | |
| const origTypeId = $row.data('original-tran-type-id') || ''; | |
| const origPatterns = $row.data('original-patterns') || []; | |
| $row.find('.tran-type-input').val(origType).data('tran-type-id', origTypeId); | |
| $row.data('current-patterns', Array.isArray(origPatterns) ? origPatterns : (typeof origPatterns === 'string' ? JSON.parse(origPatterns || '[]') : [])); | |
| $row.removeClass('changed-row').addClass(origType ? 'assigned-row' : 'blank-row'); | |
| console.warn("SmartTrantype.Init: Undo fallback applied (restored data-* values)."); | |
| } | |
| }); | |
| // Attach pattern popup opener to description cell (works with existing rows) | |
| $(document).off('click.smarttrantype.pattern').on('click.smarttrantype.pattern', '#transaction-table tbody tr td:nth-child(2)', function (e) { | |
| const $cell = $(this); | |
| const $row = $cell.closest('tr'); | |
| // This expects SmartTrantype.Pattern.openPatternPopup($row) | |
| if (SmartTrantype.Pattern && typeof SmartTrantype.Pattern.openPatternPopup === 'function') { | |
| SmartTrantype.Pattern.openPatternPopup($row); | |
| } else { | |
| console.warn('Pattern module not found'); | |
| } | |
| }); | |
| console.log("SmartTrantype.Init: modules auto-initialized:", initialized); | |
| return initialized; | |
| } | |
| return { | |
| run | |
| }; | |
| }()); | |
| SmartTrantype.Init.run = function () { | |
| SmartTrantype.Save.init(); | |
| SmartTrantype.Pattern.init(); | |
| SmartTrantype.Autocomplete.initTranTypeInputs(); | |
| SmartTrantype.Refresh.refreshRowStatesAndAutocomplete(); | |
| }; | |
| // Initialize on page load | |
| $(document).ready(function () { | |
| // 1️⃣ Initialize history stack for each transaction | |
| // window.sttCache.transactions.forEach(entry => { | |
| // entry.history = entry.history || []; // create empty undo stack | |
| // }); | |
| // 1️⃣ Initialize cache flags | |
| window.sttCache.transactions.forEach(entry => { | |
| if (entry.pre_assigned) { | |
| if (entry.saved) { | |
| entry.assigned = true; | |
| entry.changed = false; | |
| entry.blank = false; | |
| } else { | |
| entry.assigned = false; | |
| entry.changed = true; | |
| entry.blank = false; | |
| } | |
| } else { | |
| if (!entry.tran_type && !entry.tran_type_id) { | |
| entry.assigned = false; | |
| entry.changed = false; | |
| entry.blank = true; | |
| } else { | |
| entry.assigned = true; | |
| entry.changed = false; | |
| entry.blank = false; | |
| } | |
| } | |
| }); | |
| SmartTrantype.Cache.initCacheFromDOM(); | |
| // 2️⃣ Run standard initialization | |
| SmartTrantype.Init.run(); | |
| // 3️⃣ Refresh DOM based on cache | |
| SmartTrantype.Refresh.refreshRowStatesAndAutocomplete(); | |
| }); | |
| })(window, jQuery); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /* | |
| * Plugin Name: Smart TranType | |
| * Description: Frontend transaction type manager with autocomplete and suggestions | |
| */ | |
| if (!defined('ABSPATH')) | |
| exit; // Exit if accessed directly | |
| class SmartTrantype | |
| { | |
| private static $instance = null; | |
| public static function instance() | |
| { | |
| if (self::$instance === null) { | |
| self::$instance = new self(); | |
| } | |
| return self::$instance; | |
| } | |
| public function __construct() | |
| { | |
| add_shortcode('financial_year_transactions', [$this, 'financial_year_transactions']); | |
| add_action('wp_enqueue_scripts', [$this, 'enqueue_assets']); | |
| add_action('wp_ajax_save_tran_type', [$this, 'save_tran_type']); | |
| add_action('wp_ajax_nopriv_save_tran_type', [$this, 'save_tran_type']); | |
| add_action('wp_ajax_stt_get_patterns_for_tran_type', [$this, 'stt_get_patterns_for_tran_type']); | |
| add_action('wp_ajax_nopriv_stt_get_patterns_for_tran_type', [$this, 'stt_get_patterns_for_tran_type']); | |
| } | |
| // ----------------------------------------------- | |
| // Enqueue CSS and JS | |
| // ----------------------------------------------- | |
| public function enqueue_assets() | |
| { | |
| global $post; | |
| if (!isset($post->post_content) || strpos($post->post_content, '[financial_year_transactions]') === false) | |
| return; | |
| wp_enqueue_style('smart-trantype-css', plugin_dir_url(__FILE__) . 'smart-trantype.css', [], '1.0'); | |
| wp_enqueue_script('smart-trantype-js', plugin_dir_url(__FILE__) . 'smart-trantype.js', ['jquery', 'jquery-ui-autocomplete'], '1.0', true); | |
| wp_localize_script('smart-trantype-js', 'stt_ajax_obj', [ | |
| 'ajaxurl' => admin_url('admin-ajax.php'), | |
| 'nonce' => wp_create_nonce('stt_nonce'), | |
| ]); | |
| } | |
| // ----------------------------------------------- | |
| // Build financial year transactions table | |
| // ----------------------------------------------- | |
| public function financial_year_transactions() | |
| { | |
| global $wpdb; | |
| $account_id = isset($_POST['account_id']) ? intval($_POST['account_id']) : 0; | |
| $fin_year = isset($_POST['fin_year']) ? intval($_POST['fin_year']) : date('Y'); | |
| // Show account selection if none chosen | |
| if ($account_id === 0) { | |
| return $this->render_account_selection_form($fin_year); | |
| } | |
| $tblAccount = $wpdb->prefix . "smart_account_{$account_id}"; | |
| list($start_date, $end_date) = $this->get_financial_year_dates($fin_year); | |
| // Build JS cache | |
| $cache = $this->refresh_transaction_cache($account_id, $fin_year); | |
| wp_localize_script('smart-trantype-js', 'sttCache', $cache); | |
| ob_start(); | |
| ?> | |
| <h3>Transactions for Account <?= esc_html($account_id); ?> (FY <?= esc_html($fin_year); ?>)</h3> | |
| <input type="hidden" id="stt_account_id" value="<?= esc_attr($account_id); ?>"> | |
| <input type="hidden" name="stt_fin_year" id="stt_fin_year" value="<?= esc_attr($fin_year); ?>"> | |
| <table id="transaction-table"> | |
| <thead> | |
| <tr> | |
| <th>Date</th> | |
| <th>Description</th> | |
| <th>Amount</th> | |
| <th>Transaction Type</th> | |
| <th style="text-align: center; vertical-align: middle;"><button id="save-all-btn" | |
| title="Save All">💾</button></th> | |
| <th style="font-size: 0.8em;">Stmnt<br>_Line</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <?= $this->build_transaction_rows_by_account_and_year($account_id, $fin_year); ?> | |
| </tbody> | |
| </table> | |
| <?php | |
| return ob_get_clean(); | |
| } | |
| /** | |
| * Build HTML table rows for transactions with pattern matching. | |
| * | |
| * @param int $account_id | |
| * @param int $fin_year | |
| * @return string HTML table rows | |
| */ | |
| public function build_transaction_rows_by_account_and_year($account_id, $fin_year) | |
| { | |
| global $wpdb; | |
| $tblAccount = $wpdb->prefix . "smart_account_{$account_id}"; | |
| $tblTypes = $wpdb->prefix . "smart_tran_types_{$account_id}"; | |
| $tblPatterns = $wpdb->prefix . "smart_tran_type_patterns_{$account_id}"; | |
| list($start_date, $end_date) = $this->get_financial_year_dates($fin_year); | |
| // Fetch transactions for financial year | |
| $transactions = $wpdb->get_results($wpdb->prepare( | |
| "SELECT id, date, description, amount, tran_type_id | |
| FROM {$tblAccount} | |
| WHERE date BETWEEN %s AND %s | |
| ORDER BY date ASC", | |
| $start_date, | |
| $end_date | |
| ), ARRAY_A); | |
| // Fetch transaction types | |
| $types = $wpdb->get_results("SELECT id, tran_type FROM {$tblTypes}", OBJECT_K); | |
| // Fetch and normalize pattern sets | |
| $patterns = $wpdb->get_results("SELECT tran_type_id, patterns FROM {$tblPatterns}", ARRAY_A); | |
| $pattern_sets = []; | |
| foreach ($patterns as $p) { | |
| $list = json_decode($p['patterns'], true); | |
| if (is_array($list) && count($list)) { | |
| $tranTypeId = isset($p['tran_type_id']) && $p['tran_type_id'] !== '' ? $p['tran_type_id'] : ''; | |
| $pattern_sets[] = [ | |
| 'tran_type_id' => $tranTypeId, | |
| 'patterns' => array_map('trim', $list) | |
| ]; | |
| } | |
| } | |
| // Sort pattern sets by complexity (total alphanumeric chars including spaces) | |
| usort($pattern_sets, function ($a, $b) { | |
| $count_a = strlen(preg_replace('/[^a-zA-Z0-9 ]/', '', implode(' ', $a['patterns']))); | |
| $count_b = strlen(preg_replace('/[^a-zA-Z0-9 ]/', '', implode(' ', $b['patterns']))); | |
| return $count_b - $count_a; // descending | |
| }); | |
| $out = ''; | |
| foreach ($transactions as $txn) { | |
| $tran_type_id = isset($txn['tran_type_id']) && $txn['tran_type_id'] !== '' ? $txn['tran_type_id'] : ''; | |
| $tran_type_label = $tran_type_id ? ($types[$tran_type_id]->tran_type ?? '') : ''; | |
| // Pattern matching only if tran_type_id not set | |
| $matched_patterns = []; | |
| $row_classes = []; | |
| $pre_assigned = false; | |
| $json_patterns = '[]'; | |
| if (!$tran_type_id) { | |
| foreach ($pattern_sets as $ps) { | |
| $all_match = true; | |
| foreach ($ps['patterns'] as $pat) { | |
| if (stripos($txn['description'], $pat) === false) { | |
| $all_match = false; | |
| break; | |
| } | |
| } | |
| if ($all_match) { | |
| $tran_type_id = $ps['tran_type_id']; | |
| $tran_type_label = $types[$tran_type_id]->tran_type ?? ''; | |
| $matched_patterns = $ps['patterns']; | |
| $pre_assigned = true; | |
| break; | |
| } | |
| } | |
| } | |
| // Determine row classes | |
| if ($pre_assigned) { | |
| $row_classes[] = 'pre-assigned changed-row'; | |
| $json_patterns = wp_json_encode($matched_patterns); | |
| } else { | |
| if (!empty($tran_type_id)) { | |
| $row_classes[] = 'assigned-row'; | |
| } else { | |
| $row_classes[] = 'blank-row'; | |
| } | |
| } | |
| $amount_sign = isset($txn['amount_sign']) ? $txn['amount_sign'] : 'any'; | |
| $out .= '<tr class="' . esc_attr(implode(' ', $row_classes)) . '"' | |
| . ' data-txn-id="' . esc_attr($txn['id']) . '"' | |
| . ' data-original-tran-type-id="' . esc_attr($tran_type_id) . '"' | |
| . ' data-original-tran-type="' . esc_attr($tran_type_label) . '"' | |
| . ' data-original-patterns=\'' . esc_attr($json_patterns) . '\'' | |
| . ' data-current-patterns=\'' . esc_attr($json_patterns) . '\'' | |
| . ' data-current-amount-sign="' . esc_attr($amount_sign) . '">' | |
| . '<td>' . esc_html($txn['date']) . '</td>' | |
| . '<td>' . esc_html($txn['description']) . '</td>' | |
| . '<td class="numeric">' . number_format_i18n($txn['amount'], 2) . '</td>' | |
| . '<td>' | |
| . '<input type="text" class="tran-type-input" ' | |
| . 'data-tran-type-id="' . esc_attr($tran_type_id) . '" ' | |
| . 'value="' . esc_attr($tran_type_label) . '" ' | |
| . 'placeholder="Assign transaction type" autocomplete="off" />' | |
| . '</td>' | |
| . '<td class="action-buttons">' | |
| . '<button class="save-tran-type-btn" title="Save">💾</button>' | |
| . '<button class="undo-tran-type-btn" title="Reset">🔁</button>' | |
| . '</td>' | |
| . '<td class="txn-id-cell">' . esc_html($txn['id']) . '</td>' | |
| . '</tr>'; | |
| } | |
| return $out; | |
| } | |
| public function save_tran_type() | |
| { | |
| // error_log(__FUNCTION__ . ' - ' . __LINE__ . ' - called'); | |
| global $wpdb; | |
| check_ajax_referer('stt_nonce', 'nonce'); | |
| $txnId = sanitize_text_field($_POST['txn_id']); | |
| $accountId = intval($_POST['account_id']); | |
| $tranType = sanitize_text_field($_POST['tran_type']); | |
| $selectedPatternsArray = isset($_POST['current_patterns']) ? json_decode(stripslashes($_POST['current_patterns']), true) : []; | |
| // error_log(print_r($_POST, true)); | |
| $patternText = sanitize_text_field($_POST['pattern_text']); | |
| // error_log(__FUNCTION__ . ' ' . __LINE__ . ' ' . ' called for txnId=' . $txnId . ', accountId=' . $accountId . ', tranType=' . $tranType); | |
| $result = $this->_save_single_transaction($txnId, $accountId, $tranType, $selectedPatternsArray, $patternText); | |
| // error_log(__FUNCTION__ . ' ' . __LINE__ . ' ' . ' result=' . print_r($result, true)); | |
| wp_send_json($result); | |
| } | |
| private function _save_single_transaction($txnId, $accountId, $tranType, $patternsArray, $patternText) | |
| { | |
| // error_log(__FUNCTION__ . ' ' . __LINE__ . ' ' . ' called for txnId=' . $txnId . ', accountId=' . $accountId . ', tranType=' . $tranType); | |
| global $wpdb; | |
| // Table names (account-specific) | |
| $tblAccount = $wpdb->prefix . "smart_account_{$accountId}"; | |
| $tblTypes = $wpdb->prefix . "smart_tran_types_{$accountId}"; | |
| $tblPatterns = $wpdb->prefix . "smart_tran_type_patterns_{$accountId}"; | |
| // Step 1: Lookup or create tran_type | |
| $tranTypeId = $wpdb->get_var( | |
| $wpdb->prepare( | |
| "SELECT id FROM {$tblTypes} WHERE tran_type = %s LIMIT 1", | |
| $tranType | |
| ) | |
| ); | |
| if (!$tranTypeId) { | |
| // error_log('Inserting new tran_type=' . $tranType); | |
| $wpdb->insert( | |
| $tblTypes, | |
| [ | |
| 'tran_type' => $tranType, | |
| 'account_id' => $accountId | |
| ], | |
| ['%s', '%d'] | |
| ); | |
| $tranTypeId = $wpdb->insert_id; | |
| } else { | |
| // Maybe existing tran_type or account_id have been changed | |
| // error_log('Updating existing tran_type_id=' . $tranTypeId); | |
| $wpdb->update( | |
| $tblTypes, | |
| [ | |
| 'tran_type' => $tranType, | |
| 'account_id' => $accountId | |
| ], | |
| ['id' => $tranTypeId], | |
| ['%s', '%d'], | |
| ['%d'] | |
| ); | |
| } | |
| // Step 2: Normalize patterns | |
| $patternsArray = array_values(array_filter(array_map('trim', $patternsArray))); | |
| $patternsJson = wp_json_encode($patternsArray); | |
| // Step 3: Check for existing pattern row | |
| $existingPatternRow = $wpdb->get_row( | |
| $wpdb->prepare( | |
| // "SELECT id, patterns FROM {$tblPatterns} WHERE tran_type_id = %d LIMIT 1", | |
| "SELECT id, patterns FROM {$tblPatterns} WHERE tran_type_id = %d AND patterns = %s", | |
| $tranTypeId, | |
| $patternsJson | |
| ) | |
| ); | |
| if ($existingPatternRow) { | |
| // error_log('Updating existing pattern row for tran_type_id & pattern = ' . $tranTypeId . ' ' . $patternsJson); | |
| // patterns & tranType_id exist → increment usage | |
| $wpdb->query($wpdb->prepare( | |
| "UPDATE $tblPatterns SET usage_count = usage_count + 1 WHERE tran_type_id = %d AND patterns = %s", | |
| $tranTypeId, | |
| $patternsJson | |
| )); | |
| } else { | |
| // error_log('Inserting new pattern row for tran_type_id=' . $tranTypeId); | |
| // New pattern row | |
| $wpdb->insert( | |
| $tblPatterns, | |
| [ | |
| 'account_id' => $accountId, | |
| 'tran_type_id' => $tranTypeId, | |
| 'patterns' => $patternsJson, | |
| 'pattern_text' => $patternText, | |
| 'usage_count' => 1 | |
| ], | |
| ['%d', '%d', '%s', '%s', '%d'] | |
| ); | |
| } | |
| // Step 4: Update transaction row (only tran_type_id!) | |
| $wpdb->update( | |
| $tblAccount, | |
| ['tran_type_id' => $tranTypeId], | |
| ['id' => $txnId], | |
| ['%d'], | |
| ['%s'] | |
| ); | |
| // Optional: clear or update cache here if caching exists | |
| $this->refresh_transaction_cache($accountId); | |
| return [ | |
| 'success' => true, | |
| 'data' => [ | |
| 'msg' => 'Saved', | |
| 'tran_type_id' => $tranTypeId, | |
| 'patterns' => $patternsArray | |
| ] | |
| ]; | |
| } | |
| // ----------------------------------------------- | |
| // Refresh JS cache | |
| // ----------------------------------------------- | |
| private function refresh_transaction_cache($accountId, $fin_year = null) | |
| { | |
| global $wpdb; | |
| $accountId = intval($accountId); | |
| if ($accountId <= 0) { | |
| return [ | |
| 'tran_types' => [], | |
| 'pattern_sets' => [], | |
| 'transactions' => [] | |
| ]; | |
| } | |
| $tblTypes = $wpdb->prefix . "smart_tran_types_{$accountId}"; | |
| $tblPatterns = $wpdb->prefix . "smart_tran_type_patterns_{$accountId}"; | |
| $tblAccount = $wpdb->prefix . "smart_account_{$accountId}"; | |
| /* ----------------------------------------- | |
| * 1) TRANSACTION TYPES (normalised) | |
| * ----------------------------------------- */ | |
| $rawTypes = $wpdb->get_results(" | |
| SELECT id, tran_type | |
| FROM {$tblTypes} | |
| ", ARRAY_A); | |
| $tranTypes = array_map(function ($t) { | |
| return [ | |
| 'id' => intval($t['id']), | |
| 'description' => $t['tran_type'] | |
| ]; | |
| }, $rawTypes); | |
| /* ----------------------------------------- | |
| * 2) PATTERN SETS | |
| * ----------------------------------------- */ | |
| $patternRows = $wpdb->get_results(" | |
| SELECT id, tran_type_id, patterns, pattern_text, amount_sign, usage_count | |
| FROM {$tblPatterns} | |
| ORDER BY usage_count DESC | |
| ", ARRAY_A); | |
| $normalizedPatterns = []; | |
| foreach ($patternRows as $row) { | |
| $patterns = json_decode($row['patterns'], true); | |
| if (!is_array($patterns)) { | |
| $patterns = []; | |
| } | |
| $normalizedPatterns[] = [ | |
| 'id' => intval($row['id']), | |
| 'tran_type_id' => isset($row['tran_type_id']) && $row['tran_type_id'] !== '' ? $row['tran_type_id'] : '', | |
| 'patterns' => $patterns, | |
| 'pattern_text' => $row['pattern_text'], | |
| 'amount_sign' => $row['amount_sign'], | |
| 'usage_count' => intval($row['usage_count']) | |
| ]; | |
| } | |
| /* ----------------------------------------- | |
| * 3) TRANSACTIONS (limit to financial year) | |
| * ----------------------------------------- */ | |
| if ($fin_year !== null) { | |
| list($start_date, $end_date) = $this->get_financial_year_dates($fin_year); | |
| $transactions = $wpdb->get_results($wpdb->prepare(" | |
| SELECT id, tran_type_id, description, amount, date | |
| FROM {$tblAccount} | |
| WHERE date BETWEEN %s AND %s | |
| ORDER BY id ASC | |
| ", $start_date, $end_date), ARRAY_A); | |
| } else { | |
| // fallback to all rows | |
| $transactions = $wpdb->get_results(" | |
| SELECT id, tran_type_id, description, amount, date | |
| FROM {$tblAccount} | |
| ORDER BY id ASC | |
| ", ARRAY_A); | |
| } | |
| return [ | |
| 'tran_types' => $tranTypes, | |
| 'pattern_sets' => $normalizedPatterns, | |
| 'transactions' => $transactions | |
| ]; | |
| } | |
| /** | |
| * Get the start and end dates for a financial year. | |
| * | |
| * @param int $fin_year The financial year (e.g., 2025). | |
| * @return array An array containing the start and end dates in 'Y-m-d' format. | |
| */ | |
| public function get_financial_year_dates($fin_year) | |
| { | |
| global $bus_year_end; // must be set globally; default fiscal year end month | |
| if (empty($bus_year_end)) { | |
| $bus_year_end = 2; // default to February | |
| } | |
| // Financial year starts on 1 March of previous calendar year | |
| $start_year = $fin_year - 1; | |
| $start_date = sprintf("%04d-%02d-01", $start_year, $bus_year_end + 1); // March 1 | |
| $end_date = sprintf( | |
| "%04d-%02d-%02d", | |
| $fin_year, | |
| $bus_year_end, | |
| cal_days_in_month(CAL_GREGORIAN, $bus_year_end, $fin_year) | |
| ); | |
| return [$start_date, $end_date]; | |
| } | |
| // ----------------------------------------------- | |
| // Render account selection form | |
| // ----------------------------------------------- | |
| private function render_account_selection_form($fin_year) | |
| { | |
| global $wpdb; | |
| $accounts = $wpdb->get_results("SELECT id, account_name FROM {$wpdb->prefix}smart_accounts ORDER BY account_name"); | |
| ob_start(); ?> | |
| <form method="post"> | |
| <label>Account:</label> | |
| <select name="account_id" required> | |
| <option value="">-- Choose an Account --</option> | |
| <?php foreach ($accounts as $acc): ?> | |
| <option value="<?= esc_attr($acc->id); ?>"><?= esc_html($acc->account_name); ?></option> | |
| <?php endforeach; ?> | |
| </select> | |
| <label>Financial Year:</label> | |
| <input type="number" name="fin_year" value="<?= esc_attr($fin_year); ?>" required> | |
| <button type="submit">View</button> | |
| </form> | |
| <?php | |
| return ob_get_clean(); | |
| } | |
| public function ajax_create_tran_type() | |
| { | |
| global $wpdb; | |
| $account_id = intval($_POST['account_id'] ?? 0); | |
| $name = trim($_POST['name'] ?? ''); | |
| if (!$account_id || $name === '') { | |
| wp_send_json([ | |
| 'success' => false, | |
| 'message' => 'Invalid parameters.' | |
| ]); | |
| } | |
| $tblTypes = $wpdb->prefix . "smart_tran_types_{$account_id}"; | |
| // Check if already exists (case-insensitive) | |
| $existing = $wpdb->get_var( | |
| $wpdb->prepare("SELECT id FROM {$tblTypes} WHERE LOWER(tran_type) = LOWER(%s)", $name) | |
| ); | |
| if ($existing) { | |
| wp_send_json([ | |
| 'success' => true, | |
| 'id' => intval($existing), | |
| 'label' => $name | |
| ]); | |
| } | |
| // Insert new type (but DO NOT assign to any transaction) | |
| $wpdb->insert( | |
| $tblTypes, | |
| ['tran_type' => $name], | |
| ['%s'] | |
| ); | |
| $new_id = $wpdb->insert_id; | |
| wp_send_json([ | |
| 'success' => true, | |
| 'id' => intval($new_id), | |
| 'label' => $name | |
| ]); | |
| } | |
| // public function stt_get_patterns_for_tran_type() | |
| // { | |
| // global $wpdb; | |
| // $account_id = intval($_POST['account_id'] ?? 0); | |
| // $tran_type_id = $_POST['tran_type_id'] ?? ''; | |
| // $nonce = $_POST['nonce'] ?? ''; | |
| // if (!wp_verify_nonce($nonce, 'stt_nonce')) { | |
| // wp_send_json([ | |
| // 'success' => false, | |
| // 'message' => 'Invalid nonce' | |
| // ]); | |
| // } | |
| // if ($account_id == 0 || !$tran_type_id) { | |
| // wp_send_json([ | |
| // 'success' => false, | |
| // 'message' => 'Invalid parameters' | |
| // ]); | |
| // } | |
| // $tblPatterns = $wpdb->prefix . "smart_tran_patterns_{$account_id}"; | |
| // // Get all pattern groups that belong to this tran_type_id | |
| // $rows = $wpdb->get_results( | |
| // $wpdb->prepare(" | |
| // SELECT id, patterns, amount_sign | |
| // FROM {$tblPatterns} | |
| // WHERE tran_type_id = %d | |
| // ORDER BY id ASC | |
| // ", $tran_type_id), | |
| // ARRAY_A | |
| // ); | |
| // // Normalise pattern text: convert comma string → array | |
| // foreach ($rows as &$r) { | |
| // $r['patterns'] = array_filter(array_map('trim', explode(',', $r['patterns']))); | |
| // } | |
| // wp_send_json([ | |
| // 'success' => true, | |
| // 'data' => [ | |
| // 'patterns' => $rows | |
| // ] | |
| // ]); | |
| // } | |
| public function stt_get_patterns_for_tran_type() | |
| { | |
| global $wpdb; | |
| // nonce check | |
| $nonce = $_POST['nonce'] ?? ''; | |
| if (!wp_verify_nonce($nonce, 'stt_nonce')) { | |
| wp_send_json_error(['message' => 'Invalid nonce'], 400); | |
| } | |
| $account_id = intval($_POST['account_id'] ?? 0); | |
| $tran_type_id = intval($_POST['tran_type_id'] ?? ''); | |
| if (!$account_id || !$tran_type_id) { | |
| wp_send_json_error(['message' => 'Missing params'], 400); | |
| } | |
| $tblPatterns = $wpdb->prefix . "smart_tran_type_patterns_{$account_id}"; | |
| // Fetch rows that match this tran_type_id | |
| $rows = $wpdb->get_results( | |
| $wpdb->prepare("SELECT id, patterns, pattern_text, amount_sign, usage_count FROM {$tblPatterns} WHERE tran_type_id = %d ORDER BY usage_count DESC", $tran_type_id), | |
| ARRAY_A | |
| ); | |
| // decode patterns JSON to arrays | |
| $out = []; | |
| if (!empty($rows)) { | |
| foreach ($rows as $r) { | |
| $decoded = json_decode($r['patterns'], true); | |
| if (!is_array($decoded)) | |
| $decoded = []; | |
| $out[] = [ | |
| 'id' => intval($r['id']), | |
| 'patterns' => $decoded, | |
| 'pattern_text' => $r['pattern_text'], | |
| 'amount_sign' => $r['amount_sign'], | |
| 'usage_count' => intval($r['usage_count']), | |
| ]; | |
| } | |
| } | |
| wp_send_json_success(['patterns' => $out]); | |
| } | |
| } | |
| SmartTrantype::instance(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment