diff --git a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js index 431b91e72..985db20bf 100644 --- a/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js +++ b/odex25_base/fims_general_search_tree_view/static/src/js/list_search.js @@ -3,12 +3,17 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { var ListRenderer = require('web.ListRenderer'); var ListController = require('web.ListController'); + var relational_fields = require('web.relational_fields'); var core = require('web.core'); var _t = core._t; var concurrency = require('web.concurrency'); var rpc = require('web.rpc'); var session = require('web.session'); + // Get relational field classes + var FieldOne2Many = relational_fields.FieldOne2Many; + var FieldMany2Many = relational_fields.FieldMany2Many; + ListController.include({ init: function () { this._super.apply(this, arguments); @@ -512,8 +517,305 @@ odoo.define('fims_general_search_tree_view.list_search', function (require) { } }); + // ============= NEW: Relational Fields Support ============= + + // Mixin for common relational field functionality + var RelationalFieldMixin = { + + _searchState: null, + _searchMutex: null, + + init: function() { + this._super.apply(this, arguments); + this._initRelationalSearch(); + }, + + _initRelationalSearch: function() { + this._searchState = { + timer: null, + value: '', + isFiltered: false, + filteredCount: 0, + originalRecords: null, + allRecords: null, + lastSearchValue: '' + }; + this._searchMutex = new concurrency.Mutex(); + }, + + _renderView: function() { + var self = this; + return this._super.apply(this, arguments).then(function(result) { + if (self._shouldAddRelationalSearchBox()) { + self._addRelationalSearchBox(); + self._storeOriginalRecords(); + } + return result; + }); + }, + + _shouldAddRelationalSearchBox: function() { + return this.renderer && + this.renderer.$el && + this.renderer.$el.find('.o_list_view').length > 0 && + !this.renderer.$el.find('.oe_relational_search_container').length && + this.value && + this.value.data && + this.value.data.length > 0; + }, + + _storeOriginalRecords: function() { + if (this.value && this.value.data && !this._searchState.originalRecords) { + this._searchState.originalRecords = JSON.parse(JSON.stringify(this.value.data)); + this._searchState.allRecords = JSON.parse(JSON.stringify(this.value.data)); + } + }, + + _addRelationalSearchBox: function() { + var self = this; + var savedValue = this._searchState.value || ''; + var savedCount = this._searchState.filteredCount || 0; + var isFiltered = this._searchState.isFiltered || false; + + var countText = isFiltered ? + _t('عدد السجلات: ') + savedCount : ''; + + var html = + '
' + + '' + + '' + + '' + + countText + + '' + + '' + + '
'; + + var $listContainer = this.renderer.$el.find('.o_list_view').first(); + $listContainer.prepend($(html)); + + // Bind events + this.renderer.$el.find('.oe_relational_search_input').on('keyup', function(e) { + self._onRelationalSearchKeyUp(e); + }); + + this.renderer.$el.find('.oe_relational_search_input').on('input', function(e) { + self._onRelationalSearchInput(e); + }); + + this.renderer.$el.find('.oe_relational_clear_search').on('click', function(e) { + self._onRelationalClearSearch(e); + }); + + this.renderer.$el.find('.oe_relational_search_input').on('keydown', function(e) { + self._onRelationalSearchKeyDown(e); + }); + }, + + _onRelationalSearchInput: function(e) { + var currentValue = $(e.currentTarget).val(); + var hasValue = !!currentValue.trim(); + + this.renderer.$el.find('.oe_relational_clear_search').toggle(hasValue); + + if (!hasValue) { + this.renderer.$el.find('.oe_relational_search_count').hide(); + } + }, + + _onRelationalSearchKeyUp: function(e) { + var self = this; + var value = $(e.currentTarget).val().trim(); + var ignoreKeys = [13, 27, 16, 17, 18, 91, 93, 37, 38, 39, 40, + 33, 34, 35, 36, 9, 20, 144, 145, + 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123]; + + if (ignoreKeys.indexOf(e.which) !== -1) return; + if (value === this._searchState.lastSearchValue) return; + + this._searchState.lastSearchValue = value; + + if (this._searchState.timer) { + clearTimeout(this._searchState.timer); + } + + this._searchState.timer = setTimeout(function() { + self._executeRelationalSearch(value); + }, 300); + }, + + _onRelationalSearchKeyDown: function(e) { + if (e.which === 13) { + e.preventDefault(); + if (this._searchState.timer) { + clearTimeout(this._searchState.timer); + } + var value = $(e.currentTarget).val().trim(); + this._executeRelationalSearch(value); + } else if (e.which === 27) { + e.preventDefault(); + this._onRelationalClearSearch(); + } + }, + + _onRelationalClearSearch: function() { + this.renderer.$el.find('.oe_relational_search_input').val('').focus(); + this.renderer.$el.find('.oe_relational_clear_search').hide(); + this.renderer.$el.find('.oe_relational_search_count').hide(); + this._searchState.lastSearchValue = ''; + this._clearRelationalSearch(); + }, + + _executeRelationalSearch: function(value) { + var self = this; + + return this._searchMutex.exec(function() { + if (!value || value.length === 0) { + return self._clearRelationalSearch(); + } + + return self._applyRelationalSearch(value); + }); + }, + + _applyRelationalSearch: function(value) { + var self = this; + + if (!this._searchState.allRecords || this._searchState.allRecords.length === 0) { + return Promise.resolve(); + } + + var normalizedSearch = this._normalizeText(value.toLowerCase()); + var searchValues = [value.toLowerCase(), normalizedSearch].filter(function(v, i, arr) { + return arr.indexOf(v) === i; // Remove duplicates + }); + + var filteredRecords = this._searchState.allRecords.filter(function(record) { + return self._recordMatchesSearch(record, searchValues); + }); + + this._searchState.filteredCount = filteredRecords.length; + this._searchState.isFiltered = true; + this._searchState.value = value; + + // Update the UI + this._updateRelationalSearchUI(filteredRecords.length); + + // Update the field's value with filtered data + var newValue = _.clone(this.value); + newValue.data = filteredRecords; + newValue.count = filteredRecords.length; + + // Re-render with filtered data + return this._setValue(newValue, {forceChange: true}).then(function() { + return Promise.resolve(); + }); + }, + + _recordMatchesSearch: function(record, searchValues) { + var self = this; + var recordData = record.data || {}; + + // Get searchable fields from the record + var searchableValues = []; + + // Add all text-like fields + Object.keys(recordData).forEach(function(fieldName) { + var value = recordData[fieldName]; + if (value !== null && value !== undefined) { + if (typeof value === 'string') { + searchableValues.push(value.toLowerCase()); + searchableValues.push(self._normalizeText(value.toLowerCase())); + } else if (typeof value === 'number') { + searchableValues.push(value.toString()); + } else if (Array.isArray(value) && value.length === 2) { + // Many2one field format [id, display_name] + if (typeof value[1] === 'string') { + searchableValues.push(value[1].toLowerCase()); + searchableValues.push(self._normalizeText(value[1].toLowerCase())); + } + } + } + }); + + // Check if any search value matches any record value + return searchValues.some(function(searchValue) { + return searchableValues.some(function(recordValue) { + return recordValue.indexOf(searchValue) !== -1; + }); + }); + }, + + _clearRelationalSearch: function() { + if (!this._searchState.originalRecords) { + return Promise.resolve(); + } + + this._searchState.value = ''; + this._searchState.isFiltered = false; + this._searchState.filteredCount = 0; + this._searchState.lastSearchValue = ''; + + // Restore original records + var newValue = _.clone(this.value); + newValue.data = JSON.parse(JSON.stringify(this._searchState.originalRecords)); + newValue.count = this._searchState.originalRecords.length; + + // Hide search UI elements + if (this.renderer && this.renderer.$el) { + this.renderer.$el.find('.oe_relational_search_count').hide(); + this.renderer.$el.find('.oe_relational_search_loading').hide(); + } + + // Re-render with original data + return this._setValue(newValue, {forceChange: true}); + }, + + _updateRelationalSearchUI: function(count) { + if (this.renderer && this.renderer.$el) { + this.renderer.$el.find('.oe_relational_search_count') + .text(_t('عدد السجلات: ') + count) + .removeClass('text-danger') + .show(); + this.renderer.$el.find('.oe_relational_clear_search').show(); + } + }, + + _normalizeText: function(text) { + if (!text) return ''; + return text + .replace(/[\u064B-\u065F]/g, '') // Remove Arabic diacritics + .replace(/[٠-٩]/g, function(d) { // Convert Arabic-Indic digits + return String.fromCharCode(d.charCodeAt(0) - 0x0660 + 0x0030); + }) + .replace(/[۰-۹]/g, function(d) { // Convert Extended Arabic-Indic digits + return String.fromCharCode(d.charCodeAt(0) - 0x06F0 + 0x0030); + }) + .replace(/[آأإا]/g, 'ا') // Normalize Alef variations + .replace(/ة/g, 'ه') // Normalize Taa Marbouta + .replace(/[يئى]/g, 'ي') // Normalize Yaa variations + .trim(); + } + }; + + // Apply the mixin to FieldOne2Many + FieldOne2Many.include(RelationalFieldMixin); + + // Apply the mixin to FieldMany2Many + FieldMany2Many.include(RelationalFieldMixin); + return { ListController: ListController, - ListRenderer: ListRenderer + ListRenderer: ListRenderer, + FieldOne2Many: FieldOne2Many, + FieldMany2Many: FieldMany2Many }; }); \ No newline at end of file