feat:Implementar transição em slide para a tela de login com validação aprimorada e notificações via snackbar

- Adicionado componente de slider para transição entre as telas de introdução e login.
- Validação aprimorada dos campos de e-mail e senha, com mensagens de erro..
- Substituídos os componentes do Vuetify por elementos HTML padrão nos campos de entrada.
- IIntroduzido snackbar para exibição de mensagens de erro durante tentativas de login
- Estilos atualizados para melhor responsividade e apelo visual.
This commit is contained in:
flavia-vic 2025-04-29 09:04:26 -04:00
parent bb33d0b20b
commit d60af1cffc
9 changed files with 1213 additions and 648 deletions

14
package-lock.json generated
View File

@ -9,10 +9,12 @@
"version": "0.0.0",
"dependencies": {
"@mdi/font": "^7.4.47",
"animate.css": "^4.1.1",
"apexcharts": "^3.54.1",
"axios": "^1.8.4",
"dashboard": "file:",
"date-fns": "^4.1.0",
"gsap": "^3.12.7",
"jwt-decode": "^4.0.0",
"npm-check-updates": "^17.1.11",
"pdf-parse": "^1.1.1",
@ -818,6 +820,12 @@
"resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz",
"integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA=="
},
"node_modules/animate.css": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/animate.css/-/animate.css-4.1.1.tgz",
"integrity": "sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ==",
"license": "MIT"
},
"node_modules/apexcharts": {
"version": "3.54.1",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.1.tgz",
@ -992,6 +1000,12 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/gsap": {
"version": "3.12.7",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.12.7.tgz",
"integrity": "sha512-V4GsyVamhmKefvcAKaoy0h6si0xX7ogwBoBSs2CTJwt7luW0oZzC0LhdkyuKV8PJAXr7Yaj8pMjCKD4GJ+eEMg==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license. Club GSAP members get more: https://gsap.com/licensing/. Why GreenSock doesn't employ an MIT license: https://gsap.com/why-license/"
},
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",

View File

@ -10,10 +10,12 @@
},
"dependencies": {
"@mdi/font": "^7.4.47",
"animate.css": "^4.1.1",
"apexcharts": "^3.54.1",
"axios": "^1.8.4",
"dashboard": "file:",
"date-fns": "^4.1.0",
"gsap": "^3.12.7",
"jwt-decode": "^4.0.0",
"npm-check-updates": "^17.1.11",
"pdf-parse": "^1.1.1",

View File

@ -93,7 +93,9 @@ const loadHolidayData = async () => {
estado: holidayData.estado || '',
municipio: holidayData.municipio || '',
parent_id: authStore.userId,
service_instance_id: authStore.service_instance_id || 2
service_instance_id: authStore.service_instance_id || 2,
adicional_he: Number(localHoliday.value.adicional_he), // Garante tipo numérico
recorrente: localHoliday.value.recorrente
};
} else if (props.holiday) {
// Fallback para os dados recebidos via props

View File

@ -19,10 +19,34 @@
:rules="[v => !!v || 'Data é obrigatória']" required />
</v-col>
<v-col cols="12">
<v-text-field v-model="localHoliday.municipio" label="Municipio" type="municipio" :rules="[rules.required]" clearable required/>
<v-text-field v-model="localHoliday.municipio" label="Municipio" type="municipio" />
</v-col>
<v-col cols="12">
<v-text-field v-model="localHoliday.estado" label="Estado" type="estado" clearable :rules="[rules.required]" required/>
<v-text-field v-model="localHoliday.estado" label="Estado" type="estado" clearable />
</v-col>
<v-col cols="12">
<v-switch
v-model="localHoliday.recorrente"
label="Feriado Recorrente (Anual)"
color="primary"
:true-value="true"
:false-value="false"
/>
</v-col>
<v-col cols="12">
<v-text-field
v-model.number="localHoliday.adicional_he"
label="Adicional de HE (%) *"
type="number"
min="0"
max="100"
:rules="[
v => v !== null && v !== undefined || 'Campo obrigatório',
v => (v >= 0 && v <= 100) || 'Valor entre 0 e 100'
]"
required
@input="form?.validate()"
/>
</v-col>
</v-row>
</v-container>
@ -30,8 +54,13 @@
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text color="blue-darken-1" @click="$emit('cancel')">Cancelar</v-btn>
<v-btn text class="text-white salvar-btn" :disabled="!formValid"
@click="handleSave" :loading="holidayStore.loading">
<v-btn
text
class="text-white salvar-btn"
:disabled="!formValid"
@click="handleSave"
:loading="holidayStore.loading"
>
Salvar
</v-btn>
</v-card-actions>
@ -64,6 +93,8 @@ const defaultHoliday = {
date: '',
estado: '',
municipio: '',
recorrente: true,
adicional_he: 0,
parent_id: authStore.userId,
service_instance_id: authStore.service_instance_id || 2
};
@ -116,25 +147,26 @@ const closeModal = () => {
// Salvar o novo feriado
const handleSave = async (event) => {
if(form.value.validate()){
event.preventDefault(); // Impede o comportamento padrão do botão
event.preventDefault();
// Valida apenas o formulário (sem verificações extras)
const { valid } = await form.value.validate();
if (valid) { // Só depende da validação do formulário
try {
const holidayData = {
await holidayStore.createHoliday({
...localHoliday.value,
parent_id: authStore.userId,
service_instance_id: authStore.service_instance_id || 2
};
service_instance_id: authStore.service_instance_id
});
console.log('Criando novo feriado:', holidayData);
await holidayStore.createHoliday(holidayData);
emit('save', holidayData);
emit('save');
emit('update:modalValue', false);
resetLocalHoliday();
} catch (error) {
console.error('Erro ao criar feriado:', error);
}}
}
}
};
// Inicialização do componente

View File

@ -16,8 +16,19 @@ import Holiday from '../views/Holiday.vue'
import Company from '../views/Company.vue'
const routes = [
{ path: '/', redirect: '/login' },
{ path: '/login', component: Login },
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'login',
component: Login,
meta: {
transition: 'slide',
requiresGuest: true
}
},
{
path: '/dashboard',
component: Dashboard,

View File

@ -102,7 +102,19 @@ export const useHolidayStore = defineStore('holidays', {
this.error = null;
try {
const response = await api.post(url, holiday, {
// Garantindo que o objeto holiday tem todos os campos necessários
const holidayData = {
name: holiday.name,
date: holiday.date,
estado: holiday.estado || '',
municipio: holiday.municipio || '',
recorrente: holiday.recorrente !== undefined ? holiday.recorrente : true,
adicional_he: holiday.adicional_he !== undefined ? holiday.adicional_he : 0,
type: holiday.type || '',
service_instance_id: useAuthStore().service_instance_id
};
const response = await api.post(url, holidayData, {
headers: {
'Content-Type': 'application/json'
}
@ -112,7 +124,7 @@ export const useHolidayStore = defineStore('holidays', {
return response.data;
} catch (error) {
this.error = error?.response?.data?.message || error.message || 'Erro ao criar feriado';
console.error('Erro ao criar turno', error);
console.error('Erro ao criar feriado', error);
throw error;
} finally {
this.loading = false;
@ -133,6 +145,7 @@ export const useHolidayStore = defineStore('holidays', {
date: holiday.date,
estado: holiday.estado,
municipio: holiday.municipio,
type: holiday.type || '',
});
const index = this.holidays.findIndex((s) => s.id === id);

View File

@ -2,7 +2,6 @@ import { defineStore } from 'pinia';
import api from '../services/api';
import router from '../routes/router';
import { useAuthStore } from './auth';
import { ca } from 'date-fns/locale';
export const useShiftStore = defineStore('shifts', {
state: () => ({

View File

@ -1,79 +1,98 @@
<template>
<v-container class="tab">
<div class="image-wrapper">
<img src="/image_b.png" alt="Ilustração acima da tabela" class="top-image" />
<div class="holiday-management">
<!-- Header da página -->
<div class="header-container">
<div class="header-left">
<div class="icon-wrapper">
<v-icon color="primary" size="32">mdi-clock-outline</v-icon>
</div>
<v-card>
<v-toolbar class="info" flat>
<v-toolbar-title class="title"> Feriados</v-toolbar-title>
<v-spacer></v-spacer>
<!-- Barra de Pesquisa com Ícone -->
<v-text-field
v-model="filters.holiday"
:loading="loading.holiday"
prepend-inner-icon="mdi-magnify"
density="compact"
variant="outlined"
label="Pesquisar feriado"
hide-details
class="mx-2"
style="max-width: 250px;"
@update:model-value="filterholidays"
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="openCreateModal"
>
Adicionar Feriado
<div class="header-title">
<h1>Sistema de Ponto | Admin</h1>
</div>
</div>
<div class="header-right">
<v-btn variant="text" prepend-icon="mdi-arrow-left" class="back-button" to="/dashboard">
Voltar para Dashboard
</v-btn>
</v-toolbar>
<v-btn color="primary" prepend-icon="mdi-plus" class="new-holiday-btn" @click="openCreateModal">
Novo Feriado
</v-btn>
<v-badge dot color="error" class="notification-badge">
<v-icon>mdi-bell</v-icon>
</v-badge>
<div class="admin-profile">
<v-avatar class="mr-2" color="primary" size="40">
<v-img src="/api/placeholder/40/40" alt="Administrador"></v-img>
</v-avatar>
<div class="admin-info">
<span class="admin-name">Administrador</span>
<span class="admin-role">Gestor</span>
</div>
<v-icon>mdi-chevron-down</v-icon>
</div>
</div>
</div>
<!-- Tabela de Dados -->
<!-- Título e descrição da seção -->
<div class="section-header">
<h2>Feriados e Dias Especiais</h2>
<p class="section-description">Gerencie os feriados e dias com tratamento especial</p>
</div>
<!-- Card principal -->
<v-card class="main-card" elevation="1">
<div class="card-header">
<h3>Lista de Feriados</h3>
<p class="card-description">Gerencie os feriados e dias especiais do sistema</p>
</div>
<!-- Tabela de feriados -->
<v-data-table
class="dados"
:headers="headers.holidays"
:items="filteredholidays"
:search="filters.holiday"
:loading="loading.holidays"
:items-per-page="itemsPerPage"
:page.sync="page"
class="holiday-table"
>
<!-- Slot para campo tipo (adicional) -->
<template v-slot:item.type="{ item }">
<v-chip
:color="getTypeColor(item.type)"
text-color="white"
size="small"
class="type-chip"
>
{{ item.type || 'Nacional' }}
</v-chip>
</template>
<!-- Slot para recorrente -->
<template v-slot:item.recorrente="{ item }">
<span>{{ item.recorrente ? 'Sim' : 'Não' }}</span>
</template>
<!-- Slot para adicional_he -->
<template v-slot:item.adicional_he="{ item }">
{{ item.adicional_he }}%
</template>
<!-- Slot para ações -->
<template v-slot:item.actions="{ item }">
<v-icon
size="small"
class="mr-2"
@click="openEditPage('holiday', item)"
>
mdi-pencil
</v-icon>
<v-icon
size="small"
@click="confirmDelete('holiday', item)"
>
mdi-delete
</v-icon>
<div class="action-buttons">
<v-btn icon color="primary" size="small" @click="openEditPage('holiday', item)">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon color="error" size="small" @click="confirmDelete('holiday', item)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</div>
</template>
</v-data-table>
</v-card>
<!-- Diálogo de Usuário (Novo/Edição) -->
<!-- Modais existentes -->
<holidayModal
v-model="dialogs.holiday"
:isEditMode="isEditing"
@ -81,11 +100,13 @@
@save="submitForm"
@cancel="closeHolidayModal"
/>
<CreateHolidayModal
v-model="dialogs.createholiday"
@save="SubmitFormCreate"
@cancel="closeCreateModal"
/>
<!-- Diálogo de Confirmação de Exclusão -->
<v-dialog v-model="dialogs.delete" max-width="400px">
<v-card>
@ -94,8 +115,8 @@
</v-card-title>
<v-card-text class="pt-4">
<p v-if="itemToDelete && itemToDelete.holidayname">
Tem certeza que deseja excluir <strong>{{ itemToDelete.holidayname }}</strong>?
<p v-if="itemToDelete && itemToDelete.name">
Tem certeza que deseja excluir <strong>{{ itemToDelete.name }}</strong>?
Esta ação não pode ser desfeita.
</p>
<p v-else>
@ -135,17 +156,17 @@
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import holidayModal from '../components/modals/HolidayModal.vue';
import {useHolidayStore} from '../stores/holiday';
import CreateHolidayModal from '../components/modals/HolidayModalCreate.vue';
<script>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import holidayModal from '../components/modals/HolidayModal.vue';
import { useHolidayStore } from '../stores/holiday';
import CreateHolidayModal from '../components/modals/HolidayModalCreate.vue';
export default {
export default {
name: 'holidayManagement',
components: {
holidayModal,
@ -153,13 +174,10 @@
},
setup() {
const router = useRouter();
//const camera = computed(() => cameraStore.camera);
const holidayStore = useHolidayStore();
// Estados
const isCreateModalOpen = ref(false);
const isEditing = ref(false);
const itemToEdit = ref(null);
const itemToDelete = ref(null);
const page = ref(1);
const itemsPerPage = ref(10);
@ -179,7 +197,6 @@
createholiday: false,
delete: false
});
const editMode = ref(false)
// Filtros
const filters = ref({
@ -187,27 +204,31 @@
});
// Dados - array vazio que será preenchido via API ou entrada do usuário
const holidays = ref([])
const holidays = ref([]);
// Formulários
const forms = ref({
holiday: {
id: null,
name: '',
date:'',
date: '',
type: 'Nacional', // Adicionado campo tipo
estado: '',
municipio: '',
loading:false
recorrente: true,
adicional_he: 0,
loading: false
}
});
// Headers para as tabelas
// Headers para as tabelas (atualizados para o novo layout)
const headers = ref({
holidays: [
{ title: 'ID', key: 'id' },
{ title: 'Nome feriado', key: 'name' },
{ title: 'Data', key: 'date' },
//{ title: 'Permissão', key: 'permissao' },
{ title: 'Nome', key: 'name', align: 'start' },
{ title: 'Data', key: 'date', align: 'start' },
{ title: 'Tipo', key: 'type', align: 'start' }, // Novo campo tipo
{ title: 'Recorrente', key: 'recorrente', align: 'start' },
{ title: 'Adicional H.E.', key: 'adicional_he', align: 'start' },
{ title: 'Ações', key: 'actions', sortable: false, align: 'end' }
]
});
@ -219,121 +240,67 @@
color: 'success'
});
// Método para determinar a cor do chip baseado no tipo
const getTypeColor = (type) => {
const typeColors = {
'Nacional': 'primary',
'Estadual': 'success',
'Municipal': 'info',
'Ponto Facultativo': 'warning'
};
return typeColors[type] || 'primary';
};
// Computed properties
const filteredholidays = computed(() => {
const filtered = holidays.value.filter(holiday => !holiday.deleted);
return filtered.filter(holiday => {
const holidayName = holiday.name || ''; // Garantir que seja uma string vazia se undefined
return holidayName.toLowerCase().includes(filters.value.holiday.toLowerCase())
return holidays.value.filter(holiday => {
return (
holiday.name.toLowerCase().includes(filters.value.holiday.toLowerCase()) &&
!holiday.deleted
)
}).map(holiday => ({
...holiday,
date: formatDate(holiday.date),
created_at: holiday.created_at ? new Date(holiday.created_at).toLocaleString() : '-'
}));
});
/*
// Função para exportar para CSV
const exportToCSV = () => {
if (filteredcameras.value.length === 0) {
showNotification('Não há dados para exportar', 'warning');
return;
// Função auxiliar para formatar data
function formatDate(dateString) {
if (!dateString) return '';
// Verificar se a data tem ano ou é apenas DD/MM
if (dateString.includes('/') && dateString.split('/').length === 2) {
return dateString; // Já está no formato DD/MM
}
loading.value.export = true;
const options = { day: '2-digit', month: '2-digit' };
try {
// Preparar dados: filtrar apenas os campos que queremos exportar
const dataToExport = filteredcameras.value.map(camera => ({
ID: camera.id,
Nome: camera.name,
Status: camera.status,
Descrição: camera.description,
Url: camera.url,
//Phone: camera.phone,
//Image: camera.profile_image,
//Contrato: camera.contrato,
//Grupo: camera.grupo,
//Permissao: camera.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;
// Se tem ano completo, formatar como DD/MM/YYYY
if (dateString.includes('/') && dateString.split('/').length === 3) {
return dateString;
}
};
*/
return new Date(dateString).toLocaleDateString('pt-BR', options);
} catch (e) {
return dateString; // Retorna a string original se falhar
}
}
// Métodos
const openDialog = (type, action, item = null) => {
if (!dialogs.value[type]) {
dialogs.value[type] = false
}
if (!forms.value[type]) {
forms.value[type] = {
id: '',
name: '',
date: '',
loading: false
}
}
editMode.value = action === 'edit'
if (type === 'holiday') {
if (action === 'edit') {
forms.value.holiday = {
...item,
loading: false
}
}
// Agora sim abre o modal depois que tudo tá definido
dialogs.value.holiday = true
}
}
// Abrir modal de criação
const openCreateModal = () => {
isCreateModalOpen.value = true;
dialogs.value.createholiday = true
dialogs.value.createholiday = true;
};
const closeHolidayModal = () => {
dialogs.value.holiday = false;
};
const closeCreateModal = () => {
dialogs.value.createholiday = false;
};
const openEditPage = (type, item) => {
console.log('Feriado que será editado:', item); // Log para depuração
console.log('Feriado que será editado:', item);
// Criar uma cópia do item para evitar problemas de mutação
const holidayData = { ...item };
@ -343,26 +310,21 @@
id: holidayData.id,
name: holidayData.name || '',
date: holidayData.date || '',
type: holidayData.type || 'Nacional', // Adicionado campo tipo
estado: holidayData.estado || '',
municipio: holidayData.municipio || '',
recorrente: holidayData.recorrente !== undefined ? holidayData.recorrente : true,
adicional_he: holidayData.adicional_he !== undefined ? holidayData.adicional_he : 0,
};
// Definir modo de edição e abrir o diálogo
isEditing.value = true;
dialogs.value[type] = true;
selectedholiday.value = null; // Remover a seleção do usuário
};
const handleholidayUpdated = (updatedholiday) => {
const index = holidays.value.findIndex(holiday => holiday.id === updatedholiday.id);
if (index !== -1) {
holidays.value[index] = updatedholiday;
}
showNotification('Feriado atualizado com sucesso!');
selectedholiday.value = null;
};
const confirmDelete = (type, item) => {
console.log("Item recebido para exclusão:", item); // Depuração
console.log("Item recebido para exclusão:", item);
if (!item || !item.id) {
console.error("Erro: Item inválido para exclusão.", item);
@ -373,9 +335,7 @@
// 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;
};
@ -394,7 +354,6 @@
await holidayStore.deleteHoliday(holidayId);
// Remover o item diretamente da lista local
// Esta é uma abordagem mais direta e imediata
holidays.value = holidays.value.filter(holiday => holiday.id !== holidayId);
console.log("Lista atualizada após exclusão:", holidays.value);
@ -417,10 +376,6 @@
};
};
const filterholidays = () => {
// O filtro já é realizado automaticamente por meio do computed property filteredholidays
};
const submitForm = async (holidayData) => {
loading.value.submit = true;
try {
@ -440,12 +395,12 @@
loading.value.submit = false;
dialogs.value.holiday = false;
}
};
const SubmitFormCreate = async (holidayData) => {
loading.value.submit = true;
try {
//const newItem = await holidayStore.createHoliday(holidayData);
const newItem = await holidayStore.createHoliday(holidayData);
holidays.value.push(newItem);
showNotification('Feriado cadastrado com sucesso!');
} catch (error) {
@ -460,14 +415,15 @@
onMounted(async () => {
try {
loading.value.holidays = true;
console.log('cheguei aqui')
const holidaysData = await holidayStore.fetchHolidays();
holidays.value = holidaysData;
console.log("Dados recebidos da API:", holidaysData);
// Adicionar tipo para os dados existentes
holidays.value = holidaysData.map(holiday => ({
...holiday,
type: holiday.type || 'Nacional' // Adicionar campo tipo se não existir
}));
// Garanta que os dados já estão filtrados (usuários deletados removidos)
//holidays.value = holidaysData.filter(holiday => !holiday.deleted);
console.log("Dados recebidos da API:", holidays.value);
} catch (error) {
showNotification('Erro ao carregar dados da API', 'error');
console.error('Error fetching feriados:', error);
@ -493,10 +449,9 @@
// Computed
filteredholidays,
filterholidays,
// Methods
openDialog,
getTypeColor,
confirmDelete,
submitForm,
SubmitFormCreate,
@ -505,44 +460,183 @@
openCreateModal,
closeHolidayModal,
closeCreateModal,
// filtercameras,
//exportToCSV,
handleholidayUpdated
showNotification
};
}
};
</script>
};
</script>
<style scoped>
.image-wrapper {
<style scoped>
.holiday-management {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
font-family: Roboto, Arial, sans-serif;
}
/* Estilo do cabeçalho */
.header-container {
display: flex;
justify-content: flex-end; /* ou center, se quiser centralizar */
margin-bottom: 16px;
}
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 0 0 20px 0;
border-bottom: 1px solid #eaecef;
}
.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;
}
.header-left {
display: flex;
align-items: center;
}
.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;
}
.icon-wrapper {
background-color: #e3f2fd;
border-radius: 8px;
padding: 12px;
margin-right: 12px;
}
.title {
font-size: 25px;
.header-title h1 {
font-size: 18px;
font-weight: 600;
}
</style>
margin: 0;
color: #1e293b;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.back-button {
color: #1976d2;
}
.new-holiday-btn {
background-color: #1976d2;
color: white;
}
.admin-profile {
display: flex;
align-items: center;
margin-left: 16px;
cursor: pointer;
}
.admin-info {
display: flex;
flex-direction: column;
margin: 0 8px;
}
.admin-name {
font-size: 14px;
font-weight: 500;
color: #1e293b;
}
.admin-role {
font-size: 12px;
color: #64748b;
}
.notification-badge {
margin: 0 8px;
}
/* Estilo da seção de título */
.section-header {
margin-bottom: 24px;
}
.section-header h2 {
font-size: 24px;
font-weight: 600;
margin: 0 0 8px 0;
color: #1e293b;
}
.section-description {
color: #64748b;
font-size: 14px;
margin: 0;
}
/* Estilo do card principal */
.main-card {
border-radius: 12px;
overflow: hidden;
background-color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}
.card-header {
padding: 24px;
border-bottom: 1px solid #eaecef;
}
.card-header h3 {
font-size: 18px;
font-weight: 600;
margin: 0 0 8px 0;
color: #1e293b;
}
.card-description {
color: #64748b;
font-size: 14px;
margin: 0;
}
/* Estilo da tabela */
.holiday-table {
padding: 0;
}
.holiday-table :deep(th) {
font-weight: 600;
color: #1e293b;
text-transform: none;
font-size: 14px;
}
.holiday-table :deep(td) {
font-size: 14px;
color: #1e293b;
padding: 16px;
height: 64px;
}
/* Estilo dos chips de tipo */
.type-chip {
font-size: 12px;
font-weight: 500;
text-transform: none;
}
/* Estilo dos botões de ação */
.action-buttons {
display: flex;
gap: 8px;
justify-content: flex-end;
}
/* Estilos para os diferentes tipos de feriados */
.holiday-type-nacional {
background-color: #1976d2;
}
.holiday-type-estadual {
background-color: #2e7d32;
}
.holiday-type-municipal {
background-color: #0288d1;
}
.holiday-type-facultativo {
background-color: #ff9800;
}
</style>

View File

@ -1,49 +1,74 @@
<template>
<v-container class="login-container fill-height" fluid>
<v-row no-gutters class="fill-height align-center justify-center">
<v-col cols="12" md="10" class="d-flex align-center justify-center">
<div class="slider-container">
<!-- Container de slides que se move horizontalmente -->
<div class="slides-wrapper" :style="slideStyle">
<!-- Slide 1: Tela Inicial -->
<div class="slide intro-slide">
<div class="content-container">
<!-- Card Inicial - Versão Atualizada -->
<div class="intro-card">
<div class="intro-content">
<h1 class="intro-title">Olhe Cada Detalhe</h1>
<p class="intro-subtitle">Inteligência artificial trazendo dados para o seu negócio</p>
<v-btn
color="primary"
class="access-btn"
height="44"
@click="slideToLogin"
>
Acessar
</v-btn>
</div>
</div>
</div>
</div>
<!-- Slide 2: Tela de Login -->
<div class="slide login-slide">
<!-- Retângulo de fundo -->
<div class="retangulo-box">
<!-- Formulário (esquerda) -->
<!-- Formulário de Login -->
<div class="login-form-box">
<h2 class="title">Bem-vindo</h2>
<p class="subtitle">Realize o login em sua conta</p>
<v-form class="form" v-model="valid" ref="loginForm">
<v-text-field
<v-form class="form" v-model="valid" ref="loginForm" @submit.prevent="login">
<div class="input-container">
<label for="email">E-mail</label>
<input
id="email"
v-model="email"
label="E-mail"
:rules="emailRules"
required
hide-details="auto"
class="custom-field"
></v-text-field>
type="text"
:class="['custom-input', {'input-error': emailError}]"
@blur="validateEmail"
/>
<div v-if="emailError" class="error-text">{{ emailError }}</div>
</div>
<v-text-field
<div class="input-container">
<label for="password">Senha</label>
<input
id="password"
v-model="password"
label="Senha"
:rules="passwordRules"
@keyup.enter="login"
type="password"
required
hide-details="auto"
class="custom-field"
></v-text-field>
:class="['custom-input', {'input-error': passwordError}]"
@blur="validatePassword"
@keyup.enter="login"
/>
<div v-if="passwordError" class="error-text">{{ passwordError }}</div>
</div>
<div class="text-right forgot-password">
<a href="#">Esqueci a senha</a>
</div>
<v-btn
block
color="primary"
<button
class="login-btn"
height="44"
:disabled="!valid"
@click="login"
:disabled="!isFormValid"
@click.prevent="login"
>
Entrar
</v-btn>
</button>
</v-form>
<div class="register-link">
@ -51,82 +76,260 @@
</div>
</div>
<!-- Círculo (direita) -->
<!-- Gráfico/Círculo informativo -->
<div class="circle-container">
<img src="/image.png" alt="Círculo" class="circle-img" />
</div>
</div>
</v-col>
</v-row>
</v-container>
</div>
</div>
</div>
<!-- Snackbar para mensagens de erro/sucesso -->
<v-snackbar
v-model="snackbar"
:color="snackbarColor"
timeout="5000"
position="top"
>
{{ snackbarText }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar = false"
>
Fechar
</v-btn>
</template>
</v-snackbar>
</template>
<script setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useAuthStore } from '../stores/auth'; // Certifique-se de que o caminho da store está correto
const router = useRouter();
const authStore = useAuthStore();
// Estado
const currentSlide = ref(0); // 0 = intro, 1 = login
const email = ref('');
const password = ref('');
const emailError = ref('');
const passwordError = ref('');
const sliding = ref(false);
const valid = ref(false);
const emailRules = [
(v) => !!v || 'Preencha o campo', // Verifica se está preenchido
(v) =>
/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(v) || /^[a-zA-Z0-9_.-]+$/.test(v) ||
'Insira um e-mail válido ou um nome de usuário', // Permite emails ou usernames simples
];
// Estado para o Snackbar
const snackbar = ref(false);
const snackbarText = ref('');
const snackbarColor = ref('error'); // 'error', 'success', 'info', etc.
const passwordRules = [(v) => !!v || 'Preencha a senha'];
// Validação computada
const isFormValid = computed(() => {
return email.value && password.value && !emailError.value && !passwordError.value;
});
const login = async () => {
try {
await authStore.login(email.value, password.value); // Ação de login no Pinia
router.push('/dashboard'); // Redireciona ao dashboard após login bem-sucedido
} catch (error) {
alert('Falha ao fazer login. Verifique suas credenciais.'); // Exibe erro em caso de falha
// Estilo computado para a posição do slider
const slideStyle = computed(() => {
return {
transform: `translateX(-${currentSlide.value * 100}%)`,
transition: sliding.value ? 'transform 0.8s ease-in-out' : 'none'
};
});
// Funções de validação
const validateEmail = () => {
if (!email.value) {
emailError.value = 'Preencha o campo';
return;
}
if (!/^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(email.value) &&
!/^[a-zA-Z0-9_.-]+$/.test(email.value)) {
emailError.value = 'Insira um e-mail válido ou um nome de usuário';
return;
}
emailError.value = '';
};
const validatePassword = () => {
if (!password.value) {
passwordError.value = 'Preencha a senha';
return;
}
passwordError.value = '';
};
// Métodos para navegação de slides - Melhorados
const slideToLogin = () => {
console.log('Deslizando para o login...');
sliding.value = true;
currentSlide.value = 1;
// Adicionado: Garantir que o estado seja atualizado imediatamente
setTimeout(() => {
console.log('Slide atual após timeout:', currentSlide.value);
}, 100);
};
const slideToIntro = () => {
console.log('Deslizando para a introdução...');
sliding.value = true;
currentSlide.value = 0;
// Adicionado: Garantir que o estado seja atualizado imediatamente
setTimeout(() => {
console.log('Slide atual após timeout:', currentSlide.value);
}, 100);
};
// Função de login usando o authStore do Pinia
const login = async () => {
if (!isFormValid.value) {
// Validar formulário novamente
validateEmail();
validatePassword();
return;
}
try {
console.log('Tentando fazer login com:', email.value, password.value);
await authStore.login(email.value, password.value); // Ação de login no Pinia
// Forçar a navegação para o dashboard usando location.href
// em vez de router.push para garantir que saia do sistema de slides
window.location.href = '/dashboard';
// Alternativa: router.push com opção replace
// router.push({ path: '/dashboard', replace: true });
} catch (error) {
console.error('Erro de login:', error);
// Exibe o Snackbar com a mensagem de erro
snackbarText.value = 'Falha ao fazer login. Verifique suas credenciais.';
snackbarColor.value = 'error';
snackbar.value = true;
}
};
</script>
<style scoped>
.login-container {
background: linear-gradient(135deg, #2d0f44, #23408e);
color: #fff;
<style scoped>
/* Container principal que mantém todo o conteúdo e controla overflow */
.slider-container {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
/* Wrapper que desliza horizontalmente contendo ambos os slides */
.slides-wrapper {
display: flex;
width: 100vw; /* Duas vezes a largura da viewport - CORRIGIDO */
height: 100%;
will-change: transform; /* Otimiza a animação */
}
/* Cada slide individual ocupa uma viewport completa */
.slide {
width: 100vw;
height: 100vh;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
/* Estilos específicos para cada slide */
.intro-slide {
background: linear-gradient(135deg, #2d0f44, #23408e);
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.login-slide {
background: linear-gradient(135deg, #3a1788, #4c82e6);
}
/* Container para o conteúdo centralizado em cada slide */
.content-container {
display: flex;
width: 100%;
max-width: 1200px;
position: relative;
justify-content: flex-start;
align-items: center;
padding: 0 20px;
}
/* Cartão de introdução */
.intro-card {
background-color: rgba(252, 252, 252, 0.1);
padding: 50px 40px;
border-radius: 20px;
width: 500%;
max-width: 650px;
color: white;
margin-left: 10px;
backdrop-filter: blur(10px);
}
.intro-title {
font-size: 120px;
font-weight: 120;
color: white;
margin-bottom: 15px;
line-height: 1.1;
}
.intro-subtitle {
margin-bottom: 30px;
color: rgba(255, 255, 255, 0.85);
font-size: 22px;
line-height: 1.4;
font-size: 40px;
font-weight: 110;
}
.access-btn {
background: linear-gradient(to right, #5189f9, #4447f1) !important;
color: white !important;
font-weight: 600 !important;
text-transform: none !important;
border-radius: 25px !important;
margin-top: 10px !important;
width: 70% !important;
height: 60px !important;
font-size: 18px !important;
letter-spacing: 0.5px !important;
}
/* Formulário de login */
.retangulo-box {
background-image: url('/image_ret.png');
background-repeat: no-repeat;
background-size: 110%;
background-size: 98%;
background-position: center;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 1200px;
height: 600px;
padding: 120px;
box-sizing: border-box;
position: relative;
width: 110%;
max-width: 1500px;
height: 1500px;
padding: 110px;
border-radius: 30px;
}
.login-form-box {
background-color: white;
padding: 30px;
border-radius: 20px;
width: 100%;
max-width: 360px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
color: #000;
width: 34%;
margin-left: 130px;
}
.title {
@ -137,52 +340,247 @@ const login = async () => {
}
.subtitle {
margin-bottom: 20px;
color: #777;
margin-bottom: 30px;
color: rgba(255, 255, 255, 0.7);
}
.custom-field {
margin-bottom: 20px;
.form {
width: 100%;
}
.input-container {
margin-bottom: 22px;
}
.input-container label {
display: block;
margin-bottom: 6px;
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
}
.custom-input {
width: 100%;
height: 44px;
padding: 0 16px;
background-color: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
color: white;
font-size: 16px;
box-sizing: border-box;
transition: all 0.3s ease;
}
.custom-input:focus {
outline: none;
border-color: #4c9ff0;
background-color: rgba(255, 255, 255, 0.25);
}
.input-error {
border-color: #ff4d4f;
}
.error-text {
color: #ff4d4f;
font-size: 12px;
margin-top: 4px;
}
.forgot-password {
margin-bottom: 20px;
text-align: right;
margin-bottom: 25px;
font-size: 14px;
}
.login-btn {
background: linear-gradient(to right, #86b6f6, #b8b4f7);
width: 100%;
height: 44px;
background: linear-gradient(to right, #3e7bf4, #5658f5);
color: white;
font-weight: 600;
text-transform: none;
border-radius: 25px;
margin-bottom: 15px;
border: none;
border-radius: 20px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s ease;
}
.login-btn:hover {
background: linear-gradient(to right, #3568d4, #4a4ce5);
}
.register-link {
margin-top: 20px;
font-size: 14px;
color: #333;
color: rgba(255, 255, 255, 0.8);
text-align: center;
}
.register-link a,
.forgot-password a {
color: #4c9ff0;
text-decoration: none;
font-weight: 500;
}
.circle-container {
flex: 1;
.register-link a:hover,
.forgot-password a:hover {
text-decoration: underline;
}
.circle-content {
display: flex;
justify-content: flex-end;
flex-direction: column;
align-items: center;
padding-left: 20px;
justify-content: center;
}
.circle-img {
width: 100%;
max-width: 320px;
width: 270%;
max-width: 650px;
height: auto;
}
</style>
/* Responsividade */
@media (max-width: 960px) {
.content-container {
flex-direction: column;
height: auto;
padding: 40px 20px;
}
.intro-card {
margin: 0 0 30px 0;
max-width: 90%;
}
.retangulo-box {
flex-direction: column;
padding: 40px 20px;
height: auto;
}
.login-form-box,
.circle-container {
width: 90%;
margin: 15px 0;
}
@media (max-width: 1200px) {
.intro-title {
font-size: 80px;
}
.intro-subtitle {
font-size: 28px;
}
.retangulo-box {
padding: 80px;
background-size: 100%;
}
.login-form-box {
margin-left: 50px;
width: 45%;
}
.circle-img {
width: 200%;
}
}
@media (max-width: 960px) {
.slides-wrapper {
flex-direction: column;
transform: none !important;
}
.slide {
width: 100%;
height: auto;
min-height: 100vh;
}
.content-container {
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.intro-card {
margin: 0;
padding: 40px 20px;
width: 90%;
}
.intro-title {
font-size: 50px;
}
.intro-subtitle {
font-size: 22px;
}
.access-btn {
width: 80% !important;
height: 50px !important;
font-size: 16px !important;
}
.retangulo-box {
flex-direction: column;
padding: 40px 20px;
height: auto;
width: 95%;
}
.login-form-box {
width: 90%;
margin: 0;
}
.circle-container {
margin-top: 30px;
}
.circle-img {
width: 100%;
max-width: 300px;
}
}
@media (max-width: 600px) {
.intro-title {
font-size: 36px;
}
.intro-subtitle {
font-size: 18px;
}
.access-btn {
font-size: 14px !important;
height: 44px !important;
}
.title {
font-size: 24px;
}
.subtitle {
font-size: 16px;
}
.login-btn {
font-size: 14px;
height: 44px;
}
}
}
</style>