Versão 06/02 01h04
This commit is contained in:
parent
0eab037432
commit
f38df351ce
331
src/App.vue
331
src/App.vue
@ -1,100 +1,299 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- Anterior template permanece o mesmo até a parte do menu -->
|
||||||
<v-app>
|
<v-app>
|
||||||
<v-navigation-drawer
|
<v-navigation-drawer
|
||||||
app
|
v-model="drawer"
|
||||||
:width="drawerWidth"
|
:rail="isCollapsed"
|
||||||
:permanent="true"
|
:width="260"
|
||||||
color="blue-darken-4"
|
permanent
|
||||||
|
elevation="4"
|
||||||
class="sidebar-navigation"
|
class="sidebar-navigation"
|
||||||
|
theme="dark"
|
||||||
>
|
>
|
||||||
<!-- Collapse Button -->
|
<!-- Logo Section -->
|
||||||
<div class="drawer-toggle d-flex align-center" :class="{ 'justify-center': isCollapsed, 'justify-end': !isCollapsed }">
|
<div class="logo-section pa-4">
|
||||||
<v-tooltip
|
<div class="d-flex align-center" :class="{ 'justify-center': isCollapsed }">
|
||||||
:text="isCollapsed ? 'Expandir Menu' : 'Recolher Menu'"
|
<v-avatar
|
||||||
location="right"
|
:size="isCollapsed ? 40 : 36"
|
||||||
|
class="gradient-avatar"
|
||||||
>
|
>
|
||||||
<template v-slot:activator="{ props }">
|
<v-icon size="24" color="white">mdi-security</v-icon>
|
||||||
<v-btn
|
</v-avatar>
|
||||||
icon
|
<span v-if="!isCollapsed" class="text-h6 ml-3 white--text font-weight-bold">TARS</span>
|
||||||
@click="toggleDrawer"
|
</div>
|
||||||
color="white"
|
|
||||||
v-bind="props"
|
|
||||||
class="collapse-button my-2"
|
|
||||||
size="24"
|
|
||||||
variant="text"
|
|
||||||
>
|
|
||||||
<v-icon :size="20">
|
|
||||||
{{ isCollapsed ? 'mdi-menu' : 'mdi-chevron-left' }}
|
|
||||||
</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
</v-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-list density="compact">
|
<v-divider class="border-opacity-25"></v-divider>
|
||||||
<!-- TARS Logo/Title -->
|
|
||||||
<v-list-item class="logo-container py-1">
|
<!-- User Profile Section -->
|
||||||
<template v-slot:prepend>
|
<div class="profile-section px-4 py-3" v-if="!isCollapsed">
|
||||||
<v-icon color="white" :size="20">
|
<div class="d-flex align-center">
|
||||||
mdi-robot
|
<v-avatar color="primary" size="40">
|
||||||
</v-icon>
|
<v-icon>mdi-account</v-icon>
|
||||||
</template>
|
</v-avatar>
|
||||||
<v-list-item-title
|
<div class="ml-3">
|
||||||
|
<div class="text-subtitle-2 font-weight-medium">Admin User</div>
|
||||||
|
<div class="text-caption text-grey-lighten-1">Administrador</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-divider class="border-opacity-25"></v-divider>
|
||||||
|
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<v-list nav class="px-2">
|
||||||
|
<template v-for="category in menuCategories" :key="category.title">
|
||||||
|
<v-list-subheader
|
||||||
v-if="!isCollapsed"
|
v-if="!isCollapsed"
|
||||||
class="text-subtitle-2 text-white font-weight-bold"
|
class="text-caption font-weight-bold text-uppercase ml-2 mt-2"
|
||||||
>
|
>
|
||||||
TARS
|
{{ category.title }}
|
||||||
</v-list-item-title>
|
</v-list-subheader>
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-divider class="my-2 divider-custom"></v-divider>
|
|
||||||
|
|
||||||
<!-- Menu Items -->
|
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="item in menuItems"
|
v-for="item in category.items"
|
||||||
:key="item.name"
|
:key="item.name"
|
||||||
:to="item.route"
|
:to="item.route"
|
||||||
link
|
:value="item.name"
|
||||||
class="menu-item d-flex align-center"
|
rounded="lg"
|
||||||
density="compact"
|
class="mb-1"
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="white" :size="20" class="menu-icon">{{ item.icon }}</v-icon>
|
<v-icon :size="22">{{ item.icon }}</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title
|
|
||||||
v-if="!isCollapsed"
|
<v-list-item-title class="text-subtitle-2">
|
||||||
class="text-white text-body-2"
|
|
||||||
>
|
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
|
|
||||||
|
<template v-slot:append v-if="item.badge && !isCollapsed">
|
||||||
|
<v-chip
|
||||||
|
size="x-small"
|
||||||
|
:color="item.badge.color"
|
||||||
|
class="font-weight-bold"
|
||||||
|
>
|
||||||
|
{{ item.badge.text }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
|
<!-- Bottom Actions -->
|
||||||
|
<template v-slot:append>
|
||||||
|
<div class="pa-4">
|
||||||
|
<v-btn
|
||||||
|
block
|
||||||
|
@click="toggleDrawer"
|
||||||
|
color="primary"
|
||||||
|
variant="tonal"
|
||||||
|
:prepend-icon="isCollapsed ? 'mdi-chevron-right' : 'mdi-chevron-left'"
|
||||||
|
>
|
||||||
|
<span v-if="!isCollapsed">Recolher Menu</span>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
|
|
||||||
<!-- Compact Header with Logout -->
|
<!-- Top Bar -->
|
||||||
<v-app-bar
|
<v-app-bar
|
||||||
app
|
elevation="1"
|
||||||
color="blue-darken-3"
|
height="64"
|
||||||
dark
|
color="background"
|
||||||
height="48"
|
|
||||||
class="header-compact px-2"
|
|
||||||
density="compact"
|
|
||||||
>
|
>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer />
|
||||||
|
|
||||||
|
<!-- Top Bar Actions -->
|
||||||
<v-btn
|
<v-btn
|
||||||
@click="logout"
|
variant="text"
|
||||||
text
|
icon="mdi-bell"
|
||||||
class="logout-button"
|
class="mr-2"
|
||||||
density="compact"
|
>
|
||||||
size="small"
|
<v-badge
|
||||||
|
color="error"
|
||||||
|
content="3"
|
||||||
|
dot
|
||||||
|
></v-badge>
|
||||||
|
</v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
variant="text"
|
||||||
|
icon="mdi-cog"
|
||||||
|
class="mr-2"
|
||||||
|
></v-btn>
|
||||||
|
|
||||||
|
<v-btn
|
||||||
|
color="error"
|
||||||
|
variant="tonal"
|
||||||
|
@click="logout"
|
||||||
|
prepend-icon="mdi-logout"
|
||||||
|
class="mr-2"
|
||||||
>
|
>
|
||||||
<v-icon size="small" class="mr-1">mdi-logout</v-icon>
|
|
||||||
Sair
|
Sair
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
<v-main class="content-area">
|
<!-- Main Content -->
|
||||||
<router-view />
|
<v-main>
|
||||||
|
<v-container fluid class="pa-6">
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<transition name="fade" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</router-view>
|
||||||
|
</v-container>
|
||||||
</v-main>
|
</v-main>
|
||||||
</v-app>
|
</v-app>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AppLayout',
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
drawer: true,
|
||||||
|
isCollapsed: false,
|
||||||
|
menuCategories: [
|
||||||
|
{
|
||||||
|
title: 'Principal',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'home',
|
||||||
|
route: { name: 'home' },
|
||||||
|
icon: 'mdi-home',
|
||||||
|
label: 'Home'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dashboard',
|
||||||
|
route: { name: 'dashboard' },
|
||||||
|
icon: 'mdi-view-dashboard',
|
||||||
|
label: 'Dashboard',
|
||||||
|
badge: { text: 'Novo', color: 'success' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'monitoring',
|
||||||
|
route: { name: 'monitoring' },
|
||||||
|
icon: 'mdi-cctv',
|
||||||
|
label: 'Monitoramento',
|
||||||
|
badge: { text: '12', color: 'info' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Gestão',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'analytics',
|
||||||
|
route: { name: 'analytics' },
|
||||||
|
icon: 'mdi-chart-box',
|
||||||
|
label: 'Analytics'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'reports',
|
||||||
|
route: { name: 'reports' },
|
||||||
|
icon: 'mdi-file-chart',
|
||||||
|
label: 'Relatórios'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'alerts',
|
||||||
|
route: { name: 'alerts' },
|
||||||
|
icon: 'mdi-bell-ring',
|
||||||
|
label: 'Alertas',
|
||||||
|
badge: { text: '3', color: 'error' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Desenvolvimento',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'testing',
|
||||||
|
route: { name: 'testing' },
|
||||||
|
icon: 'mdi-flask',
|
||||||
|
label: 'Testes',
|
||||||
|
badge: { text: 'Dev', color: 'warning' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'register',
|
||||||
|
route: { name: 'register' },
|
||||||
|
icon: 'mdi-file-document-plus',
|
||||||
|
label: 'Registro'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Configurações',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
name: 'cameras',
|
||||||
|
route: { name: 'cameras' },
|
||||||
|
icon: 'mdi-camera',
|
||||||
|
label: 'Câmeras'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'users',
|
||||||
|
route: { name: 'users' },
|
||||||
|
icon: 'mdi-account-group',
|
||||||
|
label: 'Usuários'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'settings',
|
||||||
|
route: { name: 'settings' },
|
||||||
|
icon: 'mdi-cog',
|
||||||
|
label: 'Configurações'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapActions('auth', ['logout']),
|
||||||
|
|
||||||
|
toggleDrawer() {
|
||||||
|
this.isCollapsed = !this.isCollapsed;
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleLogout() {
|
||||||
|
try {
|
||||||
|
await this.logout();
|
||||||
|
this.$router.push('/login');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao fazer logout:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar-navigation {
|
||||||
|
background: linear-gradient(145deg, #1a237e 0%, #0d47a1 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-avatar {
|
||||||
|
background: linear-gradient(145deg, #2196f3 0%, #1565c0 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-section {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-list-item--active {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,150 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<!-- Header Section -->
|
<!-- Hero Section -->
|
||||||
<v-row class="mb-8">
|
<v-row class="mb-6">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-card elevation="4" class="rounded-lg shadow-lg">
|
<v-card class="bg-primary rounded-xl">
|
||||||
<v-card-text class="text-center pa-6">
|
<v-card-text class="text-center pa-8">
|
||||||
<h1 class="text-h3 mb-4 text-primary">Bem-vindo ao TARS</h1>
|
<h1 class="text-h2 font-weight-bold text-white mb-4">
|
||||||
<p class="text-subtitle-1 text-body-2">
|
Centro de Controle TARS
|
||||||
Sistema de gerenciamento inteligente para monitoramento por câmeras
|
</h1>
|
||||||
|
<p class="text-h6 text-white font-weight-regular">
|
||||||
|
Plataforma avançada de monitoramento e análise de segurança
|
||||||
</p>
|
</p>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- Features Grid -->
|
<!-- Quick Stats -->
|
||||||
<v-row>
|
<v-row class="mb-6">
|
||||||
<!-- Modelos Section -->
|
<v-col v-for="stat in quickStats" :key="stat.title" cols="12" sm="6" md="3">
|
||||||
<v-col cols="12" md="6" lg="4">
|
<v-card :color="stat.color" variant="tonal" class="rounded-lg h-100">
|
||||||
<v-card elevation="3" class="rounded-lg">
|
|
||||||
<v-card-title class="d-flex align-center">
|
|
||||||
<v-icon icon="mdi-camera-iris" class="mr-2" />
|
|
||||||
Modelos
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<p class="mb-3">Gerencie modelos de detecção:</p>
|
<div class="d-flex flex-column align-center text-center pa-4">
|
||||||
<ul class="pl-4">
|
<v-icon :icon="stat.icon" size="48" class="mb-4" />
|
||||||
<li>Detecção de EPI</li>
|
<div class="text-h3 font-weight-bold mb-2">{{ stat.value }}</div>
|
||||||
<li>Contagem de pessoas</li>
|
<div class="text-subtitle-1">{{ stat.title }}</div>
|
||||||
<li>Detecção de invasão</li>
|
</div>
|
||||||
<li>Análise de comportamento</li>
|
|
||||||
</ul>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions class="d-flex justify-end">
|
|
||||||
<v-btn
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
:to="{ name: 'models' }"
|
|
||||||
>
|
|
||||||
Ver Modelos
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<!-- Relatórios Section -->
|
|
||||||
<v-col cols="12" md="6" lg="4">
|
|
||||||
<v-card elevation="3" class="rounded-lg">
|
|
||||||
<v-card-title class="d-flex align-center">
|
|
||||||
<v-icon icon="mdi-chart-bar" class="mr-2" />
|
|
||||||
Relatórios
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<p class="mb-3">Acesse relatórios detalhados:</p>
|
|
||||||
<ul class="pl-4">
|
|
||||||
<li>Estatísticas de detecção</li>
|
|
||||||
<li>Alertas gerados</li>
|
|
||||||
<li>Performance dos modelos</li>
|
|
||||||
<li>Histórico de eventos</li>
|
|
||||||
</ul>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions class="d-flex justify-end">
|
|
||||||
<v-btn
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
:to="{ name: 'reports' }"
|
|
||||||
>
|
|
||||||
Ver Relatórios
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<!-- Usuários Section -->
|
|
||||||
<v-col cols="12" md="6" lg="4">
|
|
||||||
<v-card elevation="3" class="rounded-lg">
|
|
||||||
<v-card-title class="d-flex align-center">
|
|
||||||
<v-icon icon="mdi-account-group" class="mr-2" />
|
|
||||||
Usuários
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<p class="mb-3">Gerencie usuários do sistema:</p>
|
|
||||||
<ul class="pl-4">
|
|
||||||
<li>Controle de acesso</li>
|
|
||||||
<li>Permissões por função</li>
|
|
||||||
<li>Histórico de atividades</li>
|
|
||||||
<li>Notificações</li>
|
|
||||||
</ul>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions class="d-flex justify-end">
|
|
||||||
<v-btn
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
:to="{ name: 'users' }"
|
|
||||||
>
|
|
||||||
Gerenciar Usuários
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- Status Section -->
|
<!-- Main Features -->
|
||||||
<v-row class="mt-8">
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-card elevation="3" class="rounded-lg">
|
|
||||||
<v-card-title class="d-flex align-center">
|
|
||||||
<v-icon icon="mdi-information" class="mr-2" />
|
|
||||||
Status do Sistema
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" sm="6" md="3">
|
<v-col v-for="feature in features" :key="feature.title" cols="12" md="4">
|
||||||
<v-stat-card
|
<v-card height="100%" class="rounded-lg">
|
||||||
title="Câmeras Ativas"
|
<v-card-text class="pa-6">
|
||||||
:value="camerasAtivas"
|
<div class="d-flex align-center mb-4">
|
||||||
icon="mdi-camera"
|
<v-icon :icon="feature.icon" size="36" :color="feature.color" class="mr-4" />
|
||||||
color="success"
|
<h2 class="text-h5 font-weight-bold">{{ feature.title }}</h2>
|
||||||
/>
|
</div>
|
||||||
</v-col>
|
<p class="text-body-1 mb-4">{{ feature.description }}</p>
|
||||||
<v-col cols="12" sm="6" md="3">
|
<v-list>
|
||||||
<v-stat-card
|
<v-list-item v-for="item in feature.items" :key="item" class="px-0">
|
||||||
title="Modelos em Uso"
|
<template v-slot:prepend>
|
||||||
:value="modelosAtivos"
|
<v-icon icon="mdi-check-circle" color="success" size="small" />
|
||||||
icon="mdi-brain"
|
</template>
|
||||||
color="primary"
|
{{ item }}
|
||||||
/>
|
</v-list-item>
|
||||||
</v-col>
|
</v-list>
|
||||||
<v-col cols="12" sm="6" md="3">
|
|
||||||
<v-stat-card
|
|
||||||
title="Alertas Hoje"
|
|
||||||
:value="alertasHoje"
|
|
||||||
icon="mdi-alert"
|
|
||||||
color="warning"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" sm="6" md="3">
|
|
||||||
<v-stat-card
|
|
||||||
title="Usuários Online"
|
|
||||||
:value="usuariosOnline"
|
|
||||||
icon="mdi-account-check"
|
|
||||||
color="info"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
<v-card-actions class="pa-6 pt-0">
|
||||||
|
<v-btn
|
||||||
|
:color="feature.color"
|
||||||
|
variant="tonal"
|
||||||
|
:to="feature.route"
|
||||||
|
block
|
||||||
|
class="text-none"
|
||||||
|
>
|
||||||
|
{{ feature.actionText }}
|
||||||
|
<v-icon icon="mdi-chevron-right" end />
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
@ -152,71 +69,96 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Componente reutilizável para estatísticas
|
|
||||||
const VStatCard = {
|
|
||||||
props: {
|
|
||||||
title: String,
|
|
||||||
value: [Number, String],
|
|
||||||
icon: String,
|
|
||||||
color: String
|
|
||||||
},
|
|
||||||
template: `
|
|
||||||
<v-card :color="color" variant="tonal" class="rounded-lg mb-4">
|
|
||||||
<v-card-text>
|
|
||||||
<div class="d-flex align-center mb-2">
|
|
||||||
<v-icon :icon="icon" size="24" class="mr-2" />
|
|
||||||
<span class="text-subtitle-2">{{ title }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-h4 font-weight-bold">{{ value }}</div>
|
|
||||||
</v-card-text>
|
|
||||||
</v-card>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TheWelcome',
|
name: 'WelcomeDashboard',
|
||||||
|
|
||||||
components: {
|
|
||||||
VStatCard
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
camerasAtivas: 8,
|
quickStats: [
|
||||||
modelosAtivos: 4,
|
{
|
||||||
alertasHoje: 12,
|
title: 'Câmeras Ativas',
|
||||||
usuariosOnline: 5
|
value: 12,
|
||||||
|
icon: 'mdi-cctv',
|
||||||
|
color: 'success'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Modelos AI',
|
||||||
|
value: 6,
|
||||||
|
icon: 'mdi-brain',
|
||||||
|
color: 'primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Alertas 24h',
|
||||||
|
value: 18,
|
||||||
|
icon: 'mdi-bell',
|
||||||
|
color: 'warning'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Usuários',
|
||||||
|
value: 24,
|
||||||
|
icon: 'mdi-account-group',
|
||||||
|
color: 'info'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
features: [
|
||||||
|
{
|
||||||
|
title: 'Monitoramento Inteligente',
|
||||||
|
description: 'Sistema avançado de vigilância com análise em tempo real',
|
||||||
|
icon: 'mdi-security',
|
||||||
|
color: 'primary',
|
||||||
|
route: { name: 'monitoring' },
|
||||||
|
actionText: 'Acessar Monitoramento',
|
||||||
|
items: [
|
||||||
|
'Detecção de EPI em tempo real',
|
||||||
|
'Análise de fluxo de pessoas',
|
||||||
|
'Detecção de invasão de área',
|
||||||
|
'Reconhecimento de comportamentos'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Analytics & Relatórios',
|
||||||
|
description: 'Análise detalhada e geração de insights automáticos',
|
||||||
|
icon: 'mdi-chart-box',
|
||||||
|
color: 'success',
|
||||||
|
route: { name: 'analytics' },
|
||||||
|
actionText: 'Ver Analytics',
|
||||||
|
items: [
|
||||||
|
'Dashboards personalizados',
|
||||||
|
'Relatórios automatizados',
|
||||||
|
'Exportação de dados',
|
||||||
|
'Métricas de performance'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Gestão & Configuração',
|
||||||
|
description: 'Controle total sobre o sistema e suas funcionalidades',
|
||||||
|
icon: 'mdi-cog',
|
||||||
|
color: 'info',
|
||||||
|
route: { name: 'settings' },
|
||||||
|
actionText: 'Configurar Sistema',
|
||||||
|
items: [
|
||||||
|
'Gerenciamento de usuários',
|
||||||
|
'Configuração de câmeras',
|
||||||
|
'Ajuste de modelos AI',
|
||||||
|
'Políticas de segurança'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.v-card-text ul {
|
.v-card {
|
||||||
padding-left: 20px;
|
transition: transform 0.2s;
|
||||||
margin-top: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-card-text li {
|
.v-card:hover {
|
||||||
margin-bottom: 8px;
|
transform: translateY(-4px);
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-btn {
|
.v-list-item {
|
||||||
text-transform: none;
|
min-height: 40px;
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-row {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.v-col {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -10,8 +10,9 @@ import ForgotPassword from '@/views/ForgotPassword.vue';
|
|||||||
import RegisterView from '@/views/RegisterView.vue';
|
import RegisterView from '@/views/RegisterView.vue';
|
||||||
import TrainingView from '@/views/TrainingView.vue';
|
import TrainingView from '@/views/TrainingView.vue';
|
||||||
import SSOView from '@/views/SSOView.vue';
|
import SSOView from '@/views/SSOView.vue';
|
||||||
import UserProfileView from '@/views/UserProfileView.vue'
|
import UserProfileView from '@/views/UserProfileView.vue';
|
||||||
import EditUserView from '@/views/EditUserView.vue'
|
import EditUserView from '@/views/EditUserView.vue';
|
||||||
|
import RegisterUserCamView from '@/views/RegisterUserCamView.vue';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@ -104,6 +105,12 @@ const routes = [
|
|||||||
requiresAuth: false
|
requiresAuth: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/register-user-cam',
|
||||||
|
name: 'register-user-cam',
|
||||||
|
component: RegisterUserCamView,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
// Catch-all route for unmatched paths
|
// Catch-all route for unmatched paths
|
||||||
{
|
{
|
||||||
path: '/:pathMatch(.*)*',
|
path: '/:pathMatch(.*)*',
|
||||||
|
|||||||
@ -1,54 +1,423 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="reports-view">
|
<div class="reports-view">
|
||||||
<v-container>
|
<v-container fluid>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title>
|
<!-- Cabeçalho do Card -->
|
||||||
Relatórios de Horários
|
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||||
<v-spacer></v-spacer>
|
<div class="d-flex align-center">
|
||||||
<v-text-field
|
<v-icon size="24" class="mr-2">mdi-clock-outline</v-icon>
|
||||||
v-model="search"
|
<span class="text-h6">Relatórios de Horários</span>
|
||||||
label="Buscar por Nome ou ID"
|
</div>
|
||||||
append-icon="mdi-magnify"
|
<div class="d-flex gap-2">
|
||||||
hide-details
|
<v-btn
|
||||||
density="compact"
|
color="primary"
|
||||||
></v-text-field>
|
prepend-icon="mdi-filter"
|
||||||
|
@click="showFilters = !showFilters"
|
||||||
|
variant="tonal"
|
||||||
|
>
|
||||||
|
{{ showFilters ? 'Ocultar Filtros' : 'Mostrar Filtros' }}
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
prepend-icon="mdi-file-export"
|
||||||
|
@click="exportToCSV"
|
||||||
|
:loading="isExporting"
|
||||||
|
>
|
||||||
|
Exportar CSV
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
|
<!-- Seção de Filtros -->
|
||||||
|
<v-expand-transition>
|
||||||
|
<div v-if="showFilters">
|
||||||
|
<v-card-text class="pt-2">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-text-field
|
||||||
|
v-model="filters.search"
|
||||||
|
label="Buscar por Nome ou ID"
|
||||||
|
prepend-icon="mdi-magnify"
|
||||||
|
hide-details
|
||||||
|
density="comfortable"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="applyFilters"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-select
|
||||||
|
v-model="filters.timeRange"
|
||||||
|
:items="timeRanges"
|
||||||
|
label="Período"
|
||||||
|
prepend-icon="mdi-calendar"
|
||||||
|
hide-details
|
||||||
|
density="comfortable"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="4">
|
||||||
|
<v-select
|
||||||
|
v-model="filters.status"
|
||||||
|
:items="statusOptions"
|
||||||
|
label="Status"
|
||||||
|
prepend-icon="mdi-check-circle"
|
||||||
|
hide-details
|
||||||
|
density="comfortable"
|
||||||
|
multiple
|
||||||
|
chips
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
</div>
|
||||||
|
</v-expand-transition>
|
||||||
|
|
||||||
|
<v-divider />
|
||||||
|
|
||||||
|
<!-- Tabela de Dados -->
|
||||||
<v-data-table
|
<v-data-table
|
||||||
|
v-model:page="currentPage"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
:items="reports"
|
:items="filteredReports"
|
||||||
:search="search"
|
:loading="isLoading"
|
||||||
|
:items-per-page="itemsPerPage"
|
||||||
|
:search="filters.search"
|
||||||
item-value="id"
|
item-value="id"
|
||||||
class="elevation-1"
|
hover
|
||||||
></v-data-table>
|
density="comfortable"
|
||||||
|
class="elevation-0"
|
||||||
|
>
|
||||||
|
<!-- Slot para Status -->
|
||||||
|
<template v-slot:item_status="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(item.raw.status)"
|
||||||
|
size="small"
|
||||||
|
class="text-caption"
|
||||||
|
>
|
||||||
|
{{ item.raw.status }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Slot para Ações -->
|
||||||
|
<template v-slot:item_actions="{ item }">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<v-tooltip text="Visualizar Detalhes">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-eye"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
v-bind="props"
|
||||||
|
@click="viewDetails(item.raw)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
<v-tooltip text="Editar Registro">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
v-bind="props"
|
||||||
|
@click="editReport(item.raw)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Loading e Estado Vazio -->
|
||||||
|
<template v-slot:loading>
|
||||||
|
<v-skeleton-loader
|
||||||
|
type="table-row"
|
||||||
|
:loading="true"
|
||||||
|
class="pa-4"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:no-data>
|
||||||
|
<div class="d-flex flex-column align-center pa-4">
|
||||||
|
<v-icon size="48" color="grey">mdi-alert-circle-outline</v-icon>
|
||||||
|
<span class="text-grey mt-2">Nenhum registro encontrado</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<!-- Modal de Detalhes -->
|
||||||
|
<v-dialog v-model="detailsDialog" max-width="600">
|
||||||
|
<v-card v-if="selectedReport">
|
||||||
|
<v-card-title class="d-flex justify-space-between align-center pa-4">
|
||||||
|
Detalhes do Registro
|
||||||
|
<v-btn icon="mdi-close" variant="text" @click="detailsDialog = false" />
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-account</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ selectedReport.name }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>Nome do Funcionário</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-clock-in</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ selectedReport.entryTime }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>Horário de Entrada</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-clock-out</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>{{ selectedReport.exitTime }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>Horário de Saída</v-list-item-subtitle>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: "ReportsView",
|
name: 'ReportsView',
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
search: "",
|
// Estados de UI
|
||||||
headers: [
|
isLoading: false,
|
||||||
{ text: "ID", value: "id" },
|
isExporting: false,
|
||||||
{ text: "Nome", value: "name" },
|
showFilters: false,
|
||||||
{ text: "Horário de Entrada", value: "entryTime" },
|
detailsDialog: false,
|
||||||
{ text: "Horário de Saída", value: "exitTime" },
|
currentPage: 1,
|
||||||
],
|
itemsPerPage: 10,
|
||||||
reports: [
|
selectedReport: null,
|
||||||
{ id: 1, name: "João Silva", entryTime: "08:00", exitTime: "16:00" },
|
|
||||||
{ id: 2, name: "Maria Oliveira", entryTime: "09:00", exitTime: "18:00" },
|
|
||||||
|
|
||||||
],
|
// Filtros
|
||||||
};
|
filters: {
|
||||||
|
search: '',
|
||||||
|
timeRange: 'today',
|
||||||
|
status: [],
|
||||||
},
|
},
|
||||||
};
|
|
||||||
|
// Opções de Filtro
|
||||||
|
timeRanges: [
|
||||||
|
{ title: 'Hoje', value: 'today' },
|
||||||
|
{ title: 'Última Semana', value: 'week' },
|
||||||
|
{ title: 'Último Mês', value: 'month' },
|
||||||
|
],
|
||||||
|
statusOptions: [
|
||||||
|
{ title: 'Regular', value: 'regular' },
|
||||||
|
{ title: 'Atrasado', value: 'late' },
|
||||||
|
{ title: 'Saída Antecipada', value: 'early' },
|
||||||
|
],
|
||||||
|
|
||||||
|
// Configuração da Tabela
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
key: 'id',
|
||||||
|
align: 'start',
|
||||||
|
sortable: true,
|
||||||
|
width: '80'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Nome',
|
||||||
|
key: 'name',
|
||||||
|
align: 'start',
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Entrada',
|
||||||
|
key: 'entryTime',
|
||||||
|
align: 'center',
|
||||||
|
sortable: true,
|
||||||
|
width: '120'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Saída',
|
||||||
|
key: 'exitTime',
|
||||||
|
align: 'center',
|
||||||
|
sortable: true,
|
||||||
|
width: '120'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Status',
|
||||||
|
key: 'status',
|
||||||
|
align: 'center',
|
||||||
|
sortable: true,
|
||||||
|
width: '120'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Ações',
|
||||||
|
key: 'actions',
|
||||||
|
align: 'center',
|
||||||
|
sortable: false,
|
||||||
|
width: '100'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Dados Mockados (substituir por API)
|
||||||
|
reports: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'João Silva',
|
||||||
|
entryTime: '08:00',
|
||||||
|
exitTime: '17:00',
|
||||||
|
status: 'regular'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Maria Oliveira',
|
||||||
|
entryTime: '09:15',
|
||||||
|
exitTime: '18:00',
|
||||||
|
status: 'late'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Pedro Santos',
|
||||||
|
entryTime: '08:00',
|
||||||
|
exitTime: '16:30',
|
||||||
|
status: 'early'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
filteredReports() {
|
||||||
|
let filtered = [...this.reports]
|
||||||
|
|
||||||
|
// Filtra por status se selecionado
|
||||||
|
if (this.filters.status.length > 0) {
|
||||||
|
filtered = filtered.filter(report =>
|
||||||
|
this.filters.status.includes(report.status)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtra por período
|
||||||
|
filtered = this.filterByTimeRange(filtered)
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
// Métodos de Filtro
|
||||||
|
filterByTimeRange(reports) {
|
||||||
|
// Implementar lógica de filtro por período
|
||||||
|
return reports
|
||||||
|
},
|
||||||
|
|
||||||
|
applyFilters() {
|
||||||
|
// Implementar lógica adicional de filtros se necessário
|
||||||
|
this.isLoading = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isLoading = false
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Métodos de UI
|
||||||
|
getStatusColor(status) {
|
||||||
|
const colors = {
|
||||||
|
regular: 'success',
|
||||||
|
late: 'error',
|
||||||
|
early: 'warning'
|
||||||
|
}
|
||||||
|
return colors[status] || 'grey'
|
||||||
|
},
|
||||||
|
|
||||||
|
viewDetails(report) {
|
||||||
|
this.selectedReport = report
|
||||||
|
this.detailsDialog = true
|
||||||
|
},
|
||||||
|
|
||||||
|
editReport(report) {
|
||||||
|
// Implementar lógica de edição
|
||||||
|
console.log('Editar relatório:', report.id)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Exportação
|
||||||
|
async exportToCSV() {
|
||||||
|
try {
|
||||||
|
this.isExporting = true
|
||||||
|
|
||||||
|
const headers = this.headers
|
||||||
|
.filter(h => h.key !== 'actions')
|
||||||
|
.map(h => h.title)
|
||||||
|
|
||||||
|
const data = this.filteredReports.map(report =>
|
||||||
|
headers.map(header =>
|
||||||
|
report[header.toLowerCase()] || ''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...data.map(row => row.join(','))
|
||||||
|
].join('\n')
|
||||||
|
|
||||||
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = URL.createObjectURL(blob)
|
||||||
|
link.download = `relatorio_horarios_${new Date().toISOString().split('T')[0]}.csv`
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
this.$notify({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Relatório exportado com sucesso!'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao exportar:', error)
|
||||||
|
this.$notify({
|
||||||
|
type: 'error',
|
||||||
|
text: 'Erro ao exportar relatório. Tente novamente.'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
this.isExporting = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Lifecycle Hooks
|
||||||
|
async created() {
|
||||||
|
try {
|
||||||
|
this.isLoading = true
|
||||||
|
// Implementar chamada à API
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar dados:', error)
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.reports-view {
|
.reports-view {
|
||||||
padding: 16px;
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table :deep(th) {
|
||||||
|
font-weight: 600 !important;
|
||||||
|
background-color: rgb(var(--v-theme-surface)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table :deep(tr:hover) {
|
||||||
|
background-color: rgb(var(--v-theme-surface-variant)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animações */
|
||||||
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -3,51 +3,49 @@
|
|||||||
<v-container>
|
<v-container>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-tabs v-model="activeTab">
|
<v-tabs v-model="activeTab">
|
||||||
<v-tab>Cadastro</v-tab>
|
|
||||||
<v-tab>Pesquisar</v-tab>
|
<v-tab>Pesquisar</v-tab>
|
||||||
<v-tab>Treino</v-tab>
|
<v-tab>Treino</v-tab>
|
||||||
<v-tab>Lista de Nomes</v-tab>
|
<v-tab>Lista de Nomes</v-tab>
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<!-- Cadastro Tab -->
|
|
||||||
<v-form v-if="activeTab === 0" @submit.prevent="registerCameraModel">
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12" md="4">
|
|
||||||
<v-text-field v-model="cameraModel.name" label="Nome do Modelo" required></v-text-field>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="4">
|
|
||||||
<v-text-field v-model="cameraModel.responsible" label="Responsável pelo Cadastro" required></v-text-field>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="6">
|
|
||||||
<v-text-field v-model="cameraModel.description" label="Descrição" required></v-text-field>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-text-field type="date" v-model="cameraModel.creationDate" label="Data de Criação" required></v-text-field>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12" md="3">
|
|
||||||
<v-text-field type="time" v-model="cameraModel.creationTime" label="Hora de Criação" required></v-text-field>
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-btn type="submit" color="primary">Cadastrar Modelo</v-btn>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-form>
|
|
||||||
|
|
||||||
<!-- Pesquisar Tab -->
|
<!-- Pesquisar Tab -->
|
||||||
<v-form v-if="activeTab === 1" @submit.prevent="searchCameraModels">
|
<v-form v-if="activeTab === 0" @submit.prevent="searchCameraModels">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field v-model="searchCriteria.name" label="Nome do Modelo"></v-text-field>
|
<v-text-field v-model="searchCriteria.name" label="Nome do Modelo"></v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field v-model="searchCriteria.id" label="ID"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select v-model="searchCriteria.status" :items="['Ativo', 'Inativo']" label="Status"></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select v-model="searchCriteria.order" :items="['A-Z', 'Z-A']" label="Ordenar"></v-select>
|
||||||
|
</v-col>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-btn type="submit" color="primary">Pesquisar</v-btn>
|
<v-btn type="submit" color="primary">Pesquisar</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-form>
|
</v-form>
|
||||||
|
|
||||||
|
<v-list v-if="searchResults.length">
|
||||||
|
<v-list-item-group>
|
||||||
|
<v-list-item v-for="(camera, index) in searchResults" :key="index">
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-title>{{ camera.name }}</v-list-item-title>
|
||||||
|
<v-list-item-subtitle>{{ camera.id }}</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>{{ camera.registrationDate }}</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>{{ camera.isActive ? 'Ativo' : 'Inativo' }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list-item-group>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
<!-- Treino Tab -->
|
<!-- Treino Tab -->
|
||||||
<v-row v-if="activeTab === 2">
|
<v-row v-if="activeTab === 1">
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-card outlined>
|
<v-card outlined>
|
||||||
<v-card-title>Treinamento de Modelo de Câmera</v-card-title>
|
<v-card-title>Treinamento de Modelo de Câmera</v-card-title>
|
||||||
@ -80,27 +78,50 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
<v-progress-linear
|
||||||
|
v-if="isTraining"
|
||||||
|
:value="trainingProgress"
|
||||||
|
color="primary"
|
||||||
|
height="20"
|
||||||
|
></v-progress-linear>
|
||||||
|
<v-row v-if="trainingMessage">
|
||||||
|
<v-col>{{ trainingMessage }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="trainingComplete">
|
||||||
|
<v-col>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>Informações da Câmera</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<p><strong>Nome:</strong> {{ selectedCameraForTraining.name }}</p>
|
||||||
|
<p><strong>Status do Treinamento:</strong> Concluído</p>
|
||||||
|
<p><strong>Status da Integração:</strong> {{ integrationStatus }}</p>
|
||||||
|
<p><strong>Tempo de Treinamento:</strong> 5 minutos</p>
|
||||||
|
<p><strong>Tempo de Integração:</strong> 3 minutos</p>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- Lista de Nomes Tab -->
|
<!-- Lista de Nomes Tab -->
|
||||||
<v-row v-if="activeTab === 3" class="mt-4">
|
<v-row v-if="activeTab === 2" class="mt-4">
|
||||||
<v-col md="6" sm="12">
|
<v-col md="6" sm="12">
|
||||||
<v-card outlined>
|
<v-card outlined>
|
||||||
<v-card-title class="subtitle-1">Rostos Identificados</v-card-title>
|
<v-card-title class="subtitle-1">Rostos Identificados</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
<v-select v-model="selectedFaceStatus" :items="['Associado', 'Não Associado']" label="Filtrar Rostos"></v-select>
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-list-item v-for="(face, index) in identifiedFaces" :key="index">
|
<v-list-item v-for="(face, index) in filteredFaces" :key="index">
|
||||||
<v-list-item-avatar>
|
<v-list-item-avatar>
|
||||||
<v-icon large>mdi-account-circle</v-icon>
|
<v-icon large>mdi-account-circle</v-icon>
|
||||||
</v-list-item-avatar>
|
</v-list-item-avatar>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
{{ face.name }}
|
<v-list-item-title>{{ face.name }}</v-list-item-title>
|
||||||
<v-chip small :color="face.isActive ? 'success' : 'error'">
|
<v-list-item-subtitle>{{ face.isActive ? 'Associado' : 'Não Associado' }}</v-list-item-subtitle>
|
||||||
{{ face.isActive ? 'Ativo' : 'Inativo' }}
|
<v-btn v-if="!face.isActive" @click="editFaceName(face)">Associar</v-btn>
|
||||||
</v-chip>
|
|
||||||
</v-list-item-content>
|
</v-list-item-content>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
@ -108,6 +129,7 @@
|
|||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
@ -119,15 +141,11 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
cameraModel: {
|
|
||||||
name: '',
|
|
||||||
id: '',
|
|
||||||
description: '',
|
|
||||||
registrationDate: null
|
|
||||||
},
|
|
||||||
searchCriteria: {
|
searchCriteria: {
|
||||||
id: '',
|
id: '',
|
||||||
name: ''
|
name: '',
|
||||||
|
status: '',
|
||||||
|
order: ''
|
||||||
},
|
},
|
||||||
selectedCameraForTraining: null,
|
selectedCameraForTraining: null,
|
||||||
isTraining: false,
|
isTraining: false,
|
||||||
@ -138,45 +156,35 @@ export default {
|
|||||||
{ id: 'CAM003', name: 'Câmera Estacionamento', brand: 'Dahua' }
|
{ id: 'CAM003', name: 'Câmera Estacionamento', brand: 'Dahua' }
|
||||||
],
|
],
|
||||||
cameras: [
|
cameras: [
|
||||||
{ id: 'CAM001', name: 'Câmera Principal', description: 'Câmera frontal do prédio', isActive: true, registrationDate: '2025-02-03T08:30', isTrained: true },
|
{ id: 'CAM001', name: 'Câmera Principal', description: 'Câmera frontal do prédio', isActive: true, registrationDate: '2025-02-03T 08:30', isTrained: true },
|
||||||
{ id: 'CAM002', name: 'Câmera Corredor', description: 'Câmera do corredor principal', isActive: false, registrationDate: '2025-01-28T14:00', isTrained: false }
|
{ id: 'CAM002', name: 'Câmera Corredor', description: 'Câmera do corredor principal', isActive: false, registrationDate: '2025-01-28T 14:00', isTrained: false }
|
||||||
],
|
],
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
|
selectedFaceStatus: 'Associado',
|
||||||
identifiedFaces: [
|
identifiedFaces: [
|
||||||
{
|
{ id: 'USER001', name: 'João Silva', isActive: true },
|
||||||
id: 'USER001',
|
{ id: 'USER002', name: 'Maria Souza', isActive: true }
|
||||||
name: 'João Silva',
|
|
||||||
isActive: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'USER002',
|
|
||||||
name: 'Maria Souza',
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
unidentifiedFaces: [
|
unidentifiedFaces: [
|
||||||
{ selectedUser: null },
|
{ selectedUser: null, name: 'Desconhecido' },
|
||||||
{ selectedUser: null }
|
{ selectedUser: null, name: 'Desconhecido' }
|
||||||
],
|
],
|
||||||
registeredUsers: ['João Silva', 'Maria Souza', 'Pedro Santos', 'Ana Oliveira']
|
registeredUsers: ['João Silva', 'Maria Souza', 'Pedro Santos', 'Ana Oliveira'],
|
||||||
|
integrationStatus: 'Pendente',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
registerCameraModel() {
|
|
||||||
console.log('Registrando modelo de câmera:', this.cameraModel);
|
|
||||||
this.cameraModel = {
|
|
||||||
name: '',
|
|
||||||
id: '',
|
|
||||||
description: '',
|
|
||||||
registrationDate: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
searchCameraModels() {
|
searchCameraModels() {
|
||||||
console.log('Critérios de pesquisa:', this.searchCriteria);
|
|
||||||
this.searchResults = this.cameras.filter(camera =>
|
this.searchResults = this.cameras.filter(camera =>
|
||||||
(camera.id.includes(this.searchCriteria.id) || camera.name.includes(this.searchCriteria.name)) &&
|
(camera.id.includes(this.searchCriteria.id) || camera.name.includes(this.searchCriteria.name)) &&
|
||||||
(!this.searchCriteria.status || (this.searchCriteria.status === 'Ativo' && camera.isActive) || (this.searchCriteria.status === 'Inativo' && !camera.isActive))
|
(!this.searchCriteria.status || (this.searchCriteria.status === 'Ativo' && camera.isActive) || (this.searchCriteria.status === 'Inativo' && !camera.isActive))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.searchCriteria.order === 'A-Z') {
|
||||||
|
this.searchResults.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
} else if (this.searchCriteria.order === 'Z-A') {
|
||||||
|
this.searchResults.sort((a, b) => b.name.localeCompare(a.name));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
startTraining() {
|
startTraining() {
|
||||||
if (this.selectedCameraForTraining) {
|
if (this.selectedCameraForTraining) {
|
||||||
@ -210,15 +218,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
integrateModel() {
|
integrateModel() {
|
||||||
console.log('Integrando modelo:', this.selectedCameraForTraining);
|
this.integrationStatus = 'Em andamento...';
|
||||||
this.trainingComplete = false;
|
setTimeout(() => {
|
||||||
this.isTraining = false;
|
this.integrationStatus = 'Concluído';
|
||||||
},
|
}, 3000);
|
||||||
confirmAssignments() {
|
|
||||||
console.log('Atribuições confirmadas:', this.unidentifiedFaces);
|
|
||||||
},
|
},
|
||||||
editFaceName(face) {
|
editFaceName(face) {
|
||||||
console.log('Editando rosto:', face);
|
console.log('Editando rosto:', face);
|
||||||
|
face.isActive = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filteredFaces() {
|
||||||
|
return this.selectedFaceStatus === 'Associado' ?
|
||||||
|
this.identifiedFaces.filter(face => face.isActive) :
|
||||||
|
this.unidentifiedFaces.filter(face => !face.isActive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,144 +1,501 @@
|
|||||||
|
<!-- ProfilePage.vue -->
|
||||||
<template>
|
<template>
|
||||||
<v-container>
|
<div class="profile-container">
|
||||||
<v-card class="mx-auto profile-card" max-width="800">
|
<div class="profile-header">
|
||||||
<v-row no-gutters>
|
<h1 class="profile-title">Perfil do Usuário</h1>
|
||||||
<!-- Seção da foto do perfil -->
|
<p class="profile-subtitle">Gerencie suas informações pessoais</p>
|
||||||
<v-col cols="12" md="4" class="pa-5 text-center">
|
|
||||||
<v-avatar size="180">
|
|
||||||
<v-img
|
|
||||||
:src="user.avatar || 'https://cdn.vuetifyjs.com/images/john.jpg'"
|
|
||||||
:alt="user.name"
|
|
||||||
/>
|
|
||||||
</v-avatar>
|
|
||||||
</v-col>
|
|
||||||
|
|
||||||
<!-- Seção das informações -->
|
|
||||||
<v-col cols="12" md="8" class="pa-5">
|
|
||||||
<div class="d-flex justify-space-between align-center mb-6">
|
|
||||||
<h2 class="text-h4">Perfil do Usuário</h2>
|
|
||||||
<v-btn
|
|
||||||
color="#2563eb"
|
|
||||||
dark
|
|
||||||
rounded
|
|
||||||
@click="navigateToEdit"
|
|
||||||
>
|
|
||||||
<v-icon left>mdi-pencil</v-icon>
|
|
||||||
Editar Perfil
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-list>
|
<form @submit.prevent="saveProfile" class="profile-form">
|
||||||
<v-list-item>
|
<div class="profile-grid">
|
||||||
<v-list-item-icon>
|
<!-- Coluna da Foto -->
|
||||||
<v-icon color="#2563eb">mdi-account</v-icon>
|
<div class="photo-section">
|
||||||
</v-list-item-icon>
|
<div class="profile-photo-container">
|
||||||
<v-list-item-content>
|
<div class="photo-wrapper">
|
||||||
<v-list-item-subtitle>Nome</v-list-item-subtitle>
|
<img :src="profileData.photoUrl || defaultPhoto" alt="Foto de Perfil" class="profile-photo">
|
||||||
<v-list-item-title class="text-h6">
|
<div class="photo-overlay">
|
||||||
{{ user.name }}
|
<span class="photo-text">Alterar foto</span>
|
||||||
</v-list-item-title>
|
</div>
|
||||||
</v-list-item-content>
|
</div>
|
||||||
</v-list-item>
|
<input
|
||||||
|
type="file"
|
||||||
|
@change="handlePhotoUpload"
|
||||||
|
accept="image/*"
|
||||||
|
ref="photoInput"
|
||||||
|
class="photo-input"
|
||||||
|
>
|
||||||
|
<button type="button" @click="triggerPhotoUpload" class="upload-btn">
|
||||||
|
<i class="fas fa-camera"></i>
|
||||||
|
Alterar Foto
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<v-list-item>
|
<!-- Coluna das Informações -->
|
||||||
<v-list-item-icon>
|
<div class="info-section">
|
||||||
<v-icon color="#2563eb">mdi-email</v-icon>
|
<!-- Seção de Informações Básicas -->
|
||||||
</v-list-item-icon>
|
<div class="section-title">
|
||||||
<v-list-item-content>
|
<i class="fas fa-user"></i>
|
||||||
<v-list-item-subtitle>Email</v-list-item-subtitle>
|
<h2 class="sub-title">Informações Básicas</h2>
|
||||||
<v-list-item-title class="text-h6">
|
</div>
|
||||||
{{ user.email }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item>
|
<div class="form-row">
|
||||||
<v-list-item-icon>
|
<div class="form-group">
|
||||||
<v-icon color="#2563eb">mdi-phone</v-icon>
|
<label>ID</label>
|
||||||
</v-list-item-icon>
|
<input type="text" v-model="profileData.id" readonly class="form-input readonly">
|
||||||
<v-list-item-content>
|
</div>
|
||||||
<v-list-item-subtitle>Telefone</v-list-item-subtitle>
|
</div>
|
||||||
<v-list-item-title class="text-h6">
|
|
||||||
{{ user.phone || 'Não informado' }}
|
|
||||||
</v-list-item-title>
|
|
||||||
</v-list-item-content>
|
|
||||||
</v-list-item>
|
|
||||||
|
|
||||||
<v-list-item>
|
<div class="form-row">
|
||||||
<v-list-item-icon>
|
<div class="form-group">
|
||||||
<v-icon color="#2563eb">mdi-badge-account</v-icon>
|
<label>Nome</label>
|
||||||
</v-list-item-icon>
|
<input
|
||||||
<v-list-item-content>
|
type="text"
|
||||||
<v-list-item-subtitle>Cargo</v-list-item-subtitle>
|
v-model="profileData.firstName"
|
||||||
<v-list-item-title class="text-h6">
|
required
|
||||||
{{ user.role || 'Não informado' }}
|
class="form-input"
|
||||||
</v-list-item-title>
|
placeholder="Digite seu nome"
|
||||||
</v-list-item-content>
|
>
|
||||||
</v-list-item>
|
</div>
|
||||||
|
|
||||||
<v-list-item>
|
<div class="form-group">
|
||||||
<v-list-item-icon>
|
<label>Sobrenome</label>
|
||||||
<v-icon color="#2563eb">mdi-office-building</v-icon>
|
<input
|
||||||
</v-list-item-icon>
|
type="text"
|
||||||
<v-list-item-content>
|
v-model="profileData.lastName"
|
||||||
<v-list-item-subtitle>Departamento</v-list-item-subtitle>
|
required
|
||||||
<v-list-item-title class="text-h6">
|
class="form-input"
|
||||||
{{ user.department || 'Não informado' }}
|
placeholder="Digite seu sobrenome"
|
||||||
</v-list-item-title>
|
>
|
||||||
</v-list-item-content>
|
</div>
|
||||||
</v-list-item>
|
</div>
|
||||||
</v-list>
|
|
||||||
</v-col>
|
<!-- Seção de Contato -->
|
||||||
</v-row>
|
<div class="section-title">
|
||||||
</v-card>
|
<i class="fas fa-address-card"></i>
|
||||||
</v-container>
|
<h2>Informações de Contato</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Email</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
v-model="profileData.email"
|
||||||
|
required
|
||||||
|
class="form-input"
|
||||||
|
placeholder="seu@email.com"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Telefone</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
v-model="profileData.phone"
|
||||||
|
v-mask="'(##) #####-####'"
|
||||||
|
class="form-input"
|
||||||
|
placeholder="(00) 00000-0000"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Seção Profissional -->
|
||||||
|
<div class="section-title">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
<h2>Informações Profissionais</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Cargo</label>
|
||||||
|
<select v-model="profileData.role" class="form-select">
|
||||||
|
<option value="" disabled>Selecione um cargo</option>
|
||||||
|
<option v-for="role in roles" :key="role.id" :value="role.id">
|
||||||
|
{{ role.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Grupo</label>
|
||||||
|
<select v-model="profileData.group" class="form-select">
|
||||||
|
<option value="" disabled>Selecione um grupo</option>
|
||||||
|
<option v-for="group in groups" :key="group.id" :value="group.id">
|
||||||
|
{{ group.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Permissão</label>
|
||||||
|
<select v-model="profileData.permission" class="form-select">
|
||||||
|
<option value="" disabled>Selecione uma permissão</option>
|
||||||
|
<option v-for="permission in permissions" :key="permission.id" :value="permission.id">
|
||||||
|
{{ permission.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Turno</label>
|
||||||
|
<select v-model="profileData.shift" class="form-select">
|
||||||
|
<option value="" disabled>Selecione um turno</option>
|
||||||
|
<option value="morning">Manhã</option>
|
||||||
|
<option value="afternoon">Tarde</option>
|
||||||
|
<option value="night">Noite</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botões de Ação -->
|
||||||
|
<div class="button-group">
|
||||||
|
<button type="button" @click="resetForm" class="cancel-btn">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="save-btn">
|
||||||
|
<i class="fas fa-save"></i>
|
||||||
|
Salvar Alterações
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'UserProfileView',
|
name: 'ProfilePage',
|
||||||
data: () => ({
|
data() {
|
||||||
user: {
|
return {
|
||||||
name: '',
|
defaultPhoto: '/path/to/default-avatar.png',
|
||||||
|
profileData: {
|
||||||
|
id: '',
|
||||||
|
photoUrl: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
email: '',
|
email: '',
|
||||||
avatar: '',
|
|
||||||
phone: '',
|
phone: '',
|
||||||
role: '',
|
role: '',
|
||||||
department: ''
|
group: '',
|
||||||
}
|
permission: '',
|
||||||
}),
|
shift: ''
|
||||||
created() {
|
},
|
||||||
// Carregar dados do usuário do localStorage
|
roles: [
|
||||||
const userData = JSON.parse(localStorage.getItem('user'))
|
{ id: 1, name: 'Funcionário' },
|
||||||
if (userData) {
|
{ id: 2, name: 'Estagiário' },
|
||||||
this.user = { ...this.user, ...userData }
|
{ id: 3, name: 'Gerente' }
|
||||||
|
],
|
||||||
|
groups: [
|
||||||
|
{ id: 1, name: 'Beta' },
|
||||||
|
{ id: 2, name: 'Alfa' },
|
||||||
|
{ id: 3, name: 'Omega' }
|
||||||
|
],
|
||||||
|
permissions: [
|
||||||
|
{ id: 1, name: 'Administrador' },
|
||||||
|
{ id: 2, name: 'Editor' },
|
||||||
|
{ id: 3, name: 'Visualizador' }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
navigateToEdit() {
|
async loadProfile() {
|
||||||
this.$router.push('/edit-user')
|
try {
|
||||||
|
const response = await fetch('/api/profile');
|
||||||
|
const data = await response.json();
|
||||||
|
this.profileData = { ...data };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao carregar perfil:', error);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
triggerPhotoUpload() {
|
||||||
|
this.$refs.photoInput.click();
|
||||||
|
},
|
||||||
|
handlePhotoUpload(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
this.profileData.photoUrl = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveProfile() {
|
||||||
|
try {
|
||||||
|
await fetch('/api/profile', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.profileData)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$emit('profile-updated');
|
||||||
|
this.showNotification('Perfil atualizado com sucesso!', 'success');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erro ao salvar perfil:', error);
|
||||||
|
this.showNotification('Erro ao salvar perfil. Tente novamente.', 'error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetForm() {
|
||||||
|
this.loadProfile();
|
||||||
|
},
|
||||||
|
showNotification(message,_type) {
|
||||||
|
// Implementar sistema de notificação de sua preferência
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.profile-card {
|
.profile-container {
|
||||||
border-radius: 16px;
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-subtitle {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-form {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-list-item {
|
.profile-grid {
|
||||||
padding: 16px 0;
|
display: grid;
|
||||||
|
grid-template-columns: 300px 1fr;
|
||||||
|
gap: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-list-item__icon {
|
.photo-section {
|
||||||
margin-right: 16px;
|
padding: 30px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-list-item__subtitle {
|
.info-section {
|
||||||
color: #666;
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-photo {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-text {
|
||||||
|
color: white;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin-bottom: 4px;
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper:hover .photo-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper:hover .profile-photo {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 30px 0 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title i {
|
||||||
|
color: #3498db;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e1e8ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: #3498db;
|
||||||
|
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.readonly {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 30px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn,
|
||||||
|
.cancel-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background: #2ecc71;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn:hover {
|
||||||
|
background: #27ae60;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.profile-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-section {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn,
|
||||||
|
.cancel-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user