diff --git a/src/components/common/VnCard.vue b/src/components/common/VnCard.vue index 58cb12708..e836badec 100644 --- a/src/components/common/VnCard.vue +++ b/src/components/common/VnCard.vue @@ -20,6 +20,8 @@ const props = defineProps({ searchUrl: { type: String, default: undefined }, searchbarLabel: { type: String, default: '' }, searchbarInfo: { type: String, default: '' }, + searchCustomRouteRedirect: { type: String, default: undefined }, + searchRedirect: { type: Boolean, default: false }, }); const stateStore = useStateStore(); @@ -62,6 +64,8 @@ watchEffect(() => { :url="props.searchUrl" :label="props.searchbarLabel" :info="props.searchbarInfo" + :custom-route-redirect-name="searchCustomRouteRedirect" + :redirect="searchRedirect" /> </slot> </Teleport> diff --git a/src/components/ui/VnSearchbar.vue b/src/components/ui/VnSearchbar.vue index 38dcf97d1..e5b2f02d2 100644 --- a/src/components/ui/VnSearchbar.vue +++ b/src/components/ui/VnSearchbar.vue @@ -1,5 +1,5 @@ <script setup> -import { onMounted, ref } from 'vue'; +import { onMounted, ref, watch } from 'vue'; import { useQuasar } from 'quasar'; import { useArrayData } from 'composables/useArrayData'; import VnInput from 'src/components/common/VnInput.vue'; @@ -67,11 +67,19 @@ const props = defineProps({ }, }); -const arrayData = useArrayData(props.dataKey, { ...props }); -const { store } = arrayData; +let arrayData = useArrayData(props.dataKey, { ...props }); +let store = arrayData.store; const searchText = ref(''); const { navigate } = useRedirect(); +watch( + () => props.dataKey, + (val) => { + arrayData = useArrayData(val, { ...props }); + store = arrayData.store; + } +); + onMounted(() => { const params = store.userParams; if (params && params.search) { diff --git a/src/components/CreateDepartmentChild.vue b/src/pages/Worker/CreateDepartmentChild.vue similarity index 100% rename from src/components/CreateDepartmentChild.vue rename to src/pages/Worker/CreateDepartmentChild.vue diff --git a/src/pages/Worker/WorkerDepartment.vue b/src/pages/Worker/WorkerDepartment.vue index 3c0e5fdd0..fe4c23051 100644 --- a/src/pages/Worker/WorkerDepartment.vue +++ b/src/pages/Worker/WorkerDepartment.vue @@ -1,10 +1,10 @@ <script setup> -import VnTree from 'components/ui/VnTree.vue'; +import WorkerDepartmentTree from './WorkerDepartmentTree.vue'; </script> <template> <QPage class="column items-center q-pa-md"> - <VnTree /> + <WorkerDepartmentTree /> </QPage> </template> diff --git a/src/components/ui/VnTree.vue b/src/pages/Worker/WorkerDepartmentTree.vue similarity index 99% rename from src/components/ui/VnTree.vue rename to src/pages/Worker/WorkerDepartmentTree.vue index 928d045e9..34340b019 100644 --- a/src/components/ui/VnTree.vue +++ b/src/pages/Worker/WorkerDepartmentTree.vue @@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'; import { useState } from 'src/composables/useState'; import { useQuasar } from 'quasar'; import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; -import CreateDepartmentChild from '../CreateDepartmentChild.vue'; +import CreateDepartmentChild from './CreateDepartmentChild.vue'; import axios from 'axios'; import useNotify from 'src/composables/useNotify.js'; import { useRouter } from 'vue-router'; diff --git a/src/pages/Zone/Card/ZoneCard.vue b/src/pages/Zone/Card/ZoneCard.vue index f435893c0..1abbb78bf 100644 --- a/src/pages/Zone/Card/ZoneCard.vue +++ b/src/pages/Zone/Card/ZoneCard.vue @@ -5,38 +5,29 @@ import { computed } from 'vue'; import VnCard from 'components/common/VnCard.vue'; import ZoneDescriptor from './ZoneDescriptor.vue'; -import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; - -import { useStateStore } from 'stores/useStateStore'; const { t } = useI18n(); -const stateStore = useStateStore(); const route = useRoute(); const routeName = computed(() => route.name); +const customRouteRedirectName = computed(() => { + if (routeName.value === 'ZoneLocations') return null; + return routeName.value; +}); const searchBarDataKeys = { ZoneWarehouses: 'ZoneWarehouses', ZoneSummary: 'ZoneSummary', + ZoneLocations: 'ZoneLocations', }; </script> <template> - <template v-if="stateStore.isHeaderMounted()"> - <Teleport to="#searchbar"> - <VnSearchbar - :data-key="searchBarDataKeys[routeName]" - :custom-route-redirect-name="routeName" - :label="t('list.searchZone')" - :info="t('list.searchInfo')" - /> - </Teleport> - </template> <VnCard data-key="Zone" - base-url="Zones" :descriptor="ZoneDescriptor" - searchbar-data-key="ZoneList" - searchbar-url="Zones" - searchbar-label="Search zones" - searchbar-info="You can search by zone reference" + :search-data-key="searchBarDataKeys[routeName]" + :search-custom-route-redirect="customRouteRedirectName" + :search-redirect="!!customRouteRedirectName" + :searchbar-label="t('list.searchZone')" + :searchbar-info="t('list.searchInfo')" /> </template> diff --git a/src/pages/Zone/Card/ZoneLocations.vue b/src/pages/Zone/Card/ZoneLocations.vue index e4305c898..76a216215 100644 --- a/src/pages/Zone/Card/ZoneLocations.vue +++ b/src/pages/Zone/Card/ZoneLocations.vue @@ -1 +1,80 @@ -<template>Zone Locations</template> +<script setup> +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; + +import ZoneLocationsTree from './ZoneLocationsTree.vue'; + +import axios from 'axios'; + +const { t } = useI18n(); +const route = useRoute(); + +const onSelected = async (val, node) => { + try { + if (val === null) val = undefined; + const params = { geoId: node.id, isIncluded: val }; + await axios.post(`Zones/${route.params.id}/toggleIsIncluded`, params); + } catch (err) { + console.error('Error updating included', err); + } +}; +</script> + +<template> + <QPage class="column items-center q-pa-md"> + <QCard class="full-width q-pa-md" style="max-width: 800px"> + <ZoneLocationsTree :root-label="t('zoneLocations.locations')"> + <template #checkbox="{ node }"> + <QCheckbox + v-if="node.id" + v-model="node.selected" + :label="node.name" + @update:model-value="($event) => onSelected($event, node)" + toggle-indeterminate + color="transparent" + :class="[ + 'checkbox', + node.selected + ? '--checked' + : node.selected == false + ? '--unchecked' + : '--indeterminate', + ]" + /> + </template> + </ZoneLocationsTree> + </QCard> + </QPage> +</template> + +<style lang="scss"> +.checkbox { + &.--checked { + .q-checkbox__bg { + border: 1px solid $info !important; + } + .q-checkbox__svg { + color: white !important; + background-color: $info !important; + } + } + + &.--unchecked { + .q-checkbox__bg { + border: 1px solid $negative !important; + } + .q-checkbox__svg { + background-color: $negative !important; + } + } + + &.--indeterminate { + .q-checkbox__bg { + border: 1px solid $white !important; + } + .q-checkbox__svg { + color: transparent !important; + } + } +} +</style> diff --git a/src/pages/Zone/Card/ZoneLocationsTree.vue b/src/pages/Zone/Card/ZoneLocationsTree.vue new file mode 100644 index 000000000..a42111592 --- /dev/null +++ b/src/pages/Zone/Card/ZoneLocationsTree.vue @@ -0,0 +1,172 @@ +<script setup> +import { onMounted, ref, computed, watch } from 'vue'; +import { useI18n } from 'vue-i18n'; +import { useRoute } from 'vue-router'; + +import { useState } from 'src/composables/useState'; +import axios from 'axios'; +import { useArrayData } from 'composables/useArrayData'; +import { onUnmounted } from 'vue'; + +const { t } = useI18n(); +const route = useRoute(); +const state = useState(); + +const treeRef = ref(); +const expanded = ref([]); + +const arrayData = useArrayData('ZoneLocations', { + url: `Zones/${route.params.id}/getLeaves`, +}); +const { store } = arrayData; +const storeData = computed(() => store.data); + +const nodes = ref([ + { id: null, name: t('zoneLocations.locations'), sons: true, childs: [{}] }, +]); + +const previousExpandedNodes = ref(new Set()); + +const onNodeExpanded = async (nodeKeysArray) => { + let nodeKeysSet = new Set(nodeKeysArray); + const lastNodeKey = nodeKeysArray.at(-1); + + if (!nodeKeysSet.has(null)) return; + + const wasExpanded = !previousExpandedNodes.value.has(lastNodeKey); + if (wasExpanded) await fetchNodeLeaves(lastNodeKey); + else { + const difference = new Set( + [...previousExpandedNodes.value].filter((x) => !nodeKeysSet.has(x)) + ); + const collapsedNode = Array.from(difference).pop(); + const node = treeRef.value?.getNodeByKey(collapsedNode); + const allNodeIds = getNodeIds(node); + expanded.value = expanded.value.filter((id) => !allNodeIds.includes(id)); + } + previousExpandedNodes.value = nodeKeysSet; +}; + +const formatNodeSelected = (node) => { + if (node.selected === 1) node.selected = true; + else if (node.selected === 0) node.selected = false; + + if (node.childs && node.childs.length > 0) { + expanded.value.push(node.id); + + node.childs.forEach((childNode) => { + formatNodeSelected(childNode); + }); + } + + if (node.sons > 0 && !node.childs) node.childs = [{}]; +}; + +const fetchNodeLeaves = async (nodeKey) => { + try { + const node = treeRef.value?.getNodeByKey(nodeKey); + if (!node || node.sons === 0) return; + + const params = { parentId: node.id }; + const response = await axios.get(`Zones/${route.params.id}/getLeaves`, { + params, + }); + if (response.data) { + node.childs = response.data.map((n) => { + formatNodeSelected(n); + return n; + }); + } + + state.set('Tree', node); + } catch (err) { + console.error('Error fetching department leaves', err); + throw new Error(); + } +}; + +function getNodeIds(node) { + let ids = []; + if (node.id) ids.push(node.id); + + if (node.childs) + node.childs.forEach((child) => { + ids = ids.concat(getNodeIds(child)); + }); + return ids; +} + +watch(storeData, async (val) => { + // Se triggerea cuando se actualiza el store.data, el cual es el resultado del fetch de la searchbar + nodes.value[0].childs = [...val]; + const fetchedNodeKeys = val.flatMap(getNodeIds); + state.set('Tree', [...fetchedNodeKeys]); + + if (store.userParams?.search === '') { + val.forEach((n) => { + formatNodeSelected(n); + }); + } else { + for (let n of state.get('Tree')) { + await fetchNodeLeaves(n); + } + expanded.value = [null, ...fetchedNodeKeys]; + } + previousExpandedNodes.value = new Set(expanded.value); +}); + +onMounted(async () => { + if (store.userParams?.search) { + await arrayData.fetch({ append: false }); + return; + } + const stateTree = state.get('Tree'); + const tree = stateTree ? [...state.get('Tree')] : [null]; + const lastStateTree = state.get('TreeState'); + if (tree) { + for (let n of tree) { + await fetchNodeLeaves(n); + } + + if (lastStateTree) { + tree.push(lastStateTree); + await fetchNodeLeaves(lastStateTree); + } + } + + setTimeout(() => { + if (lastStateTree) { + document.getElementById(lastStateTree).scrollIntoView(); + } + }, 1000); + + previousExpandedNodes.value = new Set(expanded.value); +}); + +onUnmounted(() => { + state.set('Tree', undefined); +}); +</script> + +<template> + <QTree + ref="treeRef" + :nodes="nodes" + node-key="id" + label-key="name" + children-key="childs" + v-model:expanded="expanded" + @update:expanded="onNodeExpanded($event)" + :default-expand-all="true" + > + <template #default-header="{ node }"> + <div + :id="node.id" + class="qtr row justify-between full-width q-pr-md cursor-pointer" + > + <span v-if="!node.id">{{ node.name }}</span> + <slot name="checkbox" :node="node" /> + </div> + </template> + </QTree> +</template> diff --git a/src/pages/Zone/locale/en.yml b/src/pages/Zone/locale/en.yml index bae89fda9..a1d741b84 100644 --- a/src/pages/Zone/locale/en.yml +++ b/src/pages/Zone/locale/en.yml @@ -42,6 +42,8 @@ summary: filterPanel: name: Name agencyModeFk: Agency +zoneLocations: + locations: Locations deliveryPanel: pickup: Pick up delivery: Delivery diff --git a/src/pages/Zone/locale/es.yml b/src/pages/Zone/locale/es.yml index d74238a6e..d12c4f204 100644 --- a/src/pages/Zone/locale/es.yml +++ b/src/pages/Zone/locale/es.yml @@ -42,6 +42,8 @@ summary: filterPanel: name: Nombre agencyModeFk: Agencia +zoneLocations: + locations: Localizaciones deliveryPanel: pickup: Recogida delivery: Entrega diff --git a/src/router/modules/zone.js b/src/router/modules/zone.js index c355856b1..cf2e5321e 100644 --- a/src/router/modules/zone.js +++ b/src/router/modules/zone.js @@ -106,7 +106,7 @@ export default { path: 'location', meta: { title: 'locations', - icon: 'vn:greuge', + icon: 'my_location', }, component: () => import('src/pages/Zone/Card/ZoneLocations.vue'), },