v-data-table-server pagination not working

`I have 3 components for a table. In my employees table, when i click the next button in the bottom of the table, it sends two request, one with the next page url, and simultaneously it sends the request with the previous page url, its driving me crazy. I dont find the place where it is being called twice the request. It behaves like the next and previous button are being pressed at the same time. I removed some style for this to fit the characters maximum

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>First component
<template>
<v-container>
<v-row class="justify-start pt-4 px-4">
<v-col class="d-flex align-center mb-5" cols="9">
<v-icon class="mr-2" color="#C9C6FF">mdi-filter-variant</v-icon>
<p style="color: #C9C6FF; font-size: 1.25rem; margin-right: 1rem">Filtros</p>
<v-btn v-for="sf in statusFilters" v-if="hasQuickFilters" :key="sf.value"
:class="{ 'selected-icon': sf.isSelected }" :size="isNotMobile? 'medium': 'x-small'"
:value="sf.value" :variant="sf.isSelected ? 'flat' : 'outlined'" class="mx-2 filter-btn"
rounded="lg" @click="setFilter(sf)">{{ sf.label }}
</v-btn>
<v-btn v-if="hasQuickFilters || hasFilters" :size="isNotMobile? 'x-large': 'small'" class="ml-n3"
variant="text" @click="clearFilters">
<v-icon size="large">mdi-backspace-outline</v-icon>
</v-btn>
<v-btn v-if="!showSearchbar" :size="isNotMobile? 'x-large': 'small'" class="ml-n3"
transition="scroll-x-transition" variant="text"
@click="showSearchbar = !showSearchbar">
<v-icon>mdi-magnify</v-icon>
</v-btn>
<v-slide-x-transition>
<v-text-field
v-if="showSearchbar"
v-model="search"
density="compact"
fluid
hide-details
label="Buscar"
prepend-inner-icon="mdi-magnify"
rounded="lg"
single-line
transition="scroll-x-transition"
variant="outlined"
@input="searchFilter({value: search, key: 'search', icon: 'mdi-magnify', isSelected: false})"
>
<template v-slot:append-inner>
<v-icon @click="showSearchbar = !showSearchbar">mdi-close</v-icon>
</template>
</v-text-field>
</v-slide-x-transition>
</v-col>
<!-- <v-col cols="3" class="justify-center">-->
<!-- <v-slide-x-transition>-->
<!-- <v-text-field-->
<!-- v-if="showSearchbar"-->
<!-- v-model="search"-->
<!-- density="compact"-->
<!-- hide-details-->
<!-- label="Buscar"-->
<!-- prepend-inner-icon="mdi-magnify"-->
<!-- rounded="lg"-->
<!-- single-line-->
<!-- transition="scroll-x-transition"-->
<!-- variant="outlined"-->
<!-- @input="searchFilter({value: search, key: 'search', icon: 'mdi-magnify', isSelected: false})"-->
<!-- >-->
<!-- <template v-slot:append-inner>-->
<!-- <v-icon @click="showSearchbar = !showSearchbar">mdi-close</v-icon>-->
<!-- </template>-->
<!-- </v-text-field>-->
<!-- </v-slide-x-transition>-->
<!-- </v-col>-->
</v-row>
<v-row class="mt-0">
<v-col :cols="tableCols">
<v-data-table-server
:headers="headers"
:items="items"
:items-length="itemsLength"
:items-per-page="pageSize"
:items-per-page-options="[10, 25, 50, 100]"
:loading="loader"
:mobile="false"
:show-current-page="true"
:style="{ height: `calc(100vh - 234px)`, backgroundColor: '#1B1B1B', color: 'white'}"
disable-sort
first-icon="mdi-user"
fixed-footer
items-per-page-text=""
no-data-text="No hay datos disponibles"
search=""
@update:options="loadItems">
<template v-slot:item.is_active="{ item }: { item: ListItem }">
<v-chip v-if="item.is_active" color="green">Si</v-chip>
<v-chip v-else color="red">No</v-chip>
</template>
<template v-slot:item.status="{ item }: { item: ListItem }">
<v-chip v-if="item.status" color="green">En uso</v-chip>
<v-chip v-else color="red">Inactivo</v-chip>
</template>
<template v-slot:item.firstCol="{ item }: { item: ListItem }">
<v-btn color="#6C6CF1" style="min-width: 100px;" variant="outlined" @click="goToEdit(item)">{{
item[keyName]
}}
</v-btn>
</template>
<template v-slot:item.permissions="{ item }: { item: ListItem }">
<v-list class="d-flex">
<v-list-item
v-for="n in item.permissions"
:key="n"
:title="n"
></v-list-item>
</v-list>
</template>
<template v-slot:item.created="{ item }: { item: ListItem }">
{{ parseTimeUTCVE(item.created) }}
</template>
<template v-slot:item.access_point="{ item }: { item: ListItem }">
{{ item.access_point }} - {{ parseTimeUTCVE(item.entry_time) }}
</template>
<template v-slot:item.exit_point="{ item }: { item: ListItem }">
{{ item.exit_point }} - {{ parseTimeUTCVE(item.exit_time) }}
</template>
<template v-slot:item.cash_balance="{ item }: { item: ListItem }">
USD {{ item.cash_balance["USD"] }} - BS {{ item.cash_balance.BS }}
</template>
</v-data-table-server>
</v-col>
<v-slide-x-transition>
<v-col v-if="showFilters" :cols="filterCols"
:style="{ height: `calc(100vh - 168px)`, overflowY: 'auto', borderLeft: '1px rgb(128,128,128) solid' }">
<h3>Filtros</h3>
<v-row>
<v-col>
<slot/>
</v-col>
</v-row>
</v-col>
</v-slide-x-transition>
</v-row>
</v-container>
</template>
<script lang="ts">
import type {Filter, ListItem} from "@/types";
import {debounce} from "lodash";
import {parseTimeUTCVE} from "@/utils";
import {useDisplay} from "vuetify";
export default {
props: {
headers: {
type: Array<object>,
required: true
},
items: {
type: Array<ListItem>,
required: true
},
pageSize: {
type: Number,
required: true
},
loader: {
type: Boolean,
required: true
},
hasFilters: {
type: Boolean,
required: true
},
hasQuickFilters: {
type: Boolean,
default: true
},
keyName: {
type: String,
required: true
},
editKey: {
type: String,
required: true
},
itemsLength: {
type: Number,
required: true
},
statusFilters: {
type: Array<Filter>,
required: true
},
filterSelected: {
type: Boolean,
required: false
},
},
emits: ["submit", "create", "update-table", "set-filter", "clear-filters"],
data() {
return {
fab: false,
selectedStatusFilter: false,
showFilters: false,
areFiltersApplied: false,
search: '',
tableCols: 12,
filterCols: 0,
showSearchbar: false,
filterIcon: 'mdi-filter-menu'
}
},
computed: {
isNotMobile() {
const {mdAndUp} = useDisplay()
return mdAndUp.value
}
},
methods: {
parseTimeUTCVE,
goToEdit(item: object) {
this.$emit('submit', item)
},
goToCreate() {
this.$emit('create')
},
loadItems(options: any) {
const page = options.page
const itemsPerPage = options.itemsPerPage
const sortBy = options.sortBy
this.$emit('update-table', {page, itemsPerPage, sortBy});
},
setFilter(filter: Filter) {
filter.isSelected = !filter.isSelected
this.statusFilters.forEach((sf) => {
if (sf !== filter) {
sf.isSelected = false;
}
});
this.$emit('set-filter', filter)
},
searchFilter: debounce(function (this: any, filter: Filter) {
filter.isSelected = !filter.isSelected
this.$emit('set-filter', filter)
}, 700),
toggleFilters() {
this.showFilters = !this.showFilters
this.filterCols = this.showFilters ? 4 : 0
this.tableCols = this.showFilters ? 8 : 12
this.filterIcon = this.showFilters ? 'mdi-filter-remove' : 'mdi-filter-menu'
},
clearFilters() {
this.$emit('clear-filters');
}
},
}
</script>
</style>
Second component
<template>
<v-row>
<v-col class="d-flex justify-end" cols="12">
<v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 100px;" variant="outlined">
<v-icon class="mr-3">mdi-delete</v-icon>
Eliminar
</v-btn>
<v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 100px;" variant="outlined"
@click="goToCreate()">
<v-icon class="mr-3">mdi-plus-circle-outline</v-icon>
Crear
</v-btn>
<v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 120px;" variant="outlined">
<v-icon>mdi-folder-plus</v-icon>
Carga Masiva
</v-btn>
<v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 120px;" variant="outlined">
<v-icon class="mr-2">mdi-download</v-icon>
Descargar
</v-btn>
</v-col>
</v-row>
<v-card :style="{ height: `calc(100vh - 100px)`, overflowY: 'auto' }" class="my-4" color="#1B1B1B"
elevation="1" rounded="lg">
<InnerTable
:edit-key="editKey"
:filter-selected="filterSelected"
:has-filters="hasFilters"
:has-quick-filters="hasQuickFilters"
:headers="headers"
:items="items"
:items-length="itemsLength"
:key-name="keyName"
:loader="loader"
:page-size="pageSize"
:status-filters="statusFilters"
@create="goToCreate"
@submit="goToEdit"
@update-table="updateTable"
@set-filter="setFilter"
@clear-filters="clearFilters"
>
<template v-slot:default>
<slot></slot>
</template>
</InnerTable>
</v-card>
</template>
<script lang="ts">
import InnerTable from './InnerTable.vue';
import type {Filter, ListItem} from "@/types";
import {useDisplay} from "vuetify";
export default {
name: 'ListTable',
components: {
InnerTable
},
props: {
headers: {
type: Array<object>,
required: true
},
items: {
type: Array<ListItem>,
required: true
},
disabledFab: {
type: Boolean,
required: true
},
pageSize: {
type: Number,
required: true
},
loader: {
type: Boolean,
required: true
},
hasFilters: {
type: Boolean,
required: true
},
hasQuickFilters: {
type: Boolean,
default: true
},
keyName: {
type: String,
required: true
},
editKey: {
type: String,
required: true
},
itemsLength: {
type: Number,
required: true
},
statusFilters: {
type: Array<Filter>,
required: true
},
filterSelected: {
type: Boolean,
required: false
},
},
emits: ["submit", "create", "update-table", "set-filter", "clear-filters"],
data() {
return {
fab: false,
selectedStatusFilter: false,
showFilters: false,
areFiltersApplied: false,
search: '',
tableCols: 12,
filterCols: 0,
showSearchbar: false,
tableHeight: '',
filterIcon: 'mdi-filter-menu',
hideFooter: false,
}
},
computed: {
isNotMobile() {
const {mdAndUp} = useDisplay()
return mdAndUp.value
}
},
methods: {
goToEdit(item: object) {
this.$emit('submit', item)
},
goToCreate() {
this.$emit('create')
},
updateTable(options: any) {
this.$emit('update-table', options);
},
setFilter(filter: Filter) {
this.$emit('set-filter', filter)
},
clearFilters() {
this.$emit('clear-filters');
}
},
}
</script>
<style scoped>
@media only screen and (min-width: 760px) {
::v-deep .v-data-table-footer {
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: center;
padding: 8px 4px;
}
}
.actions-buttons {
width: 8vw;
text-transform: capitalize;
margin-left: 5px;
margin-right: 5px;
border-radius: 10px;
}
</style>
Third component
<template>
<list-table
:current-page="page"
:disabled-fab="showFab" :filter-selected="filterSelected"
:has-filters="true"
:headers="getColumns" :items="users"
:items-length="usersCount"
:loader="loader"
:page-size="pageSize"
:status-filters="statusFilters"
edit-key="id"
key-name="dni"
@create="goToCreate()"
@submit="goToEdit($event)"
@update-table="updateTable($event)"
@set-filter="setFilter"
@clear-filters="clearFilter"
>
<v-select v-model="company" :items="companies"
clearable density="compact"
item-title="name" item-value="name" label="Empresa" rounded="xl" variant="outlined"/>
</list-table>
</template>
<script lang="ts">
import ListTable from '@/components/ListTable.vue'
import {userStore} from '@/stores/user'
import type {baseObject, BreadcrumbsArray, Filter, PaginationProps, User} from "@/types";
import {miscStore} from "@/stores/runtime";
import {useDisplay} from "vuetify";
import {companyStore} from "@/stores/company";
import {errorHandling} from "@/utils/error";
import type {AxiosError} from "axios";
export default {
name: 'Employees',
components: {ListTable},
data() {
return {
miscStore: miscStore(),
userStore: userStore(),
companyStore: companyStore(),
loader: true,
showFab: true,
page: 1,
pageSize: 10,
state: null,
params: {},
isRequesting: false,
parking: null,
company: null,
filterSelected: false,
clearingFilter: false,
columns: [
{
title: 'DNI',
align: 'start',
key: 'firstCol'
},
{
title: 'Empresa',
align: 'start',
key: 'company'
},
{
title: 'Usuario',
align: 'start',
key: 'username'
},
{
title: 'Nombre',
align: 'start',
key: 'full_name'
},
{
title: 'Roles',
align: 'start',
key: 'roles'
},
{
title: 'Activo',
align: 'start',
key: 'is_active'
},
],
statusFilters: [
{key: 'is_active', value: true, label: 'Activo', isSelected: false},
{key: 'is_active', value: false, label: 'Inactivo', isSelected: false},
],
}
},
async created() {
await this.companyStore.fetchCompanies(1, {}, 100)
this.userStore.fetchUserPermissions()
if (this.userPermissions.create === false) {
this.showFab = false
}
},
computed: {
users() {
return this.userStore.getUsers
},
companies() {
return this.companyStore.getCompanies
},
userPermissions() {
return this.userStore.getUserPermissions
},
isNotMobile() {
const {mdAndUp} = useDisplay()
return mdAndUp.value
},
getColumns() {
const columnsMobile = ['Cédula', 'Nombre de usuario', 'Activo']
if (this.isNotMobile) {
return this.columns
} else {
return this.columns.filter(column => columnsMobile.includes(column.title))
}
},
usersCount() {
return this.userStore.getUsersCount
},
breadcrumbs: {
get() {
return this.miscStore.getBreadcrumbs;
},
set(newBreadcrumbs: BreadcrumbsArray) {
// Perform any validation or logic before setting
this.miscStore.setBreadcrumbs(newBreadcrumbs);
}
}
},
methods: {
async goToEdit(user: User) {
this.loader = true
this.breadcrumbs.push({title: user.username, to: '/update-employee'})
this.miscStore.setBreadcrumbs(this.breadcrumbs)
await this.userStore.fetchEmployeeById(user.id)
this.$router.push('/update-employee')
},
goToCreate() {
this.breadcrumbs.push({title: "Creación de empleado", to: '/create-employee'})
this.miscStore.setBreadcrumbs(this.breadcrumbs)
this.$router.push('/create-employee')
},
async fetchEmployees(page: number, query_params: object, pageSize: number) {
this.loader = true
await this.userStore.fetchEmployees(page, query_params, pageSize).catch((error: AxiosError) => {
errorHandling(error)
})
this.loader = false
},
async updateTable(tableProps: PaginationProps) {
await this.fetchEmployees(tableProps.page, this.params, tableProps.itemsPerPage)
},
async setFilter(filter: Filter) {
if (filter.value === (this.params as baseObject)[filter.key]) {
delete (this.params as baseObject)[filter.key];
} else {
(this.params as baseObject)[filter.key] = filter.value;
}
await this.fetchEmployees(1, this.params, this.pageSize);
},
async clearFilter() {
this.clearingFilter = true
this.params = {}
this.statusFilters = this.statusFilters.map(item => ({...item, isSelected: false}));
this.clearingFilter = false
await this.fetchEmployees(1, this.params, this.pageSize);
},
},
unmounted() {
this.userStore.clearUsers()
},
}
</script>
<style scoped>
</style>
Store
import {acceptHMRUpdate, defineStore} from 'pinia'
import {AxiosDelete, AxiosGet, AxiosLogin, AxiosPatch, AxiosPost, AxiosPut, ServiceUrls} from '@/plugins/axios'
import type {ListItem, LoginResponse, Nullable, PermissionsBasicInfo, User} from '@/types'
import {parsePermissions} from "@/utils/parsePermissions";
export const userStore = defineStore('user', {
state: () => ({
firstName: '',
middleName: '' as Nullable<string>,
lastName: '',
users: [] as Array<ListItem>,
roles: [] as Array<ListItem>,
accountOperations: [] as Array<ListItem>,
user: {} as User,
account: {} as User,
usersCount: 0 as number,
operationsCount: 0 as number,
isLoggedIn: false,
code: 0,
username: '' as string,
accountPermissions: {} as PermissionsBasicInfo,
userPermissions: {} as PermissionsBasicInfo,
employeesDropdown: [] as Array<ListItem>,
}),
getters: {
getFirstName: (state) => state.firstName,
getIsLoggedIn: (state) => state.isLoggedIn,
getUsers: (state) => state.users,
getUser: (state) => state.user,
getUsername: (state) => state.username,
getUsersCount: (state) => state.usersCount,
getRoles: (state) => state.roles,
getAccount: (state) => state.account,
getAccountPermissions: (state) => state.accountPermissions,
getUserPermissions: (state) => state.userPermissions,
getEmployeesDropdown: (state) => state.employeesDropdown
},
actions: {
loginUser(username: string, password: string) {
return AxiosLogin(ServiceUrls.LOGIN, {username, password})
},
async logoutUser() {
let url = ServiceUrls.USERS + 'logout/'
this.setLoggedInState(false);
localStorage.removeItem('permissions')
localStorage.removeItem('token')
await AxiosDelete(url)
},
async fetchEmployees(page: number, baseParams: object, pageSize: number) {
this.usersCount = 0
let url = ServiceUrls.EMPLOYEE
const params = {
page,
page_size: pageSize,
...baseParams
}
let response = await AxiosGet(url, params)
this.users = response.results
this.usersCount = response.count
},
async fetchDropDownEmployees() {
let url = ServiceUrls.DROPDOWN_EMPLOYEES
this.employeesDropdown = await AxiosGet(url)
},
async fetchAccounts(page: number, baseParams: object, pageSize: number) {
this.usersCount = 0
let url = ServiceUrls.ACCOUNTS
const params = {
page,
page_size: pageSize,
...baseParams
}
const response = await AxiosGet(url, params)
this.users = response.results
this.usersCount = response.count
},
async fetchEmployeeById(id: string) {
let url = ServiceUrls.EMPLOYEE + id + '/'
this.user = await AxiosGet(url)
localStorage.setItem('user', JSON.stringify(this.user));
},
async fetchAccountByUsername(username: string) {
let url = ServiceUrls.ACCOUNTS + username + '/'
this.account = await AxiosGet(url)
localStorage.setItem('account', JSON.stringify(this.account));
},
async fetchAccountOperations(page: number, username: string, filters: object, pageSize: number) {
let url = ServiceUrls.USERS + username + '/operations/'
this.usersCount = 0
const params = {
page,
page_size: pageSize,
...filters
}
const response = await AxiosGet(url, params)
this.accountOperations = response.results
this.operationsCount = response.count
},
async recharge(data: object, username: string) {
let url = ServiceUrls.RECHARGE
let baseUrl = url.replace(':username', username)
return await AxiosPost(baseUrl, data)
},
async createEmployee(data: object) {
let url = ServiceUrls.EMPLOYEE
return await AxiosPost(url, data)
},
async createAccount(data: object) {
let url = ServiceUrls.ACCOUNTS
return await AxiosPost(url, data)
},
async updateUser(data: object, id: string) {
let url = ServiceUrls.EMPLOYEE + id + '/'
return await AxiosPatch(url, data)
},
async updateEmployee(data: object, username: string) {
let url = ServiceUrls.EMPLOYEE + username + '/'
return await AxiosPut(url, data)
},
async patchUser(data: object, username: string) {
let url = ServiceUrls.USERS + username + '/'
return await AxiosPatch(url, data)
},
async deleteUser(username: string) {
let url = ServiceUrls.EMPLOYEE + username + '/'
return await AxiosDelete(url)
},
setUserData(data: LoginResponse) {
if (data) {
// this.username = data.username
this.setLoggedInState(true);
const permissions = JSON.stringify(data.user.permissions)
localStorage.setItem('permissions', permissions)
localStorage.setItem('token', data.token)
// localStorage.setItem('username', this.username)
} else {
throw new Error('Credenciales inválidas')
}
},
clearUsers() {
this.users = []
},
setLoggedInState(isLoggedIn: boolean) {
this.isLoggedIn = isLoggedIn;
localStorage.setItem('isLoggedIn', JSON.stringify(isLoggedIn));
},
initLoggedInState() {
const loggedInState = localStorage.getItem('isLoggedIn');
const username = localStorage.getItem('username')
if (loggedInState) {
this.isLoggedIn = JSON.parse(loggedInState);
} else {
this.isLoggedIn = false
}
if (username) {
this.username = username
}
},
initEmployee() {
const user = localStorage.getItem('user');
if (user) {
this.user = JSON.parse(user);
}
},
clearEmployee() {
this.user = {} as User
localStorage.removeItem('user')
},
initAccount() {
const account = localStorage.getItem('account');
if (account) {
this.account = JSON.parse(account);
}
},
clearAccountOperations() {
this.accountOperations = []
},
fetchAccountPermissions() {
this.accountPermissions = parsePermissions('account')
},
fetchUserPermissions() {
this.userPermissions = parsePermissions('employee')
}
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(userStore, import.meta.hot))
}
</code>
<code>First component <template> <v-container> <v-row class="justify-start pt-4 px-4"> <v-col class="d-flex align-center mb-5" cols="9"> <v-icon class="mr-2" color="#C9C6FF">mdi-filter-variant</v-icon> <p style="color: #C9C6FF; font-size: 1.25rem; margin-right: 1rem">Filtros</p> <v-btn v-for="sf in statusFilters" v-if="hasQuickFilters" :key="sf.value" :class="{ 'selected-icon': sf.isSelected }" :size="isNotMobile? 'medium': 'x-small'" :value="sf.value" :variant="sf.isSelected ? 'flat' : 'outlined'" class="mx-2 filter-btn" rounded="lg" @click="setFilter(sf)">{{ sf.label }} </v-btn> <v-btn v-if="hasQuickFilters || hasFilters" :size="isNotMobile? 'x-large': 'small'" class="ml-n3" variant="text" @click="clearFilters"> <v-icon size="large">mdi-backspace-outline</v-icon> </v-btn> <v-btn v-if="!showSearchbar" :size="isNotMobile? 'x-large': 'small'" class="ml-n3" transition="scroll-x-transition" variant="text" @click="showSearchbar = !showSearchbar"> <v-icon>mdi-magnify</v-icon> </v-btn> <v-slide-x-transition> <v-text-field v-if="showSearchbar" v-model="search" density="compact" fluid hide-details label="Buscar" prepend-inner-icon="mdi-magnify" rounded="lg" single-line transition="scroll-x-transition" variant="outlined" @input="searchFilter({value: search, key: 'search', icon: 'mdi-magnify', isSelected: false})" > <template v-slot:append-inner> <v-icon @click="showSearchbar = !showSearchbar">mdi-close</v-icon> </template> </v-text-field> </v-slide-x-transition> </v-col> <!-- <v-col cols="3" class="justify-center">--> <!-- <v-slide-x-transition>--> <!-- <v-text-field--> <!-- v-if="showSearchbar"--> <!-- v-model="search"--> <!-- density="compact"--> <!-- hide-details--> <!-- label="Buscar"--> <!-- prepend-inner-icon="mdi-magnify"--> <!-- rounded="lg"--> <!-- single-line--> <!-- transition="scroll-x-transition"--> <!-- variant="outlined"--> <!-- @input="searchFilter({value: search, key: 'search', icon: 'mdi-magnify', isSelected: false})"--> <!-- >--> <!-- <template v-slot:append-inner>--> <!-- <v-icon @click="showSearchbar = !showSearchbar">mdi-close</v-icon>--> <!-- </template>--> <!-- </v-text-field>--> <!-- </v-slide-x-transition>--> <!-- </v-col>--> </v-row> <v-row class="mt-0"> <v-col :cols="tableCols"> <v-data-table-server :headers="headers" :items="items" :items-length="itemsLength" :items-per-page="pageSize" :items-per-page-options="[10, 25, 50, 100]" :loading="loader" :mobile="false" :show-current-page="true" :style="{ height: `calc(100vh - 234px)`, backgroundColor: '#1B1B1B', color: 'white'}" disable-sort first-icon="mdi-user" fixed-footer items-per-page-text="" no-data-text="No hay datos disponibles" search="" @update:options="loadItems"> <template v-slot:item.is_active="{ item }: { item: ListItem }"> <v-chip v-if="item.is_active" color="green">Si</v-chip> <v-chip v-else color="red">No</v-chip> </template> <template v-slot:item.status="{ item }: { item: ListItem }"> <v-chip v-if="item.status" color="green">En uso</v-chip> <v-chip v-else color="red">Inactivo</v-chip> </template> <template v-slot:item.firstCol="{ item }: { item: ListItem }"> <v-btn color="#6C6CF1" style="min-width: 100px;" variant="outlined" @click="goToEdit(item)">{{ item[keyName] }} </v-btn> </template> <template v-slot:item.permissions="{ item }: { item: ListItem }"> <v-list class="d-flex"> <v-list-item v-for="n in item.permissions" :key="n" :title="n" ></v-list-item> </v-list> </template> <template v-slot:item.created="{ item }: { item: ListItem }"> {{ parseTimeUTCVE(item.created) }} </template> <template v-slot:item.access_point="{ item }: { item: ListItem }"> {{ item.access_point }} - {{ parseTimeUTCVE(item.entry_time) }} </template> <template v-slot:item.exit_point="{ item }: { item: ListItem }"> {{ item.exit_point }} - {{ parseTimeUTCVE(item.exit_time) }} </template> <template v-slot:item.cash_balance="{ item }: { item: ListItem }"> USD {{ item.cash_balance["USD"] }} - BS {{ item.cash_balance.BS }} </template> </v-data-table-server> </v-col> <v-slide-x-transition> <v-col v-if="showFilters" :cols="filterCols" :style="{ height: `calc(100vh - 168px)`, overflowY: 'auto', borderLeft: '1px rgb(128,128,128) solid' }"> <h3>Filtros</h3> <v-row> <v-col> <slot/> </v-col> </v-row> </v-col> </v-slide-x-transition> </v-row> </v-container> </template> <script lang="ts"> import type {Filter, ListItem} from "@/types"; import {debounce} from "lodash"; import {parseTimeUTCVE} from "@/utils"; import {useDisplay} from "vuetify"; export default { props: { headers: { type: Array<object>, required: true }, items: { type: Array<ListItem>, required: true }, pageSize: { type: Number, required: true }, loader: { type: Boolean, required: true }, hasFilters: { type: Boolean, required: true }, hasQuickFilters: { type: Boolean, default: true }, keyName: { type: String, required: true }, editKey: { type: String, required: true }, itemsLength: { type: Number, required: true }, statusFilters: { type: Array<Filter>, required: true }, filterSelected: { type: Boolean, required: false }, }, emits: ["submit", "create", "update-table", "set-filter", "clear-filters"], data() { return { fab: false, selectedStatusFilter: false, showFilters: false, areFiltersApplied: false, search: '', tableCols: 12, filterCols: 0, showSearchbar: false, filterIcon: 'mdi-filter-menu' } }, computed: { isNotMobile() { const {mdAndUp} = useDisplay() return mdAndUp.value } }, methods: { parseTimeUTCVE, goToEdit(item: object) { this.$emit('submit', item) }, goToCreate() { this.$emit('create') }, loadItems(options: any) { const page = options.page const itemsPerPage = options.itemsPerPage const sortBy = options.sortBy this.$emit('update-table', {page, itemsPerPage, sortBy}); }, setFilter(filter: Filter) { filter.isSelected = !filter.isSelected this.statusFilters.forEach((sf) => { if (sf !== filter) { sf.isSelected = false; } }); this.$emit('set-filter', filter) }, searchFilter: debounce(function (this: any, filter: Filter) { filter.isSelected = !filter.isSelected this.$emit('set-filter', filter) }, 700), toggleFilters() { this.showFilters = !this.showFilters this.filterCols = this.showFilters ? 4 : 0 this.tableCols = this.showFilters ? 8 : 12 this.filterIcon = this.showFilters ? 'mdi-filter-remove' : 'mdi-filter-menu' }, clearFilters() { this.$emit('clear-filters'); } }, } </script> </style> Second component <template> <v-row> <v-col class="d-flex justify-end" cols="12"> <v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 100px;" variant="outlined"> <v-icon class="mr-3">mdi-delete</v-icon> Eliminar </v-btn> <v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 100px;" variant="outlined" @click="goToCreate()"> <v-icon class="mr-3">mdi-plus-circle-outline</v-icon> Crear </v-btn> <v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 120px;" variant="outlined"> <v-icon>mdi-folder-plus</v-icon> Carga Masiva </v-btn> <v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 120px;" variant="outlined"> <v-icon class="mr-2">mdi-download</v-icon> Descargar </v-btn> </v-col> </v-row> <v-card :style="{ height: `calc(100vh - 100px)`, overflowY: 'auto' }" class="my-4" color="#1B1B1B" elevation="1" rounded="lg"> <InnerTable :edit-key="editKey" :filter-selected="filterSelected" :has-filters="hasFilters" :has-quick-filters="hasQuickFilters" :headers="headers" :items="items" :items-length="itemsLength" :key-name="keyName" :loader="loader" :page-size="pageSize" :status-filters="statusFilters" @create="goToCreate" @submit="goToEdit" @update-table="updateTable" @set-filter="setFilter" @clear-filters="clearFilters" > <template v-slot:default> <slot></slot> </template> </InnerTable> </v-card> </template> <script lang="ts"> import InnerTable from './InnerTable.vue'; import type {Filter, ListItem} from "@/types"; import {useDisplay} from "vuetify"; export default { name: 'ListTable', components: { InnerTable }, props: { headers: { type: Array<object>, required: true }, items: { type: Array<ListItem>, required: true }, disabledFab: { type: Boolean, required: true }, pageSize: { type: Number, required: true }, loader: { type: Boolean, required: true }, hasFilters: { type: Boolean, required: true }, hasQuickFilters: { type: Boolean, default: true }, keyName: { type: String, required: true }, editKey: { type: String, required: true }, itemsLength: { type: Number, required: true }, statusFilters: { type: Array<Filter>, required: true }, filterSelected: { type: Boolean, required: false }, }, emits: ["submit", "create", "update-table", "set-filter", "clear-filters"], data() { return { fab: false, selectedStatusFilter: false, showFilters: false, areFiltersApplied: false, search: '', tableCols: 12, filterCols: 0, showSearchbar: false, tableHeight: '', filterIcon: 'mdi-filter-menu', hideFooter: false, } }, computed: { isNotMobile() { const {mdAndUp} = useDisplay() return mdAndUp.value } }, methods: { goToEdit(item: object) { this.$emit('submit', item) }, goToCreate() { this.$emit('create') }, updateTable(options: any) { this.$emit('update-table', options); }, setFilter(filter: Filter) { this.$emit('set-filter', filter) }, clearFilters() { this.$emit('clear-filters'); } }, } </script> <style scoped> @media only screen and (min-width: 760px) { ::v-deep .v-data-table-footer { align-items: center; display: flex; flex-wrap: wrap; justify-content: center; padding: 8px 4px; } } .actions-buttons { width: 8vw; text-transform: capitalize; margin-left: 5px; margin-right: 5px; border-radius: 10px; } </style> Third component <template> <list-table :current-page="page" :disabled-fab="showFab" :filter-selected="filterSelected" :has-filters="true" :headers="getColumns" :items="users" :items-length="usersCount" :loader="loader" :page-size="pageSize" :status-filters="statusFilters" edit-key="id" key-name="dni" @create="goToCreate()" @submit="goToEdit($event)" @update-table="updateTable($event)" @set-filter="setFilter" @clear-filters="clearFilter" > <v-select v-model="company" :items="companies" clearable density="compact" item-title="name" item-value="name" label="Empresa" rounded="xl" variant="outlined"/> </list-table> </template> <script lang="ts"> import ListTable from '@/components/ListTable.vue' import {userStore} from '@/stores/user' import type {baseObject, BreadcrumbsArray, Filter, PaginationProps, User} from "@/types"; import {miscStore} from "@/stores/runtime"; import {useDisplay} from "vuetify"; import {companyStore} from "@/stores/company"; import {errorHandling} from "@/utils/error"; import type {AxiosError} from "axios"; export default { name: 'Employees', components: {ListTable}, data() { return { miscStore: miscStore(), userStore: userStore(), companyStore: companyStore(), loader: true, showFab: true, page: 1, pageSize: 10, state: null, params: {}, isRequesting: false, parking: null, company: null, filterSelected: false, clearingFilter: false, columns: [ { title: 'DNI', align: 'start', key: 'firstCol' }, { title: 'Empresa', align: 'start', key: 'company' }, { title: 'Usuario', align: 'start', key: 'username' }, { title: 'Nombre', align: 'start', key: 'full_name' }, { title: 'Roles', align: 'start', key: 'roles' }, { title: 'Activo', align: 'start', key: 'is_active' }, ], statusFilters: [ {key: 'is_active', value: true, label: 'Activo', isSelected: false}, {key: 'is_active', value: false, label: 'Inactivo', isSelected: false}, ], } }, async created() { await this.companyStore.fetchCompanies(1, {}, 100) this.userStore.fetchUserPermissions() if (this.userPermissions.create === false) { this.showFab = false } }, computed: { users() { return this.userStore.getUsers }, companies() { return this.companyStore.getCompanies }, userPermissions() { return this.userStore.getUserPermissions }, isNotMobile() { const {mdAndUp} = useDisplay() return mdAndUp.value }, getColumns() { const columnsMobile = ['Cédula', 'Nombre de usuario', 'Activo'] if (this.isNotMobile) { return this.columns } else { return this.columns.filter(column => columnsMobile.includes(column.title)) } }, usersCount() { return this.userStore.getUsersCount }, breadcrumbs: { get() { return this.miscStore.getBreadcrumbs; }, set(newBreadcrumbs: BreadcrumbsArray) { // Perform any validation or logic before setting this.miscStore.setBreadcrumbs(newBreadcrumbs); } } }, methods: { async goToEdit(user: User) { this.loader = true this.breadcrumbs.push({title: user.username, to: '/update-employee'}) this.miscStore.setBreadcrumbs(this.breadcrumbs) await this.userStore.fetchEmployeeById(user.id) this.$router.push('/update-employee') }, goToCreate() { this.breadcrumbs.push({title: "Creación de empleado", to: '/create-employee'}) this.miscStore.setBreadcrumbs(this.breadcrumbs) this.$router.push('/create-employee') }, async fetchEmployees(page: number, query_params: object, pageSize: number) { this.loader = true await this.userStore.fetchEmployees(page, query_params, pageSize).catch((error: AxiosError) => { errorHandling(error) }) this.loader = false }, async updateTable(tableProps: PaginationProps) { await this.fetchEmployees(tableProps.page, this.params, tableProps.itemsPerPage) }, async setFilter(filter: Filter) { if (filter.value === (this.params as baseObject)[filter.key]) { delete (this.params as baseObject)[filter.key]; } else { (this.params as baseObject)[filter.key] = filter.value; } await this.fetchEmployees(1, this.params, this.pageSize); }, async clearFilter() { this.clearingFilter = true this.params = {} this.statusFilters = this.statusFilters.map(item => ({...item, isSelected: false})); this.clearingFilter = false await this.fetchEmployees(1, this.params, this.pageSize); }, }, unmounted() { this.userStore.clearUsers() }, } </script> <style scoped> </style> Store import {acceptHMRUpdate, defineStore} from 'pinia' import {AxiosDelete, AxiosGet, AxiosLogin, AxiosPatch, AxiosPost, AxiosPut, ServiceUrls} from '@/plugins/axios' import type {ListItem, LoginResponse, Nullable, PermissionsBasicInfo, User} from '@/types' import {parsePermissions} from "@/utils/parsePermissions"; export const userStore = defineStore('user', { state: () => ({ firstName: '', middleName: '' as Nullable<string>, lastName: '', users: [] as Array<ListItem>, roles: [] as Array<ListItem>, accountOperations: [] as Array<ListItem>, user: {} as User, account: {} as User, usersCount: 0 as number, operationsCount: 0 as number, isLoggedIn: false, code: 0, username: '' as string, accountPermissions: {} as PermissionsBasicInfo, userPermissions: {} as PermissionsBasicInfo, employeesDropdown: [] as Array<ListItem>, }), getters: { getFirstName: (state) => state.firstName, getIsLoggedIn: (state) => state.isLoggedIn, getUsers: (state) => state.users, getUser: (state) => state.user, getUsername: (state) => state.username, getUsersCount: (state) => state.usersCount, getRoles: (state) => state.roles, getAccount: (state) => state.account, getAccountPermissions: (state) => state.accountPermissions, getUserPermissions: (state) => state.userPermissions, getEmployeesDropdown: (state) => state.employeesDropdown }, actions: { loginUser(username: string, password: string) { return AxiosLogin(ServiceUrls.LOGIN, {username, password}) }, async logoutUser() { let url = ServiceUrls.USERS + 'logout/' this.setLoggedInState(false); localStorage.removeItem('permissions') localStorage.removeItem('token') await AxiosDelete(url) }, async fetchEmployees(page: number, baseParams: object, pageSize: number) { this.usersCount = 0 let url = ServiceUrls.EMPLOYEE const params = { page, page_size: pageSize, ...baseParams } let response = await AxiosGet(url, params) this.users = response.results this.usersCount = response.count }, async fetchDropDownEmployees() { let url = ServiceUrls.DROPDOWN_EMPLOYEES this.employeesDropdown = await AxiosGet(url) }, async fetchAccounts(page: number, baseParams: object, pageSize: number) { this.usersCount = 0 let url = ServiceUrls.ACCOUNTS const params = { page, page_size: pageSize, ...baseParams } const response = await AxiosGet(url, params) this.users = response.results this.usersCount = response.count }, async fetchEmployeeById(id: string) { let url = ServiceUrls.EMPLOYEE + id + '/' this.user = await AxiosGet(url) localStorage.setItem('user', JSON.stringify(this.user)); }, async fetchAccountByUsername(username: string) { let url = ServiceUrls.ACCOUNTS + username + '/' this.account = await AxiosGet(url) localStorage.setItem('account', JSON.stringify(this.account)); }, async fetchAccountOperations(page: number, username: string, filters: object, pageSize: number) { let url = ServiceUrls.USERS + username + '/operations/' this.usersCount = 0 const params = { page, page_size: pageSize, ...filters } const response = await AxiosGet(url, params) this.accountOperations = response.results this.operationsCount = response.count }, async recharge(data: object, username: string) { let url = ServiceUrls.RECHARGE let baseUrl = url.replace(':username', username) return await AxiosPost(baseUrl, data) }, async createEmployee(data: object) { let url = ServiceUrls.EMPLOYEE return await AxiosPost(url, data) }, async createAccount(data: object) { let url = ServiceUrls.ACCOUNTS return await AxiosPost(url, data) }, async updateUser(data: object, id: string) { let url = ServiceUrls.EMPLOYEE + id + '/' return await AxiosPatch(url, data) }, async updateEmployee(data: object, username: string) { let url = ServiceUrls.EMPLOYEE + username + '/' return await AxiosPut(url, data) }, async patchUser(data: object, username: string) { let url = ServiceUrls.USERS + username + '/' return await AxiosPatch(url, data) }, async deleteUser(username: string) { let url = ServiceUrls.EMPLOYEE + username + '/' return await AxiosDelete(url) }, setUserData(data: LoginResponse) { if (data) { // this.username = data.username this.setLoggedInState(true); const permissions = JSON.stringify(data.user.permissions) localStorage.setItem('permissions', permissions) localStorage.setItem('token', data.token) // localStorage.setItem('username', this.username) } else { throw new Error('Credenciales inválidas') } }, clearUsers() { this.users = [] }, setLoggedInState(isLoggedIn: boolean) { this.isLoggedIn = isLoggedIn; localStorage.setItem('isLoggedIn', JSON.stringify(isLoggedIn)); }, initLoggedInState() { const loggedInState = localStorage.getItem('isLoggedIn'); const username = localStorage.getItem('username') if (loggedInState) { this.isLoggedIn = JSON.parse(loggedInState); } else { this.isLoggedIn = false } if (username) { this.username = username } }, initEmployee() { const user = localStorage.getItem('user'); if (user) { this.user = JSON.parse(user); } }, clearEmployee() { this.user = {} as User localStorage.removeItem('user') }, initAccount() { const account = localStorage.getItem('account'); if (account) { this.account = JSON.parse(account); } }, clearAccountOperations() { this.accountOperations = [] }, fetchAccountPermissions() { this.accountPermissions = parsePermissions('account') }, fetchUserPermissions() { this.userPermissions = parsePermissions('employee') } } }) if (import.meta.hot) { import.meta.hot.accept(acceptHMRUpdate(userStore, import.meta.hot)) } </code>
First component

<template>
    <v-container>
        <v-row class="justify-start pt-4 px-4">
            <v-col class="d-flex align-center mb-5" cols="9">
                <v-icon class="mr-2" color="#C9C6FF">mdi-filter-variant</v-icon>
                <p style="color: #C9C6FF; font-size: 1.25rem; margin-right: 1rem">Filtros</p>


                <v-btn v-for="sf in statusFilters" v-if="hasQuickFilters" :key="sf.value"
                       :class="{ 'selected-icon': sf.isSelected }" :size="isNotMobile? 'medium': 'x-small'"
                       :value="sf.value" :variant="sf.isSelected ? 'flat' : 'outlined'" class="mx-2 filter-btn"
                       rounded="lg" @click="setFilter(sf)">{{ sf.label }}
                </v-btn>
                <v-btn v-if="hasQuickFilters || hasFilters" :size="isNotMobile? 'x-large': 'small'" class="ml-n3"
                       variant="text" @click="clearFilters">
                    <v-icon size="large">mdi-backspace-outline</v-icon>
                </v-btn>

                <v-btn v-if="!showSearchbar" :size="isNotMobile? 'x-large': 'small'" class="ml-n3"
                       transition="scroll-x-transition" variant="text"
                       @click="showSearchbar = !showSearchbar">
                    <v-icon>mdi-magnify</v-icon>
                </v-btn>
                <v-slide-x-transition>
                    <v-text-field
                        v-if="showSearchbar"
                        v-model="search"
                        density="compact"
                        fluid
                        hide-details
                        label="Buscar"
                        prepend-inner-icon="mdi-magnify"
                        rounded="lg"
                        single-line
                        transition="scroll-x-transition"
                        variant="outlined"
                        @input="searchFilter({value: search, key: 'search', icon: 'mdi-magnify', isSelected: false})"
                    >
                        <template v-slot:append-inner>
                            <v-icon @click="showSearchbar = !showSearchbar">mdi-close</v-icon>
                        </template>
                    </v-text-field>
                </v-slide-x-transition>

            </v-col>
            <!--      <v-col cols="3" class="justify-center">-->
            <!--        <v-slide-x-transition>-->
            <!--          <v-text-field-->
            <!--              v-if="showSearchbar"-->
            <!--              v-model="search"-->
            <!--              density="compact"-->
            <!--              hide-details-->
            <!--              label="Buscar"-->
            <!--              prepend-inner-icon="mdi-magnify"-->
            <!--              rounded="lg"-->
            <!--              single-line-->
            <!--              transition="scroll-x-transition"-->
            <!--              variant="outlined"-->
            <!--              @input="searchFilter({value: search, key: 'search', icon: 'mdi-magnify', isSelected: false})"-->
            <!--          >-->
            <!--            <template v-slot:append-inner>-->
            <!--              <v-icon @click="showSearchbar = !showSearchbar">mdi-close</v-icon>-->
            <!--            </template>-->
            <!--          </v-text-field>-->
            <!--        </v-slide-x-transition>-->
            <!--      </v-col>-->
        </v-row>
        <v-row class="mt-0">
            <v-col :cols="tableCols">
                <v-data-table-server
                    :headers="headers"
                    :items="items"
                    :items-length="itemsLength"
                    :items-per-page="pageSize"
                    :items-per-page-options="[10, 25, 50, 100]"
                    :loading="loader"
                    :mobile="false"
                    :show-current-page="true"
                    :style="{ height: `calc(100vh - 234px)`, backgroundColor: '#1B1B1B', color: 'white'}"
                    disable-sort
                    first-icon="mdi-user"
                    fixed-footer
                    items-per-page-text=""
                    no-data-text="No hay datos disponibles"
                    search=""
                    @update:options="loadItems">
                    <template v-slot:item.is_active="{ item }: { item: ListItem }">
                        <v-chip v-if="item.is_active" color="green">Si</v-chip>
                        <v-chip v-else color="red">No</v-chip>
                    </template>
                    <template v-slot:item.status="{ item }: { item: ListItem }">
                        <v-chip v-if="item.status" color="green">En uso</v-chip>
                        <v-chip v-else color="red">Inactivo</v-chip>
                    </template>
                    <template v-slot:item.firstCol="{ item }: { item: ListItem }">
                        <v-btn color="#6C6CF1" style="min-width: 100px;" variant="outlined" @click="goToEdit(item)">{{
                                item[keyName]
                            }}
                        </v-btn>
                    </template>
                    <template v-slot:item.permissions="{ item }: { item: ListItem }">
                        <v-list class="d-flex">
                            <v-list-item
                                v-for="n in item.permissions"
                                :key="n"
                                :title="n"
                            ></v-list-item>
                        </v-list>
                    </template>
                    <template v-slot:item.created="{ item }: { item: ListItem }">
                        {{ parseTimeUTCVE(item.created) }}
                    </template>
                    <template v-slot:item.access_point="{ item }: { item: ListItem }">
                        {{ item.access_point }} - {{ parseTimeUTCVE(item.entry_time) }}
                    </template>
                    <template v-slot:item.exit_point="{ item }: { item: ListItem }">
                        {{ item.exit_point }} - {{ parseTimeUTCVE(item.exit_time) }}
                    </template>
                    <template v-slot:item.cash_balance="{ item }: { item: ListItem }">
                        USD {{ item.cash_balance["USD"] }} - BS {{ item.cash_balance.BS }}
                    </template>
                </v-data-table-server>
            </v-col>
            <v-slide-x-transition>
                <v-col v-if="showFilters" :cols="filterCols"
                       :style="{ height: `calc(100vh - 168px)`, overflowY: 'auto', borderLeft: '1px rgb(128,128,128) solid' }">
                    <h3>Filtros</h3>
                    <v-row>
                        <v-col>
                            <slot/>
                        </v-col>
                    </v-row>

                </v-col>
            </v-slide-x-transition>
        </v-row>

    </v-container>
</template>

<script lang="ts">

import type {Filter, ListItem} from "@/types";
import {debounce} from "lodash";
import {parseTimeUTCVE} from "@/utils";
import {useDisplay} from "vuetify";

export default {
    props: {
        headers: {
            type: Array<object>,
            required: true
        },
        items: {
            type: Array<ListItem>,
            required: true
        },
        pageSize: {
            type: Number,
            required: true
        },
        loader: {
            type: Boolean,
            required: true
        },
        hasFilters: {
            type: Boolean,
            required: true
        },
        hasQuickFilters: {
            type: Boolean,
            default: true
        },
        keyName: {
            type: String,
            required: true
        },
        editKey: {
            type: String,
            required: true
        },
        itemsLength: {
            type: Number,
            required: true
        },
        statusFilters: {
            type: Array<Filter>,
            required: true
        },
        filterSelected: {
            type: Boolean,
            required: false
        },
    },
    emits: ["submit", "create", "update-table", "set-filter", "clear-filters"],
    data() {
        return {
            fab: false,
            selectedStatusFilter: false,
            showFilters: false,
            areFiltersApplied: false,
            search: '',
            tableCols: 12,
            filterCols: 0,
            showSearchbar: false,
            filterIcon: 'mdi-filter-menu'

        }
    },
    computed: {
        isNotMobile() {
            const {mdAndUp} = useDisplay()
            return mdAndUp.value
        }
    },
    methods: {
        parseTimeUTCVE,
        goToEdit(item: object) {
            this.$emit('submit', item)
        },
        goToCreate() {
            this.$emit('create')
        },
        loadItems(options: any) {
            const page = options.page
            const itemsPerPage = options.itemsPerPage
            const sortBy = options.sortBy
            this.$emit('update-table', {page, itemsPerPage, sortBy});

        },
        setFilter(filter: Filter) {
            filter.isSelected = !filter.isSelected
            this.statusFilters.forEach((sf) => {
                if (sf !== filter) {
                    sf.isSelected = false;
                }
            });
            this.$emit('set-filter', filter)
        },
        searchFilter: debounce(function (this: any, filter: Filter) {
            filter.isSelected = !filter.isSelected
            this.$emit('set-filter', filter)
        }, 700),
        toggleFilters() {
            this.showFilters = !this.showFilters
            this.filterCols = this.showFilters ? 4 : 0
            this.tableCols = this.showFilters ? 8 : 12
            this.filterIcon = this.showFilters ? 'mdi-filter-remove' : 'mdi-filter-menu'

        },
        clearFilters() {
            this.$emit('clear-filters');
        }
    },
}
</script>

</style>

Second component

<template>
    <v-row>
        <v-col class="d-flex justify-end" cols="12">
            <v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 100px;" variant="outlined">
                <v-icon class="mr-3">mdi-delete</v-icon>
                Eliminar
            </v-btn>
            <v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 100px;" variant="outlined"
                   @click="goToCreate()">
                <v-icon class="mr-3">mdi-plus-circle-outline</v-icon>
                Crear
            </v-btn>
            <v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 120px;" variant="outlined">
                <v-icon>mdi-folder-plus</v-icon>
                Carga Masiva
            </v-btn>
            <v-btn class="actions-buttons" color="#6C6CF1" style="min-width: 120px;" variant="outlined">
                <v-icon class="mr-2">mdi-download</v-icon>
                Descargar
            </v-btn>
        </v-col>
    </v-row>
    <v-card :style="{ height: `calc(100vh - 100px)`, overflowY: 'auto' }" class="my-4" color="#1B1B1B"
            elevation="1" rounded="lg">
        <InnerTable
            :edit-key="editKey"
            :filter-selected="filterSelected"
            :has-filters="hasFilters"
            :has-quick-filters="hasQuickFilters"
            :headers="headers"
            :items="items"
            :items-length="itemsLength"
            :key-name="keyName"
            :loader="loader"
            :page-size="pageSize"
            :status-filters="statusFilters"
            @create="goToCreate"
            @submit="goToEdit"
            @update-table="updateTable"
            @set-filter="setFilter"
            @clear-filters="clearFilters"
        >
            <template v-slot:default>
                <slot></slot>
            </template>
        </InnerTable>
    </v-card>
</template>

<script lang="ts">
import InnerTable from './InnerTable.vue';
import type {Filter, ListItem} from "@/types";
import {useDisplay} from "vuetify";

export default {
    name: 'ListTable',
    components: {
        InnerTable
    },
    props: {
        headers: {
            type: Array<object>,
            required: true
        },
        items: {
            type: Array<ListItem>,
            required: true
        },
        disabledFab: {
            type: Boolean,
            required: true
        },
        pageSize: {
            type: Number,
            required: true
        },
        loader: {
            type: Boolean,
            required: true
        },
        hasFilters: {
            type: Boolean,
            required: true
        },
        hasQuickFilters: {
            type: Boolean,
            default: true
        },
        keyName: {
            type: String,
            required: true
        },
        editKey: {
            type: String,
            required: true
        },
        itemsLength: {
            type: Number,
            required: true
        },
        statusFilters: {
            type: Array<Filter>,
            required: true
        },
        filterSelected: {
            type: Boolean,
            required: false
        },
    },
    emits: ["submit", "create", "update-table", "set-filter", "clear-filters"],
    data() {
        return {
            fab: false,
            selectedStatusFilter: false,
            showFilters: false,
            areFiltersApplied: false,
            search: '',
            tableCols: 12,
            filterCols: 0,
            showSearchbar: false,
            tableHeight: '',
            filterIcon: 'mdi-filter-menu',
            hideFooter: false,

        }
    },
    computed: {
        isNotMobile() {
            const {mdAndUp} = useDisplay()
            return mdAndUp.value
        }
    },
    methods: {
        goToEdit(item: object) {
            this.$emit('submit', item)
        },
        goToCreate() {
            this.$emit('create')
        },
        updateTable(options: any) {
            this.$emit('update-table', options);
        },
        setFilter(filter: Filter) {
            this.$emit('set-filter', filter)
        },
        clearFilters() {
            this.$emit('clear-filters');
        }
    },
}
</script>

<style scoped>

@media only screen and (min-width: 760px) {
    ::v-deep .v-data-table-footer {
        align-items: center;
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        padding: 8px 4px;
    }
}

.actions-buttons {
    width: 8vw;
    text-transform: capitalize;
    margin-left: 5px;
    margin-right: 5px;
    border-radius: 10px;
}


</style>


Third component
<template>
    <list-table
        :current-page="page"
        :disabled-fab="showFab" :filter-selected="filterSelected"
        :has-filters="true"
        :headers="getColumns" :items="users"
        :items-length="usersCount"
        :loader="loader"
        :page-size="pageSize"
        :status-filters="statusFilters"
        edit-key="id"
        key-name="dni"
        @create="goToCreate()"
        @submit="goToEdit($event)"
        @update-table="updateTable($event)"
        @set-filter="setFilter"
        @clear-filters="clearFilter"
    >
        <v-select v-model="company" :items="companies"
                  clearable density="compact"
                  item-title="name" item-value="name" label="Empresa" rounded="xl" variant="outlined"/>
    </list-table>
</template>

<script lang="ts">
import ListTable from '@/components/ListTable.vue'
import {userStore} from '@/stores/user'
import type {baseObject, BreadcrumbsArray, Filter, PaginationProps, User} from "@/types";
import {miscStore} from "@/stores/runtime";
import {useDisplay} from "vuetify";
import {companyStore} from "@/stores/company";
import {errorHandling} from "@/utils/error";
import type {AxiosError} from "axios";

export default {
    name: 'Employees',
    components: {ListTable},
    data() {
        return {
            miscStore: miscStore(),
            userStore: userStore(),
            companyStore: companyStore(),
            loader: true,
            showFab: true,
            page: 1,
            pageSize: 10,
            state: null,
            params: {},
            isRequesting: false,
            parking: null,
            company: null,
            filterSelected: false,
            clearingFilter: false,
            columns: [
                {
                    title: 'DNI',
                    align: 'start',
                    key: 'firstCol'
                },
                {
                    title: 'Empresa',
                    align: 'start',
                    key: 'company'
                },
                {
                    title: 'Usuario',
                    align: 'start',
                    key: 'username'
                },
                {
                    title: 'Nombre',
                    align: 'start',
                    key: 'full_name'
                },
                {
                    title: 'Roles',
                    align: 'start',
                    key: 'roles'
                },
                {
                    title: 'Activo',
                    align: 'start',
                    key: 'is_active'
                },
            ],
            statusFilters: [
                {key: 'is_active', value: true, label: 'Activo', isSelected: false},
                {key: 'is_active', value: false, label: 'Inactivo', isSelected: false},
            ],
        }
    },
    async created() {
        await this.companyStore.fetchCompanies(1, {}, 100)
        this.userStore.fetchUserPermissions()
        if (this.userPermissions.create === false) {
            this.showFab = false
        }

    },
    computed: {
        users() {
            return this.userStore.getUsers
        },
        companies() {
            return this.companyStore.getCompanies
        },
        userPermissions() {
            return this.userStore.getUserPermissions
        },
        isNotMobile() {
            const {mdAndUp} = useDisplay()
            return mdAndUp.value
        },
        getColumns() {
            const columnsMobile = ['Cédula', 'Nombre de usuario', 'Activo']
            if (this.isNotMobile) {
                return this.columns
            } else {
                return this.columns.filter(column => columnsMobile.includes(column.title))
            }
        },
        usersCount() {
            return this.userStore.getUsersCount
        },
        breadcrumbs: {
            get() {
                return this.miscStore.getBreadcrumbs;
            },
            set(newBreadcrumbs: BreadcrumbsArray) {
                // Perform any validation or logic before setting
                this.miscStore.setBreadcrumbs(newBreadcrumbs);
            }
        }
    },
    methods: {
        async goToEdit(user: User) {
            this.loader = true
            this.breadcrumbs.push({title: user.username, to: '/update-employee'})
            this.miscStore.setBreadcrumbs(this.breadcrumbs)
            await this.userStore.fetchEmployeeById(user.id)
            this.$router.push('/update-employee')
        },
        goToCreate() {
            this.breadcrumbs.push({title: "Creación de empleado", to: '/create-employee'})
            this.miscStore.setBreadcrumbs(this.breadcrumbs)
            this.$router.push('/create-employee')
        },
        async fetchEmployees(page: number, query_params: object, pageSize: number) {
            this.loader = true
            await this.userStore.fetchEmployees(page, query_params, pageSize).catch((error: AxiosError) => {
                errorHandling(error)
            })
            this.loader = false
        },
        async updateTable(tableProps: PaginationProps) {
            await this.fetchEmployees(tableProps.page, this.params, tableProps.itemsPerPage)
        },
        async setFilter(filter: Filter) {
            if (filter.value === (this.params as baseObject)[filter.key]) {
                delete (this.params as baseObject)[filter.key];
            } else {
                (this.params as baseObject)[filter.key] = filter.value;
            }
            await this.fetchEmployees(1, this.params, this.pageSize);
        },
        async clearFilter() {
            this.clearingFilter = true
            this.params = {}
            this.statusFilters = this.statusFilters.map(item => ({...item, isSelected: false}));
            this.clearingFilter = false
            await this.fetchEmployees(1, this.params, this.pageSize);
        },
    },
    unmounted() {
        this.userStore.clearUsers()
    },
}
</script>

<style scoped>

</style>


Store
import {acceptHMRUpdate, defineStore} from 'pinia'
import {AxiosDelete, AxiosGet, AxiosLogin, AxiosPatch, AxiosPost, AxiosPut, ServiceUrls} from '@/plugins/axios'
import type {ListItem, LoginResponse, Nullable, PermissionsBasicInfo, User} from '@/types'
import {parsePermissions} from "@/utils/parsePermissions";

export const userStore = defineStore('user', {
    state: () => ({
        firstName: '',
        middleName: '' as Nullable<string>,
        lastName: '',
        users: [] as Array<ListItem>,
        roles: [] as Array<ListItem>,
        accountOperations: [] as Array<ListItem>,
        user: {} as User,
        account: {} as User,
        usersCount: 0 as number,
        operationsCount: 0 as number,
        isLoggedIn: false,
        code: 0,
        username: '' as string,
        accountPermissions: {} as PermissionsBasicInfo,
        userPermissions: {} as PermissionsBasicInfo,
        employeesDropdown: [] as Array<ListItem>,


    }),

    getters: {
        getFirstName: (state) => state.firstName,
        getIsLoggedIn: (state) => state.isLoggedIn,
        getUsers: (state) => state.users,
        getUser: (state) => state.user,
        getUsername: (state) => state.username,
        getUsersCount: (state) => state.usersCount,
        getRoles: (state) => state.roles,
        getAccount: (state) => state.account,
        getAccountPermissions: (state) => state.accountPermissions,
        getUserPermissions: (state) => state.userPermissions,
        getEmployeesDropdown: (state) => state.employeesDropdown

    },

    actions: {
        loginUser(username: string, password: string) {
            return AxiosLogin(ServiceUrls.LOGIN, {username, password})
        },

        async logoutUser() {
            let url = ServiceUrls.USERS + 'logout/'
            this.setLoggedInState(false);
            localStorage.removeItem('permissions')
            localStorage.removeItem('token')
            await AxiosDelete(url)
        },

        async fetchEmployees(page: number, baseParams: object, pageSize: number) {
            this.usersCount = 0
            let url = ServiceUrls.EMPLOYEE
            const params = {
                page,
                page_size: pageSize,
                ...baseParams
            }

            let response = await AxiosGet(url, params)
            this.users = response.results
            this.usersCount = response.count
        },

        async fetchDropDownEmployees() {
            let url = ServiceUrls.DROPDOWN_EMPLOYEES
            this.employeesDropdown = await AxiosGet(url)
        },


        async fetchAccounts(page: number, baseParams: object, pageSize: number) {
            this.usersCount = 0
            let url = ServiceUrls.ACCOUNTS
            const params = {
                page,
                page_size: pageSize,
                ...baseParams
            }
            const response = await AxiosGet(url, params)
            this.users = response.results
            this.usersCount = response.count
        },
        async fetchEmployeeById(id: string) {
            let url = ServiceUrls.EMPLOYEE + id + '/'
            this.user = await AxiosGet(url)
            localStorage.setItem('user', JSON.stringify(this.user));
        },
        async fetchAccountByUsername(username: string) {
            let url = ServiceUrls.ACCOUNTS + username + '/'
            this.account = await AxiosGet(url)
            localStorage.setItem('account', JSON.stringify(this.account));

        },
        async fetchAccountOperations(page: number, username: string, filters: object, pageSize: number) {
            let url = ServiceUrls.USERS + username + '/operations/'
            this.usersCount = 0
            const params = {
                page,
                page_size: pageSize,
                ...filters
            }
            const response = await AxiosGet(url, params)
            this.accountOperations = response.results
            this.operationsCount = response.count

        },
        async recharge(data: object, username: string) {
            let url = ServiceUrls.RECHARGE
            let baseUrl = url.replace(':username', username)
            return await AxiosPost(baseUrl, data)
        },
        async createEmployee(data: object) {
            let url = ServiceUrls.EMPLOYEE
            return await AxiosPost(url, data)
        },
        async createAccount(data: object) {
            let url = ServiceUrls.ACCOUNTS
            return await AxiosPost(url, data)
        },
        async updateUser(data: object, id: string) {
            let url = ServiceUrls.EMPLOYEE + id + '/'
            return await AxiosPatch(url, data)
        },
        async updateEmployee(data: object, username: string) {
            let url = ServiceUrls.EMPLOYEE + username + '/'
            return await AxiosPut(url, data)
        },
        async patchUser(data: object, username: string) {
            let url = ServiceUrls.USERS + username + '/'
            return await AxiosPatch(url, data)
        },
        async deleteUser(username: string) {
            let url = ServiceUrls.EMPLOYEE + username + '/'
            return await AxiosDelete(url)
        },
        setUserData(data: LoginResponse) {
            if (data) {
                // this.username = data.username
                this.setLoggedInState(true);
                const permissions = JSON.stringify(data.user.permissions)
                localStorage.setItem('permissions', permissions)
                localStorage.setItem('token', data.token)
                // localStorage.setItem('username', this.username)
            } else {
                throw new Error('Credenciales inválidas')
            }
        },
        clearUsers() {
            this.users = []
        },
        setLoggedInState(isLoggedIn: boolean) {
            this.isLoggedIn = isLoggedIn;
            localStorage.setItem('isLoggedIn', JSON.stringify(isLoggedIn));
        },
        initLoggedInState() {
            const loggedInState = localStorage.getItem('isLoggedIn');
            const username = localStorage.getItem('username')
            if (loggedInState) {
                this.isLoggedIn = JSON.parse(loggedInState);
            } else {
                this.isLoggedIn = false
            }
            if (username) {
                this.username = username
            }
        },
        initEmployee() {
            const user = localStorage.getItem('user');
            if (user) {
                this.user = JSON.parse(user);
            }
        },
        clearEmployee() {
            this.user = {} as User
            localStorage.removeItem('user')
        },
        initAccount() {
            const account = localStorage.getItem('account');
            if (account) {
                this.account = JSON.parse(account);
            }
        },
        clearAccountOperations() {
            this.accountOperations = []
        },
        fetchAccountPermissions() {
            this.accountPermissions = parsePermissions('account')
        },
        fetchUserPermissions() {
            this.userPermissions = parsePermissions('employee')
        }
    }
})

if (import.meta.hot) {
    import.meta.hot.accept(acceptHMRUpdate(userStore, import.meta.hot))
}


New contributor

Juan Eduardo Escobar is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

1

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật