From c24480099f0ad3d30a8eed19882c90f8640ad6b6 Mon Sep 17 00:00:00 2001
From: wbuezas <wbuezas@verdnatura.es>
Date: Tue, 18 Mar 2025 14:32:00 -0300
Subject: [PATCH] Create new searchbar and UsersView requests

---
 src/components/ui/NewVnSearchBar.vue | 120 +++++++++++++++++++++++++++
 src/composables/serviceUtils.js      | 116 ++++++++++++++++++++++++++
 src/pages/Admin/UsersView.vue        |  37 ++++++---
 3 files changed, 262 insertions(+), 11 deletions(-)
 create mode 100644 src/components/ui/NewVnSearchBar.vue
 create mode 100644 src/composables/serviceUtils.js

diff --git a/src/components/ui/NewVnSearchBar.vue b/src/components/ui/NewVnSearchBar.vue
new file mode 100644
index 00000000..d4abcec7
--- /dev/null
+++ b/src/components/ui/NewVnSearchBar.vue
@@ -0,0 +1,120 @@
+<script setup>
+import { onMounted, ref } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useRoute, useRouter } from 'vue-router';
+
+import VnInput from 'src/components/common/VnInput.vue';
+
+import { fetch } from 'src/composables/serviceUtils';
+
+const props = defineProps({
+    searchFn: {
+        type: Function,
+        default: null
+    },
+    placeholder: {
+        type: String,
+        default: ''
+    },
+    url: {
+        type: String,
+        default: null
+    },
+    filter: {
+        type: String,
+        default: null
+    },
+    searchField: {
+        type: String,
+        default: 'search'
+    },
+    exprBuilder: {
+        type: Function,
+        default: null
+    }
+});
+
+const emit = defineEmits(['onSearch', 'onSearchError']);
+
+const { t } = useI18n();
+const router = useRouter();
+const route = useRoute();
+
+const searchTerm = ref('');
+
+const search = async () => {
+    try {
+        router.replace({
+            query: searchTerm.value ? { search: searchTerm.value } : {}
+        });
+
+        if (!searchTerm.value) {
+            emit('onSearchError');
+            return;
+        }
+
+        if (props.url) {
+            const params = {
+                filter: props.filter,
+                [props.searchField]: searchTerm.value
+            };
+
+            const { data } = await fetch({
+                url: props.url,
+                params,
+                exprBuilder: props.exprBuilder
+            });
+
+            emit('onSearch', data);
+        }
+    } catch (error) {
+        console.error('Error searching:', error);
+        emit('onSearchError');
+    }
+};
+
+onMounted(() => {
+    if (route.query.search) {
+        searchTerm.value = route.query.search;
+        search();
+    }
+});
+</script>
+<template>
+    <VnInput
+        v-model="searchTerm"
+        @keyup.enter="search()"
+        :placeholder="props.placeholder || t('search')"
+        bg-color="white"
+        is-outlined
+        :clearable="false"
+        class="searchbar"
+        data-cy="searchBar"
+    >
+        <template #prepend>
+            <QIcon name="search" class="cursor-pointer" @click="search()" />
+        </template>
+    </VnInput>
+</template>
+
+<style lang="scss" scoped>
+@import 'src/css/responsive';
+
+.searchbar {
+    @include mobile {
+        max-width: 120px;
+    }
+}
+</style>
+<i18n lang="yaml">
+en-US:
+    search: Search
+es-ES:
+    search: Buscar
+ca-ES:
+    search: Cercar
+fr-FR:
+    search: Rechercher
+pt-PT:
+    search: Pesquisar
+</i18n>
diff --git a/src/composables/serviceUtils.js b/src/composables/serviceUtils.js
new file mode 100644
index 00000000..ce124398
--- /dev/null
+++ b/src/composables/serviceUtils.js
@@ -0,0 +1,116 @@
+import { api } from '@/boot/axios';
+
+function buildFilter(params, builderFunc) {
+    let and = [];
+
+    for (let param in params) {
+        let value = params[param];
+        if (value == null) continue;
+        let expr = builderFunc(param, value);
+        if (expr) and.push(expr);
+    }
+    return simplifyOperation(and, 'and');
+}
+
+const simplifyOperation = (operation, operator) => {
+    switch (operation.length) {
+        case 0:
+            return undefined;
+        case 1:
+            return operation[0];
+        default:
+            return { [operator]: operation };
+    }
+};
+
+async function fetch({
+    url,
+    append = false,
+    params,
+    exprBuilder,
+    mapKey,
+    existingData = [],
+    existingMap = new Map(),
+    oneRecord = false
+}) {
+    if (!url) return;
+
+    let exprFilter;
+    if (exprBuilder) {
+        exprFilter = buildFilter(params, (param, value) => {
+            if (param === 'filter') return;
+            const res = exprBuilder(param, value);
+            if (res) delete params[param];
+            return res;
+        });
+    }
+
+    if (params.filter?.where || exprFilter) {
+        params.filter.where = { ...params.filter.where, ...exprFilter };
+    }
+
+    if (!params?.filter?.order?.length) {
+        delete params?.filter?.order;
+    }
+
+    params.filter = JSON.stringify(params.filter);
+
+    const response = await api.get(url, { params });
+    const processedData = processData(response.data, {
+        mapKey,
+        map: !!mapKey,
+        append,
+        oneRecord,
+        existingData,
+        existingMap
+    });
+
+    return { response, data: processedData.data, map: processedData.map };
+}
+
+function processData(data, options) {
+    const {
+        mapKey,
+        map = true,
+        append = true,
+        oneRecord = false,
+        existingData = [],
+        existingMap = new Map()
+    } = options;
+    let resultData = [...existingData];
+    let resultMap = new Map(existingMap);
+
+    if (oneRecord) {
+        return Array.isArray(data) ? data[0] : data;
+    }
+
+    if (!append) {
+        resultData = [];
+        resultMap = new Map();
+    }
+
+    if (!Array.isArray(data)) {
+        resultData = data;
+    } else if (!map && append) {
+        resultData.push(...data);
+    } else {
+        for (const row of data) {
+            const key = row[mapKey];
+            const val = { ...row, key };
+            if (key && resultMap.has(key)) {
+                const { position } = resultMap.get(key);
+                val.position = position;
+                resultMap.set(key, val);
+                resultData[position] = val;
+            } else {
+                val.position = resultMap.size;
+                resultMap.set(key, val);
+                resultData.push(val);
+            }
+        }
+    }
+
+    return { data: resultData, map: resultMap };
+}
+
+export { buildFilter, fetch, processData };
diff --git a/src/pages/Admin/UsersView.vue b/src/pages/Admin/UsersView.vue
index 33de8f70..c72167f2 100644
--- a/src/pages/Admin/UsersView.vue
+++ b/src/pages/Admin/UsersView.vue
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
 import { useRouter } from 'vue-router';
 
 import CardList from 'src/components/ui/CardList.vue';
-import VnSearchBar from 'src/components/ui/VnSearchBar.vue';
+import VnSearchBar from 'src/components/ui/NewVnSearchBar.vue';
 import VnList from 'src/components/ui/VnList.vue';
 
 import { useAppStore } from 'stores/app';
@@ -16,16 +16,11 @@ const router = useRouter();
 const userStore = useUserStore();
 const appStore = useAppStore();
 const { isHeaderMounted } = storeToRefs(appStore);
-
 const loading = ref(false);
 const users = ref([]);
-
-const query = `SELECT u.id, u.name, u.nickname, u.active
-            FROM account.user u
-            WHERE u.name LIKE CONCAT('%', #user, '%')
-            OR u.nickname LIKE CONCAT('%', #user, '%')
-            OR u.id = #user
-            ORDER BY u.name LIMIT 200`;
+const filter = {
+    fields: ['id', 'name', 'nickname', 'active']
+};
 
 const onSearch = data => (users.value = data || []);
 
@@ -38,15 +33,35 @@ const supplantUser = async user => {
         console.error('Error supplanting user:', error);
     }
 };
+
+const usersExprBuilder = (param, value) => {
+    switch (param) {
+        case 'search':
+            return /^\d+$/.test(value)
+                ? { id: value }
+                : {
+                      or: [
+                          { name: { like: `%${value}%` } },
+                          { nickname: { like: `%${value}%` } }
+                      ]
+                  };
+        case 'name':
+        case 'nickname':
+            return { [param]: { like: `%${value}%` } };
+        case 'roleFk':
+            return { [param]: value };
+    }
+};
 </script>
 
 <template>
     <Teleport v-if="isHeaderMounted" to="#actions">
         <VnSearchBar
-            :sql-query="query"
-            search-field="user"
+            url="/VnUsers/preview"
             @on-search="onSearch"
             @on-search-error="users = []"
+            :expr-builder="usersExprBuilder"
+            :filter="filter"
             data-cy="usersViewSearchBar"
         />
     </Teleport>