Versão 06/02 01h04
This commit is contained in:
parent
0eab037432
commit
f38df351ce
357
src/App.vue
357
src/App.vue
@ -1,100 +1,299 @@
|
||||
<template>
|
||||
<!-- Anterior template permanece o mesmo até a parte do menu -->
|
||||
<v-app>
|
||||
<v-navigation-drawer
|
||||
app
|
||||
:width="drawerWidth"
|
||||
:permanent="true"
|
||||
color="blue-darken-4"
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
:rail="isCollapsed"
|
||||
:width="260"
|
||||
permanent
|
||||
elevation="4"
|
||||
class="sidebar-navigation"
|
||||
theme="dark"
|
||||
>
|
||||
<!-- Collapse Button -->
|
||||
<div class="drawer-toggle d-flex align-center" :class="{ 'justify-center': isCollapsed, 'justify-end': !isCollapsed }">
|
||||
<v-tooltip
|
||||
:text="isCollapsed ? 'Expandir Menu' : 'Recolher Menu'"
|
||||
location="right"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
@click="toggleDrawer"
|
||||
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>
|
||||
<!-- Logo Section -->
|
||||
<div class="logo-section pa-4">
|
||||
<div class="d-flex align-center" :class="{ 'justify-center': isCollapsed }">
|
||||
<v-avatar
|
||||
:size="isCollapsed ? 40 : 36"
|
||||
class="gradient-avatar"
|
||||
>
|
||||
<v-icon size="24" color="white">mdi-security</v-icon>
|
||||
</v-avatar>
|
||||
<span v-if="!isCollapsed" class="text-h6 ml-3 white--text font-weight-bold">TARS</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-list density="compact">
|
||||
<!-- TARS Logo/Title -->
|
||||
<v-list-item class="logo-container py-1">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="white" :size="20">
|
||||
mdi-robot
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title
|
||||
v-if="!isCollapsed"
|
||||
class="text-subtitle-2 text-white font-weight-bold"
|
||||
>
|
||||
TARS
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
|
||||
<v-divider class="my-2 divider-custom"></v-divider>
|
||||
<!-- User Profile Section -->
|
||||
<div class="profile-section px-4 py-3" v-if="!isCollapsed">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar color="primary" size="40">
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-avatar>
|
||||
<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>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<v-list-item
|
||||
v-for="item in menuItems"
|
||||
:key="item.name"
|
||||
:to="item.route"
|
||||
link
|
||||
class="menu-item d-flex align-center"
|
||||
density="compact"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="white" :size="20" class="menu-icon">{{ item.icon }}</v-icon>
|
||||
</template>
|
||||
<v-list-item-title
|
||||
v-if="!isCollapsed"
|
||||
class="text-white text-body-2"
|
||||
<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"
|
||||
class="text-caption font-weight-bold text-uppercase ml-2 mt-2"
|
||||
>
|
||||
{{ item.label }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
{{ category.title }}
|
||||
</v-list-subheader>
|
||||
|
||||
<v-list-item
|
||||
v-for="item in category.items"
|
||||
:key="item.name"
|
||||
:to="item.route"
|
||||
:value="item.name"
|
||||
rounded="lg"
|
||||
class="mb-1"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :size="22">{{ item.icon }}</v-icon>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="text-subtitle-2">
|
||||
{{ item.label }}
|
||||
</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>
|
||||
</template>
|
||||
</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>
|
||||
|
||||
<!-- Compact Header with Logout -->
|
||||
<v-app-bar
|
||||
app
|
||||
color="blue-darken-3"
|
||||
dark
|
||||
height="48"
|
||||
class="header-compact px-2"
|
||||
density="compact"
|
||||
<!-- Top Bar -->
|
||||
<v-app-bar
|
||||
elevation="1"
|
||||
height="64"
|
||||
color="background"
|
||||
>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
@click="logout"
|
||||
text
|
||||
class="logout-button"
|
||||
density="compact"
|
||||
size="small"
|
||||
<v-spacer />
|
||||
|
||||
<!-- Top Bar Actions -->
|
||||
<v-btn
|
||||
variant="text"
|
||||
icon="mdi-bell"
|
||||
class="mr-2"
|
||||
>
|
||||
<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
|
||||
</v-btn>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main class="content-area">
|
||||
<router-view />
|
||||
<!-- Main Content -->
|
||||
<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-app>
|
||||
</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>
|
||||
<v-container>
|
||||
<!-- Header Section -->
|
||||
<v-row class="mb-8">
|
||||
<!-- Hero Section -->
|
||||
<v-row class="mb-6">
|
||||
<v-col cols="12">
|
||||
<v-card elevation="4" class="rounded-lg shadow-lg">
|
||||
<v-card-text class="text-center pa-6">
|
||||
<h1 class="text-h3 mb-4 text-primary">Bem-vindo ao TARS</h1>
|
||||
<p class="text-subtitle-1 text-body-2">
|
||||
Sistema de gerenciamento inteligente para monitoramento por câmeras
|
||||
<v-card class="bg-primary rounded-xl">
|
||||
<v-card-text class="text-center pa-8">
|
||||
<h1 class="text-h2 font-weight-bold text-white mb-4">
|
||||
Centro de Controle TARS
|
||||
</h1>
|
||||
<p class="text-h6 text-white font-weight-regular">
|
||||
Plataforma avançada de monitoramento e análise de segurança
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<v-row>
|
||||
<!-- Modelos 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-camera-iris" class="mr-2" />
|
||||
Modelos
|
||||
</v-card-title>
|
||||
<!-- Quick Stats -->
|
||||
<v-row class="mb-6">
|
||||
<v-col v-for="stat in quickStats" :key="stat.title" cols="12" sm="6" md="3">
|
||||
<v-card :color="stat.color" variant="tonal" class="rounded-lg h-100">
|
||||
<v-card-text>
|
||||
<p class="mb-3">Gerencie modelos de detecção:</p>
|
||||
<ul class="pl-4">
|
||||
<li>Detecção de EPI</li>
|
||||
<li>Contagem de pessoas</li>
|
||||
<li>Detecção de invasão</li>
|
||||
<li>Análise de comportamento</li>
|
||||
</ul>
|
||||
<div class="d-flex flex-column align-center text-center pa-4">
|
||||
<v-icon :icon="stat.icon" size="48" class="mb-4" />
|
||||
<div class="text-h3 font-weight-bold mb-2">{{ stat.value }}</div>
|
||||
<div class="text-subtitle-1">{{ stat.title }}</div>
|
||||
</div>
|
||||
</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-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Status Section -->
|
||||
<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-col cols="12" sm="6" md="3">
|
||||
<v-stat-card
|
||||
title="Câmeras Ativas"
|
||||
:value="camerasAtivas"
|
||||
icon="mdi-camera"
|
||||
color="success"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-stat-card
|
||||
title="Modelos em Uso"
|
||||
:value="modelosAtivos"
|
||||
icon="mdi-brain"
|
||||
color="primary"
|
||||
/>
|
||||
</v-col>
|
||||
<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>
|
||||
<!-- Main Features -->
|
||||
<v-row>
|
||||
<v-col v-for="feature in features" :key="feature.title" cols="12" md="4">
|
||||
<v-card height="100%" class="rounded-lg">
|
||||
<v-card-text class="pa-6">
|
||||
<div class="d-flex align-center mb-4">
|
||||
<v-icon :icon="feature.icon" size="36" :color="feature.color" class="mr-4" />
|
||||
<h2 class="text-h5 font-weight-bold">{{ feature.title }}</h2>
|
||||
</div>
|
||||
<p class="text-body-1 mb-4">{{ feature.description }}</p>
|
||||
<v-list>
|
||||
<v-list-item v-for="item in feature.items" :key="item" class="px-0">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-check-circle" color="success" size="small" />
|
||||
</template>
|
||||
{{ item }}
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</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-col>
|
||||
</v-row>
|
||||
@ -152,71 +69,96 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
name: 'TheWelcome',
|
||||
|
||||
components: {
|
||||
VStatCard
|
||||
},
|
||||
name: 'WelcomeDashboard',
|
||||
|
||||
data() {
|
||||
return {
|
||||
camerasAtivas: 8,
|
||||
modelosAtivos: 4,
|
||||
alertasHoje: 12,
|
||||
usuariosOnline: 5
|
||||
quickStats: [
|
||||
{
|
||||
title: 'Câmeras Ativas',
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.v-card-text ul {
|
||||
padding-left: 20px;
|
||||
margin-top: 8px;
|
||||
.v-card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.v-card-text li {
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
.v-card:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.v-btn {
|
||||
text-transform: none;
|
||||
font-weight: bold;
|
||||
.v-list-item {
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.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 TrainingView from '@/views/TrainingView.vue';
|
||||
import SSOView from '@/views/SSOView.vue';
|
||||
import UserProfileView from '@/views/UserProfileView.vue'
|
||||
import EditUserView from '@/views/EditUserView.vue'
|
||||
import UserProfileView from '@/views/UserProfileView.vue';
|
||||
import EditUserView from '@/views/EditUserView.vue';
|
||||
import RegisterUserCamView from '@/views/RegisterUserCamView.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@ -104,6 +105,12 @@ const routes = [
|
||||
requiresAuth: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/register-user-cam',
|
||||
name: 'register-user-cam',
|
||||
component: RegisterUserCamView,
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
// Catch-all route for unmatched paths
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
@ -116,4 +123,4 @@ const router = createRouter({
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@ -1,54 +1,423 @@
|
||||
<template>
|
||||
<div class="reports-view">
|
||||
<v-container>
|
||||
<v-container fluid>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Relatórios de Horários
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
label="Buscar por Nome ou ID"
|
||||
append-icon="mdi-magnify"
|
||||
hide-details
|
||||
density="compact"
|
||||
></v-text-field>
|
||||
<!-- Cabeçalho do Card -->
|
||||
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="24" class="mr-2">mdi-clock-outline</v-icon>
|
||||
<span class="text-h6">Relatórios de Horários</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<v-btn
|
||||
color="primary"
|
||||
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>
|
||||
|
||||
<!-- 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-model:page="currentPage"
|
||||
:headers="headers"
|
||||
:items="reports"
|
||||
:search="search"
|
||||
:items="filteredReports"
|
||||
:loading="isLoading"
|
||||
:items-per-page="itemsPerPage"
|
||||
:search="filters.search"
|
||||
item-value="id"
|
||||
class="elevation-1"
|
||||
></v-data-table>
|
||||
hover
|
||||
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>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ReportsView",
|
||||
name: 'ReportsView',
|
||||
|
||||
data() {
|
||||
return {
|
||||
search: "",
|
||||
// Estados de UI
|
||||
isLoading: false,
|
||||
isExporting: false,
|
||||
showFilters: false,
|
||||
detailsDialog: false,
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
selectedReport: null,
|
||||
|
||||
// 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: [
|
||||
{ text: "ID", value: "id" },
|
||||
{ text: "Nome", value: "name" },
|
||||
{ text: "Horário de Entrada", value: "entryTime" },
|
||||
{ text: "Horário de Saída", value: "exitTime" },
|
||||
{
|
||||
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: "16:00" },
|
||||
{ id: 2, name: "Maria Oliveira", entryTime: "09:00", exitTime: "18:00" },
|
||||
|
||||
{
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.reports-view {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
.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>
|
||||
@ -3,51 +3,49 @@
|
||||
<v-container>
|
||||
<v-card>
|
||||
<v-tabs v-model="activeTab">
|
||||
<v-tab>Cadastro</v-tab>
|
||||
<v-tab>Pesquisar</v-tab>
|
||||
<v-tab>Treino</v-tab>
|
||||
<v-tab>Lista de Nomes</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<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 -->
|
||||
<v-form v-if="activeTab === 1" @submit.prevent="searchCameraModels">
|
||||
<v-form v-if="activeTab === 0" @submit.prevent="searchCameraModels">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field v-model="searchCriteria.name" label="Nome do Modelo"></v-text-field>
|
||||
</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-btn type="submit" color="primary">Pesquisar</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</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 -->
|
||||
<v-row v-if="activeTab === 2">
|
||||
<v-row v-if="activeTab === 1">
|
||||
<v-col>
|
||||
<v-card outlined>
|
||||
<v-card-title>Treinamento de Modelo de Câmera</v-card-title>
|
||||
@ -80,27 +78,50 @@
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</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>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 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-card outlined>
|
||||
<v-card-title class="subtitle-1">Rostos Identificados</v-card-title>
|
||||
<v-card-text>
|
||||
<v-select v-model="selectedFaceStatus" :items="['Associado', 'Não Associado']" label="Filtrar Rostos"></v-select>
|
||||
<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-icon large>mdi-account-circle</v-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-content>
|
||||
{{ face.name }}
|
||||
<v-chip small :color="face.isActive ? 'success' : 'error'">
|
||||
{{ face.isActive ? 'Ativo' : 'Inativo' }}
|
||||
</v-chip>
|
||||
<v-list-item-title>{{ face.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ face.isActive ? 'Associado' : 'Não Associado' }}</v-list-item-subtitle>
|
||||
<v-btn v-if="!face.isActive" @click="editFaceName(face)">Associar</v-btn>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@ -108,6 +129,7 @@
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
@ -119,15 +141,11 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
activeTab: 0,
|
||||
cameraModel: {
|
||||
name: '',
|
||||
id: '',
|
||||
description: '',
|
||||
registrationDate: null
|
||||
},
|
||||
searchCriteria: {
|
||||
id: '',
|
||||
name: ''
|
||||
name: '',
|
||||
status: '',
|
||||
order: ''
|
||||
},
|
||||
selectedCameraForTraining: null,
|
||||
isTraining: false,
|
||||
@ -138,45 +156,35 @@ export default {
|
||||
{ id: 'CAM003', name: 'Câmera Estacionamento', brand: 'Dahua' }
|
||||
],
|
||||
cameras: [
|
||||
{ id: 'CAM001', name: 'Câmera Principal', description: 'Câmera frontal do prédio', isActive: true, registrationDate: '2025-02-03T08: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: '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-28T 14:00', isTrained: false }
|
||||
],
|
||||
searchResults: [],
|
||||
selectedFaceStatus: 'Associado',
|
||||
identifiedFaces: [
|
||||
{
|
||||
id: 'USER001',
|
||||
name: 'João Silva',
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
id: 'USER002',
|
||||
name: 'Maria Souza',
|
||||
isActive: true
|
||||
}
|
||||
{ id: 'USER001', name: 'João Silva', isActive: true },
|
||||
{ id: 'USER002', name: 'Maria Souza', isActive: true }
|
||||
],
|
||||
unidentifiedFaces: [
|
||||
{ selectedUser: null },
|
||||
{ selectedUser: null }
|
||||
{ selectedUser: null, name: 'Desconhecido' },
|
||||
{ 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: {
|
||||
registerCameraModel() {
|
||||
console.log('Registrando modelo de câmera:', this.cameraModel);
|
||||
this.cameraModel = {
|
||||
name: '',
|
||||
id: '',
|
||||
description: '',
|
||||
registrationDate: null
|
||||
};
|
||||
},
|
||||
searchCameraModels() {
|
||||
console.log('Critérios de pesquisa:', this.searchCriteria);
|
||||
this.searchResults = this.cameras.filter(camera =>
|
||||
(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))
|
||||
);
|
||||
|
||||
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() {
|
||||
if (this.selectedCameraForTraining) {
|
||||
@ -210,16 +218,22 @@ export default {
|
||||
}
|
||||
},
|
||||
integrateModel() {
|
||||
console.log('Integrando modelo:', this.selectedCameraForTraining);
|
||||
this.trainingComplete = false;
|
||||
this.isTraining = false;
|
||||
},
|
||||
confirmAssignments() {
|
||||
console.log('Atribuições confirmadas:', this.unidentifiedFaces);
|
||||
this.integrationStatus = 'Em andamento...';
|
||||
setTimeout(() => {
|
||||
this.integrationStatus = 'Concluído';
|
||||
}, 3000);
|
||||
},
|
||||
editFaceName(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
@ -1,144 +1,501 @@
|
||||
<!-- ProfilePage.vue -->
|
||||
<template>
|
||||
<v-container>
|
||||
<v-card class="mx-auto profile-card" max-width="800">
|
||||
<v-row no-gutters>
|
||||
<!-- Seção da foto do perfil -->
|
||||
<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"
|
||||
<div class="profile-container">
|
||||
<div class="profile-header">
|
||||
<h1 class="profile-title">Perfil do Usuário</h1>
|
||||
<p class="profile-subtitle">Gerencie suas informações pessoais</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="saveProfile" class="profile-form">
|
||||
<div class="profile-grid">
|
||||
<!-- Coluna da Foto -->
|
||||
<div class="photo-section">
|
||||
<div class="profile-photo-container">
|
||||
<div class="photo-wrapper">
|
||||
<img :src="profileData.photoUrl || defaultPhoto" alt="Foto de Perfil" class="profile-photo">
|
||||
<div class="photo-overlay">
|
||||
<span class="photo-text">Alterar foto</span>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
@change="handlePhotoUpload"
|
||||
accept="image/*"
|
||||
ref="photoInput"
|
||||
class="photo-input"
|
||||
>
|
||||
<v-icon left>mdi-pencil</v-icon>
|
||||
Editar Perfil
|
||||
</v-btn>
|
||||
<button type="button" @click="triggerPhotoUpload" class="upload-btn">
|
||||
<i class="fas fa-camera"></i>
|
||||
Alterar Foto
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coluna das Informações -->
|
||||
<div class="info-section">
|
||||
<!-- Seção de Informações Básicas -->
|
||||
<div class="section-title">
|
||||
<i class="fas fa-user"></i>
|
||||
<h2 class="sub-title">Informações Básicas</h2>
|
||||
</div>
|
||||
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon color="#2563eb">mdi-account</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>Nome</v-list-item-subtitle>
|
||||
<v-list-item-title class="text-h6">
|
||||
{{ user.name }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>ID</label>
|
||||
<input type="text" v-model="profileData.id" readonly class="form-input readonly">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon color="#2563eb">mdi-email</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>Email</v-list-item-subtitle>
|
||||
<v-list-item-title class="text-h6">
|
||||
{{ user.email }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Nome</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="profileData.firstName"
|
||||
required
|
||||
class="form-input"
|
||||
placeholder="Digite seu nome"
|
||||
>
|
||||
</div>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon color="#2563eb">mdi-phone</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>Telefone</v-list-item-subtitle>
|
||||
<v-list-item-title class="text-h6">
|
||||
{{ user.phone || 'Não informado' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<div class="form-group">
|
||||
<label>Sobrenome</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="profileData.lastName"
|
||||
required
|
||||
class="form-input"
|
||||
placeholder="Digite seu sobrenome"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon color="#2563eb">mdi-badge-account</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>Cargo</v-list-item-subtitle>
|
||||
<v-list-item-title class="text-h6">
|
||||
{{ user.role || 'Não informado' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<!-- Seção de Contato -->
|
||||
<div class="section-title">
|
||||
<i class="fas fa-address-card"></i>
|
||||
<h2>Informações de Contato</h2>
|
||||
</div>
|
||||
|
||||
<v-list-item>
|
||||
<v-list-item-icon>
|
||||
<v-icon color="#2563eb">mdi-office-building</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-subtitle>Departamento</v-list-item-subtitle>
|
||||
<v-list-item-title class="text-h6">
|
||||
{{ user.department || 'Não informado' }}
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-container>
|
||||
<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>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'UserProfileView',
|
||||
data: () => ({
|
||||
user: {
|
||||
name: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
department: ''
|
||||
}
|
||||
}),
|
||||
created() {
|
||||
// Carregar dados do usuário do localStorage
|
||||
const userData = JSON.parse(localStorage.getItem('user'))
|
||||
if (userData) {
|
||||
this.user = { ...this.user, ...userData }
|
||||
name: 'ProfilePage',
|
||||
data() {
|
||||
return {
|
||||
defaultPhoto: '/path/to/default-avatar.png',
|
||||
profileData: {
|
||||
id: '',
|
||||
photoUrl: '',
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
group: '',
|
||||
permission: '',
|
||||
shift: ''
|
||||
},
|
||||
roles: [
|
||||
{ id: 1, name: 'Funcionário' },
|
||||
{ id: 2, name: 'Estagiário' },
|
||||
{ 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: {
|
||||
navigateToEdit() {
|
||||
this.$router.push('/edit-user')
|
||||
async loadProfile() {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
.profile-card {
|
||||
border-radius: 16px;
|
||||
.profile-container {
|
||||
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;
|
||||
}
|
||||
|
||||
.v-list-item {
|
||||
padding: 16px 0;
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.v-list-item__icon {
|
||||
margin-right: 16px;
|
||||
.photo-section {
|
||||
padding: 30px;
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.v-list-item__subtitle {
|
||||
color: #666;
|
||||
.info-section {
|
||||
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;
|
||||
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>
|
||||
Loading…
Reference in New Issue
Block a user