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> <template>
<!-- Anterior template permanece o mesmo até a parte do menu -->
<v-app> <v-app>
<v-navigation-drawer <v-navigation-drawer
app v-model="drawer"
:width="drawerWidth" :rail="isCollapsed"
:permanent="true" :width="260"
color="blue-darken-4" permanent
elevation="4"
class="sidebar-navigation" class="sidebar-navigation"
theme="dark"
> >
<!-- Collapse Button --> <!-- Logo Section -->
<div class="drawer-toggle d-flex align-center" :class="{ 'justify-center': isCollapsed, 'justify-end': !isCollapsed }"> <div class="logo-section pa-4">
<v-tooltip <div class="d-flex align-center" :class="{ 'justify-center': isCollapsed }">
:text="isCollapsed ? 'Expandir Menu' : 'Recolher Menu'" <v-avatar
location="right" :size="isCollapsed ? 40 : 36"
class="gradient-avatar"
> >
<template v-slot:activator="{ props }"> <v-icon size="24" color="white">mdi-security</v-icon>
<v-btn </v-avatar>
icon <span v-if="!isCollapsed" class="text-h6 ml-3 white--text font-weight-bold">TARS</span>
@click="toggleDrawer" </div>
color="white"
v-bind="props"
class="collapse-button my-2"
size="24"
variant="text"
>
<v-icon :size="20">
{{ isCollapsed ? 'mdi-menu' : 'mdi-chevron-left' }}
</v-icon>
</v-btn>
</template>
</v-tooltip>
</div> </div>
<v-list density="compact"> <v-divider class="border-opacity-25"></v-divider>
<!-- TARS Logo/Title -->
<v-list-item class="logo-container py-1"> <!-- User Profile Section -->
<template v-slot:prepend> <div class="profile-section px-4 py-3" v-if="!isCollapsed">
<v-icon color="white" :size="20"> <div class="d-flex align-center">
mdi-robot <v-avatar color="primary" size="40">
</v-icon> <v-icon>mdi-account</v-icon>
</template> </v-avatar>
<v-list-item-title <div class="ml-3">
<div class="text-subtitle-2 font-weight-medium">Admin User</div>
<div class="text-caption text-grey-lighten-1">Administrador</div>
</div>
</div>
</div>
<v-divider class="border-opacity-25"></v-divider>
<!-- Navigation Menu -->
<v-list nav class="px-2">
<template v-for="category in menuCategories" :key="category.title">
<v-list-subheader
v-if="!isCollapsed" v-if="!isCollapsed"
class="text-subtitle-2 text-white font-weight-bold" class="text-caption font-weight-bold text-uppercase ml-2 mt-2"
> >
TARS {{ category.title }}
</v-list-item-title> </v-list-subheader>
</v-list-item>
<v-divider class="my-2 divider-custom"></v-divider>
<!-- Menu Items -->
<v-list-item <v-list-item
v-for="item in menuItems" v-for="item in category.items"
:key="item.name" :key="item.name"
:to="item.route" :to="item.route"
link :value="item.name"
class="menu-item d-flex align-center" rounded="lg"
density="compact" class="mb-1"
> >
<template v-slot:prepend> <template v-slot:prepend>
<v-icon color="white" :size="20" class="menu-icon">{{ item.icon }}</v-icon> <v-icon :size="22">{{ item.icon }}</v-icon>
</template> </template>
<v-list-item-title
v-if="!isCollapsed" <v-list-item-title class="text-subtitle-2">
class="text-white text-body-2"
>
{{ item.label }} {{ item.label }}
</v-list-item-title> </v-list-item-title>
<template v-slot:append v-if="item.badge && !isCollapsed">
<v-chip
size="x-small"
:color="item.badge.color"
class="font-weight-bold"
>
{{ item.badge.text }}
</v-chip>
</template>
</v-list-item> </v-list-item>
</template>
</v-list> </v-list>
<!-- Bottom Actions -->
<template v-slot:append>
<div class="pa-4">
<v-btn
block
@click="toggleDrawer"
color="primary"
variant="tonal"
:prepend-icon="isCollapsed ? 'mdi-chevron-right' : 'mdi-chevron-left'"
>
<span v-if="!isCollapsed">Recolher Menu</span>
</v-btn>
</div>
</template>
</v-navigation-drawer> </v-navigation-drawer>
<!-- Compact Header with Logout --> <!-- Top Bar -->
<v-app-bar <v-app-bar
app elevation="1"
color="blue-darken-3" height="64"
dark color="background"
height="48"
class="header-compact px-2"
density="compact"
> >
<v-spacer></v-spacer> <v-spacer />
<!-- Top Bar Actions -->
<v-btn <v-btn
@click="logout" variant="text"
text icon="mdi-bell"
class="logout-button" class="mr-2"
density="compact" >
size="small" <v-badge
color="error"
content="3"
dot
></v-badge>
</v-btn>
<v-btn
variant="text"
icon="mdi-cog"
class="mr-2"
></v-btn>
<v-btn
color="error"
variant="tonal"
@click="logout"
prepend-icon="mdi-logout"
class="mr-2"
> >
<v-icon size="small" class="mr-1">mdi-logout</v-icon>
Sair Sair
</v-btn> </v-btn>
</v-app-bar> </v-app-bar>
<v-main class="content-area"> <!-- Main Content -->
<router-view /> <v-main>
<v-container fluid class="pa-6">
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</v-container>
</v-main> </v-main>
</v-app> </v-app>
</template> </template>
<script>
import { mapActions } from 'vuex';
export default {
name: 'AppLayout',
data() {
return {
drawer: true,
isCollapsed: false,
menuCategories: [
{
title: 'Principal',
items: [
{
name: 'home',
route: { name: 'home' },
icon: 'mdi-home',
label: 'Home'
},
{
name: 'dashboard',
route: { name: 'dashboard' },
icon: 'mdi-view-dashboard',
label: 'Dashboard',
badge: { text: 'Novo', color: 'success' }
},
{
name: 'monitoring',
route: { name: 'monitoring' },
icon: 'mdi-cctv',
label: 'Monitoramento',
badge: { text: '12', color: 'info' }
}
]
},
{
title: 'Gestão',
items: [
{
name: 'analytics',
route: { name: 'analytics' },
icon: 'mdi-chart-box',
label: 'Analytics'
},
{
name: 'reports',
route: { name: 'reports' },
icon: 'mdi-file-chart',
label: 'Relatórios'
},
{
name: 'alerts',
route: { name: 'alerts' },
icon: 'mdi-bell-ring',
label: 'Alertas',
badge: { text: '3', color: 'error' }
}
]
},
{
title: 'Desenvolvimento',
items: [
{
name: 'testing',
route: { name: 'testing' },
icon: 'mdi-flask',
label: 'Testes',
badge: { text: 'Dev', color: 'warning' }
},
{
name: 'register',
route: { name: 'register' },
icon: 'mdi-file-document-plus',
label: 'Registro'
}
]
},
{
title: 'Configurações',
items: [
{
name: 'cameras',
route: { name: 'cameras' },
icon: 'mdi-camera',
label: 'Câmeras'
},
{
name: 'users',
route: { name: 'users' },
icon: 'mdi-account-group',
label: 'Usuários'
},
{
name: 'settings',
route: { name: 'settings' },
icon: 'mdi-cog',
label: 'Configurações'
}
]
}
]
};
},
methods: {
...mapActions('auth', ['logout']),
toggleDrawer() {
this.isCollapsed = !this.isCollapsed;
},
async handleLogout() {
try {
await this.logout();
this.$router.push('/login');
} catch (error) {
console.error('Erro ao fazer logout:', error);
}
}
}
};
</script>
<style scoped>
.sidebar-navigation {
background: linear-gradient(145deg, #1a237e 0%, #0d47a1 100%);
}
.gradient-avatar {
background: linear-gradient(145deg, #2196f3 0%, #1565c0 100%);
}
.profile-section {
background-color: rgba(255, 255, 255, 0.05);
}
.v-list-item--active {
background-color: rgba(255, 255, 255, 0.1) !important;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.2s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -1,150 +1,67 @@
<template> <template>
<v-container> <v-container>
<!-- Header Section --> <!-- Hero Section -->
<v-row class="mb-8"> <v-row class="mb-6">
<v-col cols="12"> <v-col cols="12">
<v-card elevation="4" class="rounded-lg shadow-lg"> <v-card class="bg-primary rounded-xl">
<v-card-text class="text-center pa-6"> <v-card-text class="text-center pa-8">
<h1 class="text-h3 mb-4 text-primary">Bem-vindo ao TARS</h1> <h1 class="text-h2 font-weight-bold text-white mb-4">
<p class="text-subtitle-1 text-body-2"> Centro de Controle TARS
Sistema de gerenciamento inteligente para monitoramento por câmeras </h1>
<p class="text-h6 text-white font-weight-regular">
Plataforma avançada de monitoramento e análise de segurança
</p> </p>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<!-- Features Grid --> <!-- Quick Stats -->
<v-row> <v-row class="mb-6">
<!-- Modelos Section --> <v-col v-for="stat in quickStats" :key="stat.title" cols="12" sm="6" md="3">
<v-col cols="12" md="6" lg="4"> <v-card :color="stat.color" variant="tonal" class="rounded-lg h-100">
<v-card elevation="3" class="rounded-lg">
<v-card-title class="d-flex align-center">
<v-icon icon="mdi-camera-iris" class="mr-2" />
Modelos
</v-card-title>
<v-card-text> <v-card-text>
<p class="mb-3">Gerencie modelos de detecção:</p> <div class="d-flex flex-column align-center text-center pa-4">
<ul class="pl-4"> <v-icon :icon="stat.icon" size="48" class="mb-4" />
<li>Detecção de EPI</li> <div class="text-h3 font-weight-bold mb-2">{{ stat.value }}</div>
<li>Contagem de pessoas</li> <div class="text-subtitle-1">{{ stat.title }}</div>
<li>Detecção de invasão</li> </div>
<li>Análise de comportamento</li>
</ul>
</v-card-text> </v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn
variant="contained"
color="primary"
:to="{ name: 'models' }"
>
Ver Modelos
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<!-- Relatórios Section -->
<v-col cols="12" md="6" lg="4">
<v-card elevation="3" class="rounded-lg">
<v-card-title class="d-flex align-center">
<v-icon icon="mdi-chart-bar" class="mr-2" />
Relatórios
</v-card-title>
<v-card-text>
<p class="mb-3">Acesse relatórios detalhados:</p>
<ul class="pl-4">
<li>Estatísticas de detecção</li>
<li>Alertas gerados</li>
<li>Performance dos modelos</li>
<li>Histórico de eventos</li>
</ul>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn
variant="contained"
color="primary"
:to="{ name: 'reports' }"
>
Ver Relatórios
</v-btn>
</v-card-actions>
</v-card>
</v-col>
<!-- Usuários Section -->
<v-col cols="12" md="6" lg="4">
<v-card elevation="3" class="rounded-lg">
<v-card-title class="d-flex align-center">
<v-icon icon="mdi-account-group" class="mr-2" />
Usuários
</v-card-title>
<v-card-text>
<p class="mb-3">Gerencie usuários do sistema:</p>
<ul class="pl-4">
<li>Controle de acesso</li>
<li>Permissões por função</li>
<li>Histórico de atividades</li>
<li>Notificações</li>
</ul>
</v-card-text>
<v-card-actions class="d-flex justify-end">
<v-btn
variant="contained"
color="primary"
:to="{ name: 'users' }"
>
Gerenciar Usuários
</v-btn>
</v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<!-- Status Section --> <!-- Main Features -->
<v-row class="mt-8">
<v-col cols="12">
<v-card elevation="3" class="rounded-lg">
<v-card-title class="d-flex align-center">
<v-icon icon="mdi-information" class="mr-2" />
Status do Sistema
</v-card-title>
<v-card-text>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col v-for="feature in features" :key="feature.title" cols="12" md="4">
<v-stat-card <v-card height="100%" class="rounded-lg">
title="Câmeras Ativas" <v-card-text class="pa-6">
:value="camerasAtivas" <div class="d-flex align-center mb-4">
icon="mdi-camera" <v-icon :icon="feature.icon" size="36" :color="feature.color" class="mr-4" />
color="success" <h2 class="text-h5 font-weight-bold">{{ feature.title }}</h2>
/> </div>
</v-col> <p class="text-body-1 mb-4">{{ feature.description }}</p>
<v-col cols="12" sm="6" md="3"> <v-list>
<v-stat-card <v-list-item v-for="item in feature.items" :key="item" class="px-0">
title="Modelos em Uso" <template v-slot:prepend>
:value="modelosAtivos" <v-icon icon="mdi-check-circle" color="success" size="small" />
icon="mdi-brain" </template>
color="primary" {{ item }}
/> </v-list-item>
</v-col> </v-list>
<v-col cols="12" sm="6" md="3">
<v-stat-card
title="Alertas Hoje"
:value="alertasHoje"
icon="mdi-alert"
color="warning"
/>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-stat-card
title="Usuários Online"
:value="usuariosOnline"
icon="mdi-account-check"
color="info"
/>
</v-col>
</v-row>
</v-card-text> </v-card-text>
<v-card-actions class="pa-6 pt-0">
<v-btn
:color="feature.color"
variant="tonal"
:to="feature.route"
block
class="text-none"
>
{{ feature.actionText }}
<v-icon icon="mdi-chevron-right" end />
</v-btn>
</v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
@ -152,71 +69,96 @@
</template> </template>
<script> <script>
// Componente reutilizável para estatísticas
const VStatCard = {
props: {
title: String,
value: [Number, String],
icon: String,
color: String
},
template: `
<v-card :color="color" variant="tonal" class="rounded-lg mb-4">
<v-card-text>
<div class="d-flex align-center mb-2">
<v-icon :icon="icon" size="24" class="mr-2" />
<span class="text-subtitle-2">{{ title }}</span>
</div>
<div class="text-h4 font-weight-bold">{{ value }}</div>
</v-card-text>
</v-card>
`
}
export default { export default {
name: 'TheWelcome', name: 'WelcomeDashboard',
components: {
VStatCard
},
data() { data() {
return { return {
camerasAtivas: 8, quickStats: [
modelosAtivos: 4, {
alertasHoje: 12, title: 'Câmeras Ativas',
usuariosOnline: 5 value: 12,
icon: 'mdi-cctv',
color: 'success'
},
{
title: 'Modelos AI',
value: 6,
icon: 'mdi-brain',
color: 'primary'
},
{
title: 'Alertas 24h',
value: 18,
icon: 'mdi-bell',
color: 'warning'
},
{
title: 'Usuários',
value: 24,
icon: 'mdi-account-group',
color: 'info'
}
],
features: [
{
title: 'Monitoramento Inteligente',
description: 'Sistema avançado de vigilância com análise em tempo real',
icon: 'mdi-security',
color: 'primary',
route: { name: 'monitoring' },
actionText: 'Acessar Monitoramento',
items: [
'Detecção de EPI em tempo real',
'Análise de fluxo de pessoas',
'Detecção de invasão de área',
'Reconhecimento de comportamentos'
]
},
{
title: 'Analytics & Relatórios',
description: 'Análise detalhada e geração de insights automáticos',
icon: 'mdi-chart-box',
color: 'success',
route: { name: 'analytics' },
actionText: 'Ver Analytics',
items: [
'Dashboards personalizados',
'Relatórios automatizados',
'Exportação de dados',
'Métricas de performance'
]
},
{
title: 'Gestão & Configuração',
description: 'Controle total sobre o sistema e suas funcionalidades',
icon: 'mdi-cog',
color: 'info',
route: { name: 'settings' },
actionText: 'Configurar Sistema',
items: [
'Gerenciamento de usuários',
'Configuração de câmeras',
'Ajuste de modelos AI',
'Políticas de segurança'
]
}
]
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.v-card-text ul { .v-card {
padding-left: 20px; transition: transform 0.2s;
margin-top: 8px;
} }
.v-card-text li { .v-card:hover {
margin-bottom: 8px; transform: translateY(-4px);
font-size: 14px;
} }
.v-btn { .v-list-item {
text-transform: none; min-height: 40px;
font-weight: bold;
}
.v-icon {
font-size: 32px;
}
.v-row {
margin-top: 16px;
}
.v-col {
display: flex;
justify-content: center;
} }
</style> </style>

View File

@ -10,8 +10,9 @@ import ForgotPassword from '@/views/ForgotPassword.vue';
import RegisterView from '@/views/RegisterView.vue'; import RegisterView from '@/views/RegisterView.vue';
import TrainingView from '@/views/TrainingView.vue'; import TrainingView from '@/views/TrainingView.vue';
import SSOView from '@/views/SSOView.vue'; import SSOView from '@/views/SSOView.vue';
import UserProfileView from '@/views/UserProfileView.vue' import UserProfileView from '@/views/UserProfileView.vue';
import EditUserView from '@/views/EditUserView.vue' import EditUserView from '@/views/EditUserView.vue';
import RegisterUserCamView from '@/views/RegisterUserCamView.vue';
const routes = [ const routes = [
{ {
@ -104,6 +105,12 @@ const routes = [
requiresAuth: false requiresAuth: false
} }
}, },
{
path: '/register-user-cam',
name: 'register-user-cam',
component: RegisterUserCamView,
meta: { requiresAuth: true }
},
// Catch-all route for unmatched paths // Catch-all route for unmatched paths
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',

View File

@ -1,54 +1,423 @@
<template> <template>
<div class="reports-view"> <div class="reports-view">
<v-container> <v-container fluid>
<v-card> <v-card>
<v-card-title> <!-- Cabeçalho do Card -->
Relatórios de Horários <v-card-title class="d-flex align-center justify-space-between pa-4">
<v-spacer></v-spacer> <div class="d-flex align-center">
<v-text-field <v-icon size="24" class="mr-2">mdi-clock-outline</v-icon>
v-model="search" <span class="text-h6">Relatórios de Horários</span>
label="Buscar por Nome ou ID" </div>
append-icon="mdi-magnify" <div class="d-flex gap-2">
hide-details <v-btn
density="compact" color="primary"
></v-text-field> prepend-icon="mdi-filter"
@click="showFilters = !showFilters"
variant="tonal"
>
{{ showFilters ? 'Ocultar Filtros' : 'Mostrar Filtros' }}
</v-btn>
<v-btn
color="success"
prepend-icon="mdi-file-export"
@click="exportToCSV"
:loading="isExporting"
>
Exportar CSV
</v-btn>
</div>
</v-card-title> </v-card-title>
<!-- Seção de Filtros -->
<v-expand-transition>
<div v-if="showFilters">
<v-card-text class="pt-2">
<v-row>
<v-col cols="12" sm="4">
<v-text-field
v-model="filters.search"
label="Buscar por Nome ou ID"
prepend-icon="mdi-magnify"
hide-details
density="comfortable"
clearable
@keyup.enter="applyFilters"
/>
</v-col>
<v-col cols="12" sm="4">
<v-select
v-model="filters.timeRange"
:items="timeRanges"
label="Período"
prepend-icon="mdi-calendar"
hide-details
density="comfortable"
/>
</v-col>
<v-col cols="12" sm="4">
<v-select
v-model="filters.status"
:items="statusOptions"
label="Status"
prepend-icon="mdi-check-circle"
hide-details
density="comfortable"
multiple
chips
/>
</v-col>
</v-row>
</v-card-text>
</div>
</v-expand-transition>
<v-divider />
<!-- Tabela de Dados -->
<v-data-table <v-data-table
v-model:page="currentPage"
:headers="headers" :headers="headers"
:items="reports" :items="filteredReports"
:search="search" :loading="isLoading"
:items-per-page="itemsPerPage"
:search="filters.search"
item-value="id" item-value="id"
class="elevation-1" hover
></v-data-table> density="comfortable"
class="elevation-0"
>
<!-- Slot para Status -->
<template v-slot:item_status="{ item }">
<v-chip
:color="getStatusColor(item.raw.status)"
size="small"
class="text-caption"
>
{{ item.raw.status }}
</v-chip>
</template>
<!-- Slot para Ações -->
<template v-slot:item_actions="{ item }">
<div class="d-flex gap-2">
<v-tooltip text="Visualizar Detalhes">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-eye"
size="small"
variant="text"
v-bind="props"
@click="viewDetails(item.raw)"
/>
</template>
</v-tooltip>
<v-tooltip text="Editar Registro">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-pencil"
size="small"
variant="text"
v-bind="props"
@click="editReport(item.raw)"
/>
</template>
</v-tooltip>
</div>
</template>
<!-- Loading e Estado Vazio -->
<template v-slot:loading>
<v-skeleton-loader
type="table-row"
:loading="true"
class="pa-4"
/>
</template>
<template v-slot:no-data>
<div class="d-flex flex-column align-center pa-4">
<v-icon size="48" color="grey">mdi-alert-circle-outline</v-icon>
<span class="text-grey mt-2">Nenhum registro encontrado</span>
</div>
</template>
</v-data-table>
</v-card> </v-card>
<!-- Modal de Detalhes -->
<v-dialog v-model="detailsDialog" max-width="600">
<v-card v-if="selectedReport">
<v-card-title class="d-flex justify-space-between align-center pa-4">
Detalhes do Registro
<v-btn icon="mdi-close" variant="text" @click="detailsDialog = false" />
</v-card-title>
<v-card-text>
<v-list>
<v-list-item>
<template v-slot:prepend>
<v-icon color="primary">mdi-account</v-icon>
</template>
<v-list-item-title>{{ selectedReport.name }}</v-list-item-title>
<v-list-item-subtitle>Nome do Funcionário</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon color="primary">mdi-clock-in</v-icon>
</template>
<v-list-item-title>{{ selectedReport.entryTime }}</v-list-item-title>
<v-list-item-subtitle>Horário de Entrada</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon color="primary">mdi-clock-out</v-icon>
</template>
<v-list-item-title>{{ selectedReport.exitTime }}</v-list-item-title>
<v-list-item-subtitle>Horário de Saída</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-dialog>
</v-container> </v-container>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: "ReportsView", name: 'ReportsView',
data() { data() {
return { return {
search: "", // Estados de UI
headers: [ isLoading: false,
{ text: "ID", value: "id" }, isExporting: false,
{ text: "Nome", value: "name" }, showFilters: false,
{ text: "Horário de Entrada", value: "entryTime" }, detailsDialog: false,
{ text: "Horário de Saída", value: "exitTime" }, currentPage: 1,
], itemsPerPage: 10,
reports: [ selectedReport: null,
{ id: 1, name: "João Silva", entryTime: "08:00", exitTime: "16:00" },
{ id: 2, name: "Maria Oliveira", entryTime: "09:00", exitTime: "18:00" },
], // Filtros
}; filters: {
search: '',
timeRange: 'today',
status: [],
}, },
};
// Opções de Filtro
timeRanges: [
{ title: 'Hoje', value: 'today' },
{ title: 'Última Semana', value: 'week' },
{ title: 'Último Mês', value: 'month' },
],
statusOptions: [
{ title: 'Regular', value: 'regular' },
{ title: 'Atrasado', value: 'late' },
{ title: 'Saída Antecipada', value: 'early' },
],
// Configuração da Tabela
headers: [
{
title: 'ID',
key: 'id',
align: 'start',
sortable: true,
width: '80'
},
{
title: 'Nome',
key: 'name',
align: 'start',
sortable: true,
},
{
title: 'Entrada',
key: 'entryTime',
align: 'center',
sortable: true,
width: '120'
},
{
title: 'Saída',
key: 'exitTime',
align: 'center',
sortable: true,
width: '120'
},
{
title: 'Status',
key: 'status',
align: 'center',
sortable: true,
width: '120'
},
{
title: 'Ações',
key: 'actions',
align: 'center',
sortable: false,
width: '100'
},
],
// Dados Mockados (substituir por API)
reports: [
{
id: 1,
name: 'João Silva',
entryTime: '08:00',
exitTime: '17:00',
status: 'regular'
},
{
id: 2,
name: 'Maria Oliveira',
entryTime: '09:15',
exitTime: '18:00',
status: 'late'
},
{
id: 3,
name: 'Pedro Santos',
entryTime: '08:00',
exitTime: '16:30',
status: 'early'
},
],
}
},
computed: {
filteredReports() {
let filtered = [...this.reports]
// Filtra por status se selecionado
if (this.filters.status.length > 0) {
filtered = filtered.filter(report =>
this.filters.status.includes(report.status)
)
}
// Filtra por período
filtered = this.filterByTimeRange(filtered)
return filtered
},
},
methods: {
// Métodos de Filtro
filterByTimeRange(reports) {
// Implementar lógica de filtro por período
return reports
},
applyFilters() {
// Implementar lógica adicional de filtros se necessário
this.isLoading = true
setTimeout(() => {
this.isLoading = false
}, 500)
},
// Métodos de UI
getStatusColor(status) {
const colors = {
regular: 'success',
late: 'error',
early: 'warning'
}
return colors[status] || 'grey'
},
viewDetails(report) {
this.selectedReport = report
this.detailsDialog = true
},
editReport(report) {
// Implementar lógica de edição
console.log('Editar relatório:', report.id)
},
// Exportação
async exportToCSV() {
try {
this.isExporting = true
const headers = this.headers
.filter(h => h.key !== 'actions')
.map(h => h.title)
const data = this.filteredReports.map(report =>
headers.map(header =>
report[header.toLowerCase()] || ''
)
)
const csvContent = [
headers.join(','),
...data.map(row => row.join(','))
].join('\n')
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `relatorio_horarios_${new Date().toISOString().split('T')[0]}.csv`
link.click()
this.$notify({
type: 'success',
text: 'Relatório exportado com sucesso!'
})
} catch (error) {
console.error('Erro ao exportar:', error)
this.$notify({
type: 'error',
text: 'Erro ao exportar relatório. Tente novamente.'
})
} finally {
this.isExporting = false
}
},
},
// Lifecycle Hooks
async created() {
try {
this.isLoading = true
// Implementar chamada à API
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (error) {
console.error('Erro ao carregar dados:', error)
} finally {
this.isLoading = false
}
},
}
</script> </script>
<style scoped> <style scoped>
.reports-view { .reports-view {
padding: 16px; height: 100%;
}
.v-data-table :deep(th) {
font-weight: 600 !important;
background-color: rgb(var(--v-theme-surface)) !important;
}
.v-data-table :deep(tr:hover) {
background-color: rgb(var(--v-theme-surface-variant)) !important;
}
/* Animações */
.v-enter-active,
.v-leave-active {
transition: opacity 0.3s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
} }
</style> </style>

View File

@ -3,51 +3,49 @@
<v-container> <v-container>
<v-card> <v-card>
<v-tabs v-model="activeTab"> <v-tabs v-model="activeTab">
<v-tab>Cadastro</v-tab>
<v-tab>Pesquisar</v-tab> <v-tab>Pesquisar</v-tab>
<v-tab>Treino</v-tab> <v-tab>Treino</v-tab>
<v-tab>Lista de Nomes</v-tab> <v-tab>Lista de Nomes</v-tab>
</v-tabs> </v-tabs>
<v-card-text> <v-card-text>
<!-- Cadastro Tab -->
<v-form v-if="activeTab === 0" @submit.prevent="registerCameraModel">
<v-row>
<v-col cols="12" md="4">
<v-text-field v-model="cameraModel.name" label="Nome do Modelo" required></v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-text-field v-model="cameraModel.responsible" label="Responsável pelo Cadastro" required></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field v-model="cameraModel.description" label="Descrição" required></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-text-field type="date" v-model="cameraModel.creationDate" label="Data de Criação" required></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-text-field type="time" v-model="cameraModel.creationTime" label="Hora de Criação" required></v-text-field>
</v-col>
<v-col cols="12">
<v-btn type="submit" color="primary">Cadastrar Modelo</v-btn>
</v-col>
</v-row>
</v-form>
<!-- Pesquisar Tab --> <!-- Pesquisar Tab -->
<v-form v-if="activeTab === 1" @submit.prevent="searchCameraModels"> <v-form v-if="activeTab === 0" @submit.prevent="searchCameraModels">
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field v-model="searchCriteria.name" label="Nome do Modelo"></v-text-field> <v-text-field v-model="searchCriteria.name" label="Nome do Modelo"></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6">
<v-text-field v-model="searchCriteria.id" label="ID"></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-select v-model="searchCriteria.status" :items="['Ativo', 'Inativo']" label="Status"></v-select>
</v-col>
<v-col cols="12" md="6">
<v-select v-model="searchCriteria.order" :items="['A-Z', 'Z-A']" label="Ordenar"></v-select>
</v-col>
<v-col cols="12"> <v-col cols="12">
<v-btn type="submit" color="primary">Pesquisar</v-btn> <v-btn type="submit" color="primary">Pesquisar</v-btn>
</v-col> </v-col>
</v-row> </v-row>
</v-form> </v-form>
<v-list v-if="searchResults.length">
<v-list-item-group>
<v-list-item v-for="(camera, index) in searchResults" :key="index">
<v-list-item-content>
<v-list-item-title>{{ camera.name }}</v-list-item-title>
<v-list-item-subtitle>{{ camera.id }}</v-list-item-subtitle>
<v-list-item-subtitle>{{ camera.registrationDate }}</v-list-item-subtitle>
<v-list-item-subtitle>{{ camera.isActive ? 'Ativo' : 'Inativo' }}</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
<!-- Treino Tab --> <!-- Treino Tab -->
<v-row v-if="activeTab === 2"> <v-row v-if="activeTab === 1">
<v-col> <v-col>
<v-card outlined> <v-card outlined>
<v-card-title>Treinamento de Modelo de Câmera</v-card-title> <v-card-title>Treinamento de Modelo de Câmera</v-card-title>
@ -80,27 +78,50 @@
</v-btn> </v-btn>
</v-col> </v-col>
</v-row> </v-row>
<v-progress-linear
v-if="isTraining"
:value="trainingProgress"
color="primary"
height="20"
></v-progress-linear>
<v-row v-if="trainingMessage">
<v-col>{{ trainingMessage }}</v-col>
</v-row>
<v-row v-if="trainingComplete">
<v-col>
<v-card>
<v-card-title>Informações da Câmera</v-card-title>
<v-card-text>
<p><strong>Nome:</strong> {{ selectedCameraForTraining.name }}</p>
<p><strong>Status do Treinamento:</strong> Concluído</p>
<p><strong>Status da Integração:</strong> {{ integrationStatus }}</p>
<p><strong>Tempo de Treinamento:</strong> 5 minutos</p>
<p><strong>Tempo de Integração:</strong> 3 minutos</p>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
<!-- Lista de Nomes Tab --> <!-- Lista de Nomes Tab -->
<v-row v-if="activeTab === 3" class="mt-4"> <v-row v-if="activeTab === 2" class="mt-4">
<v-col md="6" sm="12"> <v-col md="6" sm="12">
<v-card outlined> <v-card outlined>
<v-card-title class="subtitle-1">Rostos Identificados</v-card-title> <v-card-title class="subtitle-1">Rostos Identificados</v-card-title>
<v-card-text> <v-card-text>
<v-select v-model="selectedFaceStatus" :items="['Associado', 'Não Associado']" label="Filtrar Rostos"></v-select>
<v-list> <v-list>
<v-list-item v-for="(face, index) in identifiedFaces" :key="index"> <v-list-item v-for="(face, index) in filteredFaces" :key="index">
<v-list-item-avatar> <v-list-item-avatar>
<v-icon large>mdi-account-circle</v-icon> <v-icon large>mdi-account-circle</v-icon>
</v-list-item-avatar> </v-list-item-avatar>
<v-list-item-content> <v-list-item-content>
{{ face.name }} <v-list-item-title>{{ face.name }}</v-list-item-title>
<v-chip small :color="face.isActive ? 'success' : 'error'"> <v-list-item-subtitle>{{ face.isActive ? 'Associado' : 'Não Associado' }}</v-list-item-subtitle>
{{ face.isActive ? 'Ativo' : 'Inativo' }} <v-btn v-if="!face.isActive" @click="editFaceName(face)">Associar</v-btn>
</v-chip>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -108,6 +129,7 @@
</v-card> </v-card>
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-container> </v-container>
@ -119,15 +141,11 @@ export default {
data() { data() {
return { return {
activeTab: 0, activeTab: 0,
cameraModel: {
name: '',
id: '',
description: '',
registrationDate: null
},
searchCriteria: { searchCriteria: {
id: '', id: '',
name: '' name: '',
status: '',
order: ''
}, },
selectedCameraForTraining: null, selectedCameraForTraining: null,
isTraining: false, isTraining: false,
@ -138,45 +156,35 @@ export default {
{ id: 'CAM003', name: 'Câmera Estacionamento', brand: 'Dahua' } { id: 'CAM003', name: 'Câmera Estacionamento', brand: 'Dahua' }
], ],
cameras: [ cameras: [
{ id: 'CAM001', name: 'Câmera Principal', description: 'Câmera frontal do prédio', isActive: true, registrationDate: '2025-02-03T08:30', isTrained: true }, { id: 'CAM001', name: 'Câmera Principal', description: 'Câmera frontal do prédio', isActive: true, registrationDate: '2025-02-03T 08:30', isTrained: true },
{ id: 'CAM002', name: 'Câmera Corredor', description: 'Câmera do corredor principal', isActive: false, registrationDate: '2025-01-28T14:00', isTrained: false } { id: 'CAM002', name: 'Câmera Corredor', description: 'Câmera do corredor principal', isActive: false, registrationDate: '2025-01-28T 14:00', isTrained: false }
], ],
searchResults: [], searchResults: [],
selectedFaceStatus: 'Associado',
identifiedFaces: [ identifiedFaces: [
{ { id: 'USER001', name: 'João Silva', isActive: true },
id: 'USER001', { id: 'USER002', name: 'Maria Souza', isActive: true }
name: 'João Silva',
isActive: true
},
{
id: 'USER002',
name: 'Maria Souza',
isActive: true
}
], ],
unidentifiedFaces: [ unidentifiedFaces: [
{ selectedUser: null }, { selectedUser: null, name: 'Desconhecido' },
{ selectedUser: null } { selectedUser: null, name: 'Desconhecido' }
], ],
registeredUsers: ['João Silva', 'Maria Souza', 'Pedro Santos', 'Ana Oliveira'] registeredUsers: ['João Silva', 'Maria Souza', 'Pedro Santos', 'Ana Oliveira'],
integrationStatus: 'Pendente',
}; };
}, },
methods: { methods: {
registerCameraModel() {
console.log('Registrando modelo de câmera:', this.cameraModel);
this.cameraModel = {
name: '',
id: '',
description: '',
registrationDate: null
};
},
searchCameraModels() { searchCameraModels() {
console.log('Critérios de pesquisa:', this.searchCriteria);
this.searchResults = this.cameras.filter(camera => this.searchResults = this.cameras.filter(camera =>
(camera.id.includes(this.searchCriteria.id) || camera.name.includes(this.searchCriteria.name)) && (camera.id.includes(this.searchCriteria.id) || camera.name.includes(this.searchCriteria.name)) &&
(!this.searchCriteria.status || (this.searchCriteria.status === 'Ativo' && camera.isActive) || (this.searchCriteria.status === 'Inativo' && !camera.isActive)) (!this.searchCriteria.status || (this.searchCriteria.status === 'Ativo' && camera.isActive) || (this.searchCriteria.status === 'Inativo' && !camera.isActive))
); );
if (this.searchCriteria.order === 'A-Z') {
this.searchResults.sort((a, b) => a.name.localeCompare(b.name));
} else if (this.searchCriteria.order === 'Z-A') {
this.searchResults.sort((a, b) => b.name.localeCompare(a.name));
}
}, },
startTraining() { startTraining() {
if (this.selectedCameraForTraining) { if (this.selectedCameraForTraining) {
@ -210,15 +218,21 @@ export default {
} }
}, },
integrateModel() { integrateModel() {
console.log('Integrando modelo:', this.selectedCameraForTraining); this.integrationStatus = 'Em andamento...';
this.trainingComplete = false; setTimeout(() => {
this.isTraining = false; this.integrationStatus = 'Concluído';
}, }, 3000);
confirmAssignments() {
console.log('Atribuições confirmadas:', this.unidentifiedFaces);
}, },
editFaceName(face) { editFaceName(face) {
console.log('Editando rosto:', face); console.log('Editando rosto:', face);
face.isActive = true;
},
},
computed: {
filteredFaces() {
return this.selectedFaceStatus === 'Associado' ?
this.identifiedFaces.filter(face => face.isActive) :
this.unidentifiedFaces.filter(face => !face.isActive);
} }
} }
} }

View File

@ -1,144 +1,501 @@
<!-- ProfilePage.vue -->
<template> <template>
<v-container> <div class="profile-container">
<v-card class="mx-auto profile-card" max-width="800"> <div class="profile-header">
<v-row no-gutters> <h1 class="profile-title">Perfil do Usuário</h1>
<!-- Seção da foto do perfil --> <p class="profile-subtitle">Gerencie suas informações pessoais</p>
<v-col cols="12" md="4" class="pa-5 text-center">
<v-avatar size="180">
<v-img
:src="user.avatar || 'https://cdn.vuetifyjs.com/images/john.jpg'"
:alt="user.name"
/>
</v-avatar>
</v-col>
<!-- Seção das informações -->
<v-col cols="12" md="8" class="pa-5">
<div class="d-flex justify-space-between align-center mb-6">
<h2 class="text-h4">Perfil do Usuário</h2>
<v-btn
color="#2563eb"
dark
rounded
@click="navigateToEdit"
>
<v-icon left>mdi-pencil</v-icon>
Editar Perfil
</v-btn>
</div> </div>
<v-list> <form @submit.prevent="saveProfile" class="profile-form">
<v-list-item> <div class="profile-grid">
<v-list-item-icon> <!-- Coluna da Foto -->
<v-icon color="#2563eb">mdi-account</v-icon> <div class="photo-section">
</v-list-item-icon> <div class="profile-photo-container">
<v-list-item-content> <div class="photo-wrapper">
<v-list-item-subtitle>Nome</v-list-item-subtitle> <img :src="profileData.photoUrl || defaultPhoto" alt="Foto de Perfil" class="profile-photo">
<v-list-item-title class="text-h6"> <div class="photo-overlay">
{{ user.name }} <span class="photo-text">Alterar foto</span>
</v-list-item-title> </div>
</v-list-item-content> </div>
</v-list-item> <input
type="file"
@change="handlePhotoUpload"
accept="image/*"
ref="photoInput"
class="photo-input"
>
<button type="button" @click="triggerPhotoUpload" class="upload-btn">
<i class="fas fa-camera"></i>
Alterar Foto
</button>
</div>
</div>
<v-list-item> <!-- Coluna das Informações -->
<v-list-item-icon> <div class="info-section">
<v-icon color="#2563eb">mdi-email</v-icon> <!-- Seção de Informações Básicas -->
</v-list-item-icon> <div class="section-title">
<v-list-item-content> <i class="fas fa-user"></i>
<v-list-item-subtitle>Email</v-list-item-subtitle> <h2 class="sub-title">Informações Básicas</h2>
<v-list-item-title class="text-h6"> </div>
{{ user.email }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item> <div class="form-row">
<v-list-item-icon> <div class="form-group">
<v-icon color="#2563eb">mdi-phone</v-icon> <label>ID</label>
</v-list-item-icon> <input type="text" v-model="profileData.id" readonly class="form-input readonly">
<v-list-item-content> </div>
<v-list-item-subtitle>Telefone</v-list-item-subtitle> </div>
<v-list-item-title class="text-h6">
{{ user.phone || 'Não informado' }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item> <div class="form-row">
<v-list-item-icon> <div class="form-group">
<v-icon color="#2563eb">mdi-badge-account</v-icon> <label>Nome</label>
</v-list-item-icon> <input
<v-list-item-content> type="text"
<v-list-item-subtitle>Cargo</v-list-item-subtitle> v-model="profileData.firstName"
<v-list-item-title class="text-h6"> required
{{ user.role || 'Não informado' }} class="form-input"
</v-list-item-title> placeholder="Digite seu nome"
</v-list-item-content> >
</v-list-item> </div>
<v-list-item> <div class="form-group">
<v-list-item-icon> <label>Sobrenome</label>
<v-icon color="#2563eb">mdi-office-building</v-icon> <input
</v-list-item-icon> type="text"
<v-list-item-content> v-model="profileData.lastName"
<v-list-item-subtitle>Departamento</v-list-item-subtitle> required
<v-list-item-title class="text-h6"> class="form-input"
{{ user.department || 'Não informado' }} placeholder="Digite seu sobrenome"
</v-list-item-title> >
</v-list-item-content> </div>
</v-list-item> </div>
</v-list>
</v-col> <!-- Seção de Contato -->
</v-row> <div class="section-title">
</v-card> <i class="fas fa-address-card"></i>
</v-container> <h2>Informações de Contato</h2>
</div>
<div class="form-row">
<div class="form-group">
<label>Email</label>
<input
type="email"
v-model="profileData.email"
required
class="form-input"
placeholder="seu@email.com"
>
</div>
<div class="form-group">
<label>Telefone</label>
<input
type="tel"
v-model="profileData.phone"
v-mask="'(##) #####-####'"
class="form-input"
placeholder="(00) 00000-0000"
>
</div>
</div>
<!-- Seção Profissional -->
<div class="section-title">
<i class="fas fa-briefcase"></i>
<h2>Informações Profissionais</h2>
</div>
<div class="form-row">
<div class="form-group">
<label>Cargo</label>
<select v-model="profileData.role" class="form-select">
<option value="" disabled>Selecione um cargo</option>
<option v-for="role in roles" :key="role.id" :value="role.id">
{{ role.name }}
</option>
</select>
</div>
<div class="form-group">
<label>Grupo</label>
<select v-model="profileData.group" class="form-select">
<option value="" disabled>Selecione um grupo</option>
<option v-for="group in groups" :key="group.id" :value="group.id">
{{ group.name }}
</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Permissão</label>
<select v-model="profileData.permission" class="form-select">
<option value="" disabled>Selecione uma permissão</option>
<option v-for="permission in permissions" :key="permission.id" :value="permission.id">
{{ permission.name }}
</option>
</select>
</div>
<div class="form-group">
<label>Turno</label>
<select v-model="profileData.shift" class="form-select">
<option value="" disabled>Selecione um turno</option>
<option value="morning">Manhã</option>
<option value="afternoon">Tarde</option>
<option value="night">Noite</option>
</select>
</div>
</div>
</div>
</div>
<!-- Botões de Ação -->
<div class="button-group">
<button type="button" @click="resetForm" class="cancel-btn">
<i class="fas fa-times"></i>
Cancelar
</button>
<button type="submit" class="save-btn">
<i class="fas fa-save"></i>
Salvar Alterações
</button>
</div>
</form>
</div>
</template> </template>
<script> <script>
export default { export default {
name: 'UserProfileView', name: 'ProfilePage',
data: () => ({ data() {
user: { return {
name: '', defaultPhoto: '/path/to/default-avatar.png',
profileData: {
id: '',
photoUrl: '',
firstName: '',
lastName: '',
email: '', email: '',
avatar: '',
phone: '', phone: '',
role: '', role: '',
department: '' group: '',
} permission: '',
}), shift: ''
created() { },
// Carregar dados do usuário do localStorage roles: [
const userData = JSON.parse(localStorage.getItem('user')) { id: 1, name: 'Funcionário' },
if (userData) { { id: 2, name: 'Estagiário' },
this.user = { ...this.user, ...userData } { id: 3, name: 'Gerente' }
],
groups: [
{ id: 1, name: 'Beta' },
{ id: 2, name: 'Alfa' },
{ id: 3, name: 'Omega' }
],
permissions: [
{ id: 1, name: 'Administrador' },
{ id: 2, name: 'Editor' },
{ id: 3, name: 'Visualizador' }
]
} }
}, },
methods: { methods: {
navigateToEdit() { async loadProfile() {
this.$router.push('/edit-user') try {
const response = await fetch('/api/profile');
const data = await response.json();
this.profileData = { ...data };
} catch (error) {
console.error('Erro ao carregar perfil:', error);
} }
},
triggerPhotoUpload() {
this.$refs.photoInput.click();
},
handlePhotoUpload(event) {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
this.profileData.photoUrl = e.target.result;
};
reader.readAsDataURL(file);
}
},
async saveProfile() {
try {
await fetch('/api/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.profileData)
});
this.$emit('profile-updated');
this.showNotification('Perfil atualizado com sucesso!', 'success');
} catch (error) {
console.error('Erro ao salvar perfil:', error);
this.showNotification('Erro ao salvar perfil. Tente novamente.', 'error');
}
},
resetForm() {
this.loadProfile();
},
showNotification(message,_type) {
// Implementar sistema de notificação de sua preferência
alert(message);
}
},
created() {
this.loadProfile();
} }
} }
</script> </script>
<style scoped> <style scoped>
.profile-card { .profile-container {
border-radius: 16px; max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.profile-header {
text-align: center;
margin-bottom: 40px;
}
.profile-title {
font-size: 2.5rem;
color: #2c3e50;
margin-bottom: 10px;
}
.profile-subtitle {
color: #7f8c8d;
font-size: 1.1rem;
}
.profile-form {
background: #fff;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden; overflow: hidden;
} }
.v-list-item { .profile-grid {
padding: 16px 0; display: grid;
grid-template-columns: 300px 1fr;
gap: 30px;
} }
.v-list-item__icon { .photo-section {
margin-right: 16px; padding: 30px;
background: #f8f9fa;
border-right: 1px solid #eee;
} }
.v-list-item__subtitle { .info-section {
color: #666; padding: 30px;
}
.photo-wrapper {
position: relative;
width: 200px;
height: 200px;
margin: 0 auto 20px;
border-radius: 50%;
overflow: hidden;
cursor: pointer;
}
.profile-photo {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.photo-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.photo-text {
color: white;
font-size: 0.9rem; font-size: 0.9rem;
margin-bottom: 4px; font-weight: 500;
}
.photo-wrapper:hover .photo-overlay {
opacity: 1;
}
.photo-wrapper:hover .profile-photo {
transform: scale(1.1);
}
.section-title {
display: flex;
align-items: center;
gap: 10px;
margin: 30px 0 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e1e8ed;
}
.section-title i {
color: #3498db;
font-size: 1.2rem;
}
.section-title h2 {
font-size: 1.2rem;
color: #2c3e50;
margin: 0;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 0;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #34495e;
}
.form-input,
.form-select {
width: 100%;
padding: 12px;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
}
.form-input:focus,
.form-select:focus {
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
outline: none;
}
.form-input.readonly {
background-color: #f8f9fa;
cursor: not-allowed;
color: #7f8c8d;
}
.photo-input {
display: none;
}
.upload-btn {
width: 100%;
padding: 12px;
background: #3498db;
color: white;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background 0.3s ease;
}
.upload-btn:hover {
background: #2980b9;
}
.button-group {
display: flex;
gap: 15px;
justify-content: flex-end;
padding: 30px;
background: #f8f9fa;
border-top: 1px solid #eee;
}
.save-btn,
.cancel-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.3s ease;
}
.save-btn {
background: #2ecc71;
color: white;
}
.save-btn:hover {
background: #27ae60;
transform: translateY(-1px);
}
.cancel-btn {
background: #e74c3c;
color: white;
}
.cancel-btn:hover {
background: #c0392b;
transform: translateY(-1px);
}
@media (max-width: 768px) {
.profile-grid {
grid-template-columns: 1fr;
}
.photo-section {
border-right: none;
border-bottom: 1px solid #eee;
}
.form-row {
grid-template-columns: 1fr;
}
.button-group {
flex-direction: column-reverse;
}
.save-btn,
.cancel-btn {
width: 100%;
justify-content: center;
}
} }
</style> </style>