666 lines
19 KiB
Vue
666 lines
19 KiB
Vue
<template>
|
|
<v-container class="tab">
|
|
<div class="image-wrapper">
|
|
<img src="/image_b.png" alt="Ilustração acima da tabela" class="top-image" />
|
|
</div>
|
|
<v-card>
|
|
<v-toolbar class="info" flat>
|
|
<v-toolbar-title class="title"> Colaboradores</v-toolbar-title>
|
|
<v-spacer></v-spacer>
|
|
|
|
<!-- Barra de Pesquisa com Ícone -->
|
|
<v-text-field
|
|
v-model="filters.user"
|
|
:loading="loading.users"
|
|
prepend-inner-icon="mdi-magnify"
|
|
density="compact"
|
|
variant="outlined"
|
|
label="Pesquisar usuário"
|
|
hide-details
|
|
class="mx-2"
|
|
style="max-width: 250px;"
|
|
@update:model-value="filterUsers"
|
|
clearable
|
|
/>
|
|
<!-- Filtro de Status -->
|
|
<v-select
|
|
v-model="filters.status"
|
|
:items="statusOptions"
|
|
label="Status"
|
|
density="compact"
|
|
variant="outlined"
|
|
hide-details
|
|
class="mx-2"
|
|
style="max-width: 150px;"
|
|
@update:model-value="filterUsers"
|
|
clearable
|
|
/>
|
|
<!-- Botão para exportar CSV -->
|
|
<v-btn
|
|
color="primary"
|
|
class="ml-2"
|
|
prepend-icon="mdi-file-export"
|
|
:loading="loading.export"
|
|
variant="tonal"
|
|
@click="exportToCSV"
|
|
>
|
|
Exportar
|
|
</v-btn>
|
|
|
|
<!-- Botão para adicionar usuário -->
|
|
<v-btn
|
|
color="primary"
|
|
class="ml-2"
|
|
prepend-icon="mdi-plus"
|
|
variant="elevated"
|
|
@click="openDialog('user', 'create')"
|
|
>
|
|
Adicionar Usuário
|
|
</v-btn>
|
|
</v-toolbar>
|
|
|
|
<!-- Tabela de Dados -->
|
|
<v-data-table
|
|
class="dados"
|
|
:headers="headers.users"
|
|
:items="filteredUsers"
|
|
:search="filters.user"
|
|
:loading="loading.users"
|
|
:items-per-page="itemsPerPage"
|
|
:page.sync="page"
|
|
>
|
|
<!-- Coluna de Status -->
|
|
<template v-slot:item.status="{ item }">
|
|
<!-- Botão para alternar status -->
|
|
|
|
<v-icon
|
|
size="small"
|
|
class="mr-2"
|
|
:color="item.status ? 'error' : 'success'"
|
|
@click="toggleUserStatus(item)"
|
|
:title="item.status ? 'Desativar usuário' : 'Ativar usuário'"
|
|
>
|
|
{{ item.status ? 'mdi-account-cancel' : 'mdi-account-check' }}
|
|
</v-icon>
|
|
<!--
|
|
<v-chip
|
|
:color="item.status ? 'success' : 'error'"
|
|
text-color="white"
|
|
size="small"
|
|
>
|
|
{{ item.status ? 'Ativo' : 'Inativo' }}
|
|
</v-chip>-->
|
|
</template>
|
|
|
|
<template v-slot:item.actions="{ item }">
|
|
<v-icon
|
|
size="small"
|
|
class="mr-2"
|
|
@click="openEditPage('user', item)"
|
|
>
|
|
mdi-pencil
|
|
</v-icon>
|
|
<v-icon
|
|
size="small"
|
|
@click="confirmDelete('user', item)"
|
|
>
|
|
mdi-delete
|
|
</v-icon>
|
|
</template>
|
|
</v-data-table>
|
|
</v-card>
|
|
|
|
<!-- Diálogo de Usuário (Novo/Edição) -->
|
|
<UserModal
|
|
v-model:isModalOpen="dialogs.user"
|
|
:isEditMode="isEditing"
|
|
:modalUser="forms.user"
|
|
@save="submitForm"
|
|
/>
|
|
|
|
<!-- Diálogo de Confirmação de Exclusão -->
|
|
<v-dialog v-model="dialogs.delete" max-width="400px">
|
|
<v-card>
|
|
<v-card-title class="text-h5 bg-error text-white">
|
|
Confirmar Exclusão
|
|
</v-card-title>
|
|
|
|
<v-card-text class="pt-4">
|
|
<p v-if="itemToDelete && itemToDelete.username">
|
|
Tem certeza que deseja excluir <strong>{{ itemToDelete.username }}</strong>?
|
|
Esta ação não pode ser desfeita.
|
|
</p>
|
|
<p v-else>
|
|
Erro: Nenhum item selecionado para exclusão.
|
|
</p>
|
|
</v-card-text>
|
|
|
|
<v-card-actions>
|
|
<v-spacer></v-spacer>
|
|
<v-btn color="grey" @click="dialogs.delete = false">Cancelar</v-btn>
|
|
<v-btn
|
|
color="error"
|
|
:loading="loading.delete"
|
|
:disabled="!itemToDelete || !itemToDelete.id"
|
|
@click="deleteItem"
|
|
>
|
|
Excluir
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<!-- Diálogo de Confirmação de Alteração de Status -->
|
|
<v-dialog v-model="dialogs.status" max-width="400px">
|
|
<v-card>
|
|
<v-card-title class="text-h5" :class="statusToChange ? 'bg-error' : 'bg-success'" style="color: white;">
|
|
{{ statusToChange ? 'Desativar Usuário' : 'Ativar Usuário' }}
|
|
</v-card-title>
|
|
|
|
<v-card-text class="pt-4">
|
|
<p v-if="itemToChangeStatus && itemToChangeStatus.username">
|
|
Tem certeza que deseja {{ statusToChange ? 'desativar' : 'ativar' }} o usuário
|
|
<strong>{{ itemToChangeStatus.username }}</strong>?
|
|
</p>
|
|
<p v-else>
|
|
Erro: Nenhum usuário selecionado.
|
|
</p>
|
|
</v-card-text>
|
|
|
|
<v-card-actions>
|
|
<v-spacer></v-spacer>
|
|
<v-btn color="grey" @click="dialogs.status = false">Cancelar</v-btn>
|
|
<v-btn
|
|
:color="statusToChange ? 'error' : 'success'"
|
|
:loading="loading.status"
|
|
:disabled="!itemToChangeStatus || !itemToChangeStatus.id"
|
|
@click="changeUserStatus"
|
|
>
|
|
{{ statusToChange ? 'Desativar' : 'Ativar' }}
|
|
</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<!-- Snackbar para Feedback -->
|
|
<v-snackbar
|
|
v-model="snackbar.show"
|
|
:color="snackbar.color"
|
|
:timeout="3000"
|
|
>
|
|
{{ snackbar.text }}
|
|
<template v-slot:actions>
|
|
<v-btn
|
|
color="white"
|
|
variant="text"
|
|
@click="snackbar.show = false"
|
|
>
|
|
Fechar
|
|
</v-btn>
|
|
</template>
|
|
</v-snackbar>
|
|
</v-container>
|
|
</template>
|
|
|
|
<script>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import UserModal from '../components/modals/UserModal.vue';
|
|
import { useUserStore } from '../stores/users';
|
|
|
|
export default {
|
|
name: 'UserManagement',
|
|
components: {
|
|
UserModal
|
|
},
|
|
setup() {
|
|
const router = useRouter();
|
|
|
|
// Estados
|
|
const isEditing = ref(false);
|
|
const itemToEdit = ref(null);
|
|
const itemToDelete = ref(null);
|
|
const page = ref(1);
|
|
const itemsPerPage = ref(10);
|
|
const selectedUser = ref(null);
|
|
const itemToChangeStatus = ref(null);
|
|
const statusToChange = ref(null);
|
|
|
|
// Status options para filtro
|
|
const statusOptions = [
|
|
{ title: 'Ativo', value: true },
|
|
{ title: 'Inativo', value: false }
|
|
];
|
|
|
|
// Loading states
|
|
const loading = ref({
|
|
users: false,
|
|
submit: false,
|
|
delete: false,
|
|
export: false,
|
|
status: false
|
|
});
|
|
|
|
// Diálogos
|
|
const dialogs = ref({
|
|
user: false,
|
|
delete: false,
|
|
status: false
|
|
});
|
|
|
|
// Filtros
|
|
const filters = ref({
|
|
user: '',
|
|
status: null
|
|
});
|
|
|
|
// Dados - array vazio que será preenchido via API ou entrada do usuário
|
|
const users = ref([]);
|
|
|
|
// Formulários
|
|
const forms = ref({
|
|
userValid: false,
|
|
user: {
|
|
id: null,
|
|
username: '',
|
|
email: '',
|
|
birth_date: '',
|
|
phone: '',
|
|
profile_image: '',
|
|
status: true,
|
|
//contrato: '',
|
|
// grupo: '',
|
|
//permissao: ''
|
|
}
|
|
});
|
|
|
|
// Headers para as tabelas
|
|
const headers = ref({
|
|
users: [
|
|
{ title: 'ID', key: 'id' },
|
|
{ title: 'Usuário', key: 'username' },
|
|
{ title: 'Empresa', key: 'empresa' },
|
|
{ title: 'Cargo', key: 'cargo' },
|
|
{ title: 'Setor', key: 'setor' },
|
|
{ title: 'Escala', key: 'escala' },
|
|
{ title: 'Feriado', key: 'feriado' },
|
|
{ title: 'Status', key: 'status', align: 'center' },
|
|
//{ title: 'Contrato', key: 'contrato' },
|
|
//{ title: 'Grupo', key: 'grupo' },
|
|
//{ title: 'Permissão', key: 'permissao' },
|
|
{ title: 'Ações', key: 'actions', sortable: false, align: 'end' }
|
|
]
|
|
});
|
|
|
|
// Snackbar
|
|
const snackbar = ref({
|
|
show: false,
|
|
text: '',
|
|
color: 'success'
|
|
});
|
|
|
|
// Computed properties
|
|
const filteredUsers = computed(() => {
|
|
const filtered = users.value.filter(user => !user.deleted);
|
|
console.log("Usuários após filtro 'deleted':", filtered);
|
|
|
|
return filtered.filter(user => {
|
|
// Filtro por texto (username ou email)
|
|
const textMatch = user.username.toLowerCase().includes(filters.value.user.toLowerCase()) ||
|
|
user.email.toLowerCase().includes(filters.value.user.toLowerCase());
|
|
|
|
// Filtro por status (se selecionado)
|
|
const statusMatch = filters.value.status === null || user.status === filters.value.status;
|
|
|
|
return textMatch && statusMatch;
|
|
}).map(user => ({
|
|
...user,
|
|
created_at: user.created_at ? new Date(user.created_at).toLocaleString() : '-'
|
|
}));
|
|
});
|
|
|
|
// Função para alternar status do usuário
|
|
const toggleUserStatus = (item) => {
|
|
itemToChangeStatus.value = {...item};
|
|
statusToChange.value = item.status; // Se true, vai desativar; se false, vai ativar
|
|
dialogs.value.status = true;
|
|
};
|
|
|
|
// Função para confirmar a alteração de status
|
|
const changeUserStatus = async () => {
|
|
if (!itemToChangeStatus.value || itemToChangeStatus.value.id === undefined) {
|
|
showNotification('Erro ao alterar status do usuário', 'error');
|
|
|
|
return;
|
|
}
|
|
console.log("Alterando status do usuário:", itemToChangeStatus.value);
|
|
loading.value.status = true;
|
|
try {
|
|
const userId = itemToChangeStatus.value.id;
|
|
const newStatusAt = statusToChange.value ? 'inativo' : 'ativo';
|
|
const updatedUser = {
|
|
...itemToChangeStatus.value,
|
|
status: newStatusAt
|
|
};
|
|
|
|
await userStore.updateUser(userId, updatedUser);
|
|
|
|
const index = users.value.findIndex(user => user.id === userId);
|
|
if (index !== -1) {
|
|
users.value[index].status = newStatusAt === 'ativo';
|
|
users.value[index].status = newStatusAt;
|
|
}
|
|
|
|
showNotification(`Usuário ${newStatusAt === 'ativo' ? 'ativado' : 'desativado'} com sucesso!`);
|
|
dialogs.value.status = false;
|
|
} catch (error) {
|
|
showNotification('Erro ao alterar status do usuário', 'error');
|
|
console.error('Error updating user status:', error);
|
|
} finally {
|
|
loading.value.status = false;
|
|
itemToChangeStatus.value = null;
|
|
}
|
|
};
|
|
|
|
// Função para exportar para CSV
|
|
const exportToCSV = () => {
|
|
if (filteredUsers.value.length === 0) {
|
|
showNotification('Não há dados para exportar', 'warning');
|
|
return;
|
|
}
|
|
|
|
loading.value.export = true;
|
|
try {
|
|
// Preparar dados: filtrar apenas os campos que queremos exportar
|
|
const dataToExport = filteredUsers.value.map(user => ({
|
|
ID: user.id,
|
|
Nome: user.username,
|
|
Email: user.email,
|
|
Birth: user.birth_date,
|
|
Phone: user.phone,
|
|
Image: user.profile_image,
|
|
//Contrato: user.contrato,
|
|
//Grupo: user.grupo,
|
|
//Permissao: user.permissao
|
|
}));
|
|
|
|
// Gerar cabeçalho do CSV
|
|
const headers = Object.keys(dataToExport[0]);
|
|
|
|
// Converter dados para linhas CSV
|
|
const csvContent = [
|
|
headers.join(','), // Cabeçalho
|
|
...dataToExport.map(row =>
|
|
headers.map(field => {
|
|
// Escapar strings que contêm vírgulas com aspas
|
|
const value = String(row[field]).replace(/"/g, '""');
|
|
return /[,"]/.test(value) ? `"${value}"` : value;
|
|
}).join(',')
|
|
)
|
|
].join('\n');
|
|
|
|
// Criar blob e link para download
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', 'usuarios_export.csv');
|
|
link.style.visibility = 'hidden';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
showNotification('Dados exportados com sucesso!');
|
|
} catch (error) {
|
|
showNotification('Erro ao exportar dados', 'error');
|
|
console.error('Error exporting data:', error);
|
|
} finally {
|
|
loading.value.export = false;
|
|
}
|
|
};
|
|
|
|
// Métodos
|
|
const openDialog = (type, action, item = null) => {
|
|
isEditing.value = action === 'edit';
|
|
if (item) {
|
|
// Ajustando para o formato esperado pelo UserModal
|
|
forms.value[type] = {
|
|
id: item.id,
|
|
username: item.username,
|
|
email: item.email,
|
|
birth_date: item.birth_date,
|
|
phone: item.phone,
|
|
profile_image: item.profile_image,
|
|
status: item.status,
|
|
//contrato: item.contrato,
|
|
//grupo: item.grupo,
|
|
//permissao: item.permissao
|
|
};
|
|
} else {
|
|
// Reset form
|
|
forms.value[type] = {
|
|
id: null,
|
|
username: '',
|
|
email: '',
|
|
birth_date: '',
|
|
phone: '',
|
|
profile_image: '',
|
|
status: true,
|
|
//contrato: '',
|
|
//grupo: '',
|
|
//permissao: ''
|
|
};
|
|
}
|
|
dialogs.value[type] = true;
|
|
};
|
|
|
|
const openEditPage = (type, item) => {
|
|
console.log('Usuário que será editado:', item); // Log para depuração
|
|
|
|
// Criar uma cópia do item para evitar problemas de mutação
|
|
const userData = { ...item };
|
|
|
|
// Garantir que todos os campos necessários estejam presentes
|
|
forms.value[type] = {
|
|
id: userData.id,
|
|
username: userData.username || '',
|
|
email: userData.email || '',
|
|
birth_date: userData.birth_date || '',
|
|
phone: userData.phone || '',
|
|
profile_image: userData.profile_image || '',
|
|
status: userData.status || true,
|
|
};
|
|
|
|
// Definir modo de edição e abrir o diálogo
|
|
isEditing.value = true;
|
|
dialogs.value[type] = true;
|
|
selectedUser.value = null; // Remover a seleção do usuário
|
|
};
|
|
|
|
const handleUserUpdated = (updatedUser) => {
|
|
const index = users.value.findIndex(user => user.id === updatedUser.id);
|
|
if (index !== -1) {
|
|
users.value[index] = updatedUser;
|
|
}
|
|
showNotification('Usuário atualizado com sucesso!');
|
|
};
|
|
|
|
const confirmDelete = (type, item) => {
|
|
console.log("Item recebido para exclusão:", item); // Depuração
|
|
|
|
if (!item || !item.id) {
|
|
console.error("Erro: Item inválido para exclusão.", item);
|
|
showNotification("Erro: Não foi possível selecionar o item para exclusão.", "error");
|
|
return;
|
|
}
|
|
|
|
// Armazena explicitamente o item completo
|
|
itemToDelete.value = {...item};
|
|
|
|
// Verifica se o itemToDelete foi definido corretamente
|
|
console.log("itemToDelete definido como:", itemToDelete.value);
|
|
|
|
dialogs.value.delete = true;
|
|
};
|
|
|
|
const deleteItem = async () => {
|
|
if (!itemToDelete.value || !itemToDelete.value.id) {
|
|
console.error("Erro: Item inválido para exclusão.", itemToDelete.value);
|
|
return;
|
|
}
|
|
|
|
loading.value.delete = true;
|
|
try {
|
|
const userId = itemToDelete.value.id;
|
|
console.log(`Excluindo usuário com ID: ${userId}`);
|
|
|
|
// Faz a requisição para deletar
|
|
await userStore.deleteUser(userId);
|
|
|
|
// Remover o item diretamente da lista local
|
|
// Esta é uma abordagem mais direta e imediata
|
|
users.value = users.value.filter(user => user.id !== userId);
|
|
console.log("Lista atualizada após exclusão:", users.value);
|
|
|
|
showNotification(`Usuário excluído com sucesso!`);
|
|
dialogs.value.delete = false;
|
|
} catch (error) {
|
|
showNotification('Erro ao excluir o item', 'error');
|
|
console.error('Error deleting item:', error);
|
|
} finally {
|
|
loading.value.delete = false;
|
|
itemToDelete.value = null;
|
|
}
|
|
};
|
|
|
|
const showNotification = (text, color = 'success') => {
|
|
snackbar.value = {
|
|
show: true,
|
|
text,
|
|
color
|
|
};
|
|
};
|
|
|
|
const filterUsers = () => {
|
|
// O filtro já é realizado automaticamente por meio do computed property filteredUsers
|
|
};
|
|
|
|
const submitForm = async (userData) => {
|
|
loading.value.submit = true;
|
|
try {
|
|
if (isEditing.value) {
|
|
// Atualizar item existente
|
|
await userStore.updateUser(userData.id, userData);
|
|
const index = users.value.findIndex(user => user.id === userData.id);
|
|
if (index !== -1) {
|
|
users.value[index] = userData;
|
|
}
|
|
} else {
|
|
// Criar novo item
|
|
const newItem = await userStore.createUser(userData);
|
|
users.value.push(newItem);
|
|
}
|
|
showNotification(`Usuário ${isEditing.value ? 'atualizado' : 'cadastrado'} com sucesso!`);
|
|
dialogs.value.user = false;
|
|
} catch (error) {
|
|
showNotification('Erro ao salvar os dados', 'error');
|
|
console.error('Error submitting form:', error);
|
|
} finally {
|
|
loading.value.submit = false;
|
|
}
|
|
};
|
|
|
|
const userStore = useUserStore();
|
|
|
|
// Lifecycle hooks
|
|
onMounted(async () => {
|
|
try {
|
|
loading.value.users = true;
|
|
const usersData = await userStore.catchUsers();
|
|
console.log("Dados recebidos da API:", usersData);
|
|
|
|
// Garanta que os dados já estão filtrados (usuários deletados removidos)
|
|
users.value = usersData
|
|
.filter(user => !user.deleted)
|
|
.map(user => ({
|
|
...user,
|
|
status: user.status !== 'inativo'
|
|
}));
|
|
} catch (error) {
|
|
showNotification('Erro ao carregar dados da API', 'error');
|
|
console.error('Error fetching users:', error);
|
|
} finally {
|
|
loading.value.users = false;
|
|
}
|
|
});
|
|
|
|
return {
|
|
// States
|
|
isEditing,
|
|
loading,
|
|
dialogs,
|
|
filters,
|
|
users,
|
|
forms,
|
|
headers,
|
|
snackbar,
|
|
page,
|
|
itemsPerPage,
|
|
itemToDelete,
|
|
selectedUser,
|
|
statusOptions,
|
|
itemToChangeStatus,
|
|
statusToChange,
|
|
// Computed
|
|
filteredUsers,
|
|
|
|
// Methods
|
|
openDialog,
|
|
confirmDelete,
|
|
submitForm,
|
|
deleteItem,
|
|
openEditPage,
|
|
filterUsers,
|
|
exportToCSV,
|
|
handleUserUpdated,
|
|
toggleUserStatus,
|
|
changeUserStatus
|
|
};
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.image-wrapper {
|
|
display: flex;
|
|
justify-content: flex-end; /* ou center, se quiser centralizar */
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.top-image {
|
|
max-width: 180px;
|
|
height: auto;
|
|
}
|
|
|
|
.info{
|
|
color: rgb(0, 0, 0);
|
|
background-color: rgba(76,201,240);
|
|
background: linear-gradient(145deg, #eeeded, #ffffff);
|
|
box-shadow: 20px 20px 60px #d9d9d9,
|
|
-20px -20px 60px #ffffff;
|
|
}
|
|
|
|
.dados{
|
|
font-size: 17px;
|
|
color: rgb(0, 0, 0);
|
|
background-color: rgb(0, 0, 0);
|
|
background: linear-gradient(145deg, #eeeded, #ffffff);
|
|
box-shadow: 20px 20px 60px #d9d9d9,
|
|
-20px -20px 60px #ffffff;
|
|
}
|
|
|
|
.title {
|
|
font-size: 25px;
|
|
font-weight: 600;
|
|
}
|
|
</style> |