Versão 06/02 01h04

This commit is contained in:
Thaís Ferreira 2025-02-06 01:14:17 -03:00
parent 0eab037432
commit f38df351ce
6 changed files with 1366 additions and 478 deletions

View File

@ -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-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"
<!-- 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"
>
<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>
<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-divider class="border-opacity-25"></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>
<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-subtitle-2 text-white font-weight-bold"
class="text-caption font-weight-bold text-uppercase ml-2 mt-2"
>
TARS
</v-list-item-title>
</v-list-item>
{{ category.title }}
</v-list-subheader>
<v-divider class="my-2 divider-custom"></v-divider>
<!-- Menu Items -->
<v-list-item
v-for="item in menuItems"
v-for="item in category.items"
:key="item.name"
:to="item.route"
link
class="menu-item d-flex align-center"
density="compact"
:value="item.name"
rounded="lg"
class="mb-1"
>
<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>
<v-list-item-title
v-if="!isCollapsed"
class="text-white text-body-2"
>
<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 -->
<!-- Top Bar -->
<v-app-bar
app
color="blue-darken-3"
dark
height="48"
class="header-compact px-2"
density="compact"
elevation="1"
height="64"
color="background"
>
<v-spacer></v-spacer>
<v-spacer />
<!-- Top Bar Actions -->
<v-btn
@click="logout"
text
class="logout-button"
density="compact"
size="small"
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>

View File

@ -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>
<!-- Main Features -->
<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>
<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-icon {
font-size: 32px;
}
.v-row {
margin-top: 16px;
}
.v-col {
display: flex;
justify-content: center;
.v-list-item {
min-height: 40px;
}
</style>

View File

@ -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(.*)*',

View File

@ -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: "",
headers: [
{ text: "ID", value: "id" },
{ text: "Nome", value: "name" },
{ text: "Horário de Entrada", value: "entryTime" },
{ text: "Horário de Saída", value: "exitTime" },
],
reports: [
{ id: 1, name: "João Silva", entryTime: "08:00", exitTime: "16:00" },
{ id: 2, name: "Maria Oliveira", entryTime: "09:00", exitTime: "18:00" },
// 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: [
{
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>
<style scoped>
.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>

View File

@ -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,15 +218,21 @@ 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);
}
}
}

View File

@ -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"
>
<v-icon left>mdi-pencil</v-icon>
Editar Perfil
</v-btn>
<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>
<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>
<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"
>
<button type="button" @click="triggerPhotoUpload" class="upload-btn">
<i class="fas fa-camera"></i>
Alterar Foto
</button>
</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>
<!-- 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-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-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-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>
<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-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-group">
<label>Sobrenome</label>
<input
type="text"
v-model="profileData.lastName"
required
class="form-input"
placeholder="Digite seu sobrenome"
>
</div>
</div>
<!-- Seção de Contato -->
<div class="section-title">
<i class="fas fa-address-card"></i>
<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>
<script>
export default {
name: 'UserProfileView',
data: () => ({
user: {
name: '',
name: 'ProfilePage',
data() {
return {
defaultPhoto: '/path/to/default-avatar.png',
profileData: {
id: '',
photoUrl: '',
firstName: '',
lastName: '',
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 }
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>