versão 14h21
This commit is contained in:
parent
f38df351ce
commit
cb4a5ab5ab
90
package-lock.json
generated
90
package-lock.json
generated
@ -9,6 +9,8 @@
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"axios": "^1.7.9",
|
||||
"pinia": "^2.1.3",
|
||||
"vue": "^3.3.4",
|
||||
@ -2780,6 +2782,94 @@
|
||||
"vue-component-type-helpers": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vuelidate/core": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.3.tgz",
|
||||
"integrity": "sha512-AN6l7KF7+mEfyWG0doT96z+47ljwPpZfi9/JrNMkOGLFv27XVZvKzRLXlmDPQjPl/wOB1GNnHuc54jlCLRNqGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": "^0.13.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^2.0.0 || >=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vuelidate/core/node_modules/vue-demi": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vuelidate/validators": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.4.tgz",
|
||||
"integrity": "sha512-odTxtUZ2JpwwiQ10t0QWYJkkYrfd0SyFYhdHH44QQ1jDatlZgTh/KRzrWVmn/ib9Gq7H4hFD4e8ahoo5YlUlDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vue-demi": "^0.13.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^2.0.0 || >=3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vuelidate/validators/node_modules/vue-demi": {
|
||||
"version": "0.13.11",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
|
||||
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/abab": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
"@vuelidate/core": "^2.0.3",
|
||||
"@vuelidate/validators": "^2.0.4",
|
||||
"axios": "^1.7.9",
|
||||
"pinia": "^2.1.3",
|
||||
"vue": "^3.3.4",
|
||||
|
||||
53
src/App.vue
53
src/App.vue
@ -1,5 +1,4 @@
|
||||
<template>
|
||||
<!-- Anterior template permanece o mesmo até a parte do menu -->
|
||||
<v-app>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
@ -26,12 +25,12 @@
|
||||
<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">
|
||||
<div class="profile-section px-4 py-3" @click="goToUserProfile">
|
||||
<div class="d-flex align-center" :class="{ 'justify-center': isCollapsed }">
|
||||
<v-avatar color="primary" size="40">
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</v-avatar>
|
||||
<div class="ml-3">
|
||||
<div v-if="!isCollapsed" 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>
|
||||
@ -40,18 +39,10 @@
|
||||
|
||||
<v-divider class="border-opacity-25"></v-divider>
|
||||
|
||||
<!-- Navigation Menu -->
|
||||
<!-- Flattened 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"
|
||||
>
|
||||
{{ category.title }}
|
||||
</v-list-subheader>
|
||||
|
||||
<v-list-item
|
||||
v-for="item in category.items"
|
||||
v-for="item in flattenedMenuItems"
|
||||
:key="item.name"
|
||||
:to="item.route"
|
||||
:value="item.name"
|
||||
@ -76,7 +67,6 @@
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
|
||||
<!-- Bottom Actions -->
|
||||
@ -125,7 +115,7 @@
|
||||
<v-btn
|
||||
color="error"
|
||||
variant="tonal"
|
||||
@click="logout"
|
||||
@click="handleLogout"
|
||||
prepend-icon="mdi-logout"
|
||||
class="mr-2"
|
||||
>
|
||||
@ -156,10 +146,7 @@ export default {
|
||||
return {
|
||||
drawer: true,
|
||||
isCollapsed: false,
|
||||
menuCategories: [
|
||||
{
|
||||
title: 'Principal',
|
||||
items: [
|
||||
flattenedMenuItems: [
|
||||
{
|
||||
name: 'home',
|
||||
route: { name: 'home' },
|
||||
@ -179,12 +166,7 @@ export default {
|
||||
icon: 'mdi-cctv',
|
||||
label: 'Monitoramento',
|
||||
badge: { text: '12', color: 'info' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Gestão',
|
||||
items: [
|
||||
{
|
||||
name: 'analytics',
|
||||
route: { name: 'analytics' },
|
||||
@ -203,12 +185,7 @@ export default {
|
||||
icon: 'mdi-bell-ring',
|
||||
label: 'Alertas',
|
||||
badge: { text: '3', color: 'error' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Desenvolvimento',
|
||||
items: [
|
||||
{
|
||||
name: 'testing',
|
||||
route: { name: 'testing' },
|
||||
@ -217,16 +194,11 @@ export default {
|
||||
badge: { text: 'Dev', color: 'warning' }
|
||||
},
|
||||
{
|
||||
name: 'register',
|
||||
route: { name: 'register' },
|
||||
name: 'register-user-cam',
|
||||
route: { name: 'register-user-cam' },
|
||||
icon: 'mdi-file-document-plus',
|
||||
label: 'Registro'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Configurações',
|
||||
items: [
|
||||
{
|
||||
name: 'cameras',
|
||||
route: { name: 'cameras' },
|
||||
@ -246,8 +218,6 @@ export default {
|
||||
label: 'Configurações'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
@ -258,6 +228,10 @@ export default {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
},
|
||||
|
||||
goToUserProfile() {
|
||||
this.$router.push({ name: 'user-profile' });
|
||||
},
|
||||
|
||||
async handleLogout() {
|
||||
try {
|
||||
await this.logout();
|
||||
@ -281,6 +255,7 @@ export default {
|
||||
|
||||
.profile-section {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.v-list-item--active {
|
||||
|
||||
18
src/services/profileservice.js
Normal file
18
src/services/profileservice.js
Normal file
@ -0,0 +1,18 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export default function useProfileService() {
|
||||
const getProfile = async () => {
|
||||
const response = await axios.get('/api/profile')
|
||||
return response.data
|
||||
}
|
||||
|
||||
const updateProfile = async (profileData) => {
|
||||
const response = await axios.put('/api/profile', profileData)
|
||||
return response.data
|
||||
}
|
||||
|
||||
return {
|
||||
getProfile,
|
||||
updateProfile
|
||||
}
|
||||
}
|
||||
24
src/services/useprofileservice.js
Normal file
24
src/services/useprofileservice.js
Normal file
@ -0,0 +1,24 @@
|
||||
import api from './api'
|
||||
|
||||
export default function useProfileService() {
|
||||
return {
|
||||
async getProfile() {
|
||||
try {
|
||||
const response = await api.get('/profile')
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
throw error
|
||||
}
|
||||
},
|
||||
async updateProfile(profileData) {
|
||||
try {
|
||||
const response = await api.put('/profile', profileData)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Failed to update profile:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,104 +1,236 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- Aba de Cadastro de Câmera -->
|
||||
<div class="camera-registration">
|
||||
<h2>Cadastro de Câmera</h2>
|
||||
<form @submit.prevent="handleCameraSubmit" ref="cameraForm">
|
||||
<div>
|
||||
<label for="modelName">Nome do Modelo:</label>
|
||||
<input v-model="cameraForm.modelName" type="text" id="modelName" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="id">ID:</label>
|
||||
<input v-model="cameraForm.id" type="text" id="id" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="responsible">Responsável pelo Cadastro:</label>
|
||||
<input v-model="cameraForm.responsible" type="text" id="responsible" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="description">Descrição:</label>
|
||||
<textarea v-model="cameraForm.description" id="description" required></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="date">Data:</label>
|
||||
<input v-model="cameraForm.date" type="date" id="date" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="time">Hora:</label>
|
||||
<input v-model="cameraForm.time" type="time" id="time" required />
|
||||
</div>
|
||||
<button type="submit">Cadastrar</button>
|
||||
</form>
|
||||
<p v-if="cameraRegistered" class="success-message">Cadastro de câmera concluído!</p>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<!-- Camera Registration -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon left>mdi-camera</v-icon>
|
||||
Cadastro de Câmera
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="cameraForm" v-model="cameraFormValid" lazy-validation @submit.prevent="handleCameraSubmit">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="cameraForm.modelName"
|
||||
:rules="[v => !!v || 'Nome do modelo é obrigatório']"
|
||||
label="Nome do Modelo"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="cameraForm.id"
|
||||
:rules="[v => !!v || 'ID é obrigatório']"
|
||||
label="ID"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
v-model="cameraForm.responsible"
|
||||
:rules="[v => !!v || 'Responsável é obrigatório']"
|
||||
label="Responsável pelo Cadastro"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="cameraForm.description"
|
||||
:rules="[v => !!v || 'Descrição é obrigatória']"
|
||||
label="Descrição"
|
||||
required
|
||||
></v-textarea>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="cameraForm.date"
|
||||
type="date"
|
||||
:rules="[v => !!v || 'Data é obrigatória']"
|
||||
label="Data"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="cameraForm.time"
|
||||
type="time"
|
||||
:rules="[v => !!v || 'Hora é obrigatória']"
|
||||
label="Hora"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!cameraFormValid"
|
||||
>
|
||||
Cadastrar Câmera
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<h3>Últimos Cadastros de Câmeras</h3>
|
||||
<ul>
|
||||
<li v-for="(camera, index) in recentCameras" :key="index">
|
||||
{{ camera.modelName }} - {{ camera.id }} (Responsável: {{ camera.responsible }})
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Recent Cameras -->
|
||||
<v-card class="mt-4" v-if="recentCameras.length">
|
||||
<v-card-title>Últimos Cadastros de Câmeras</v-card-title>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(camera, index) in recentCameras"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ camera.modelName }} - {{ camera.id }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
Responsável: {{ camera.responsible }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Aba de Cadastro de Usuário -->
|
||||
<div class="user-registration">
|
||||
<h2>Cadastro de Usuário</h2>
|
||||
<form @submit.prevent="handleUserSubmit" ref="userForm">
|
||||
<div>
|
||||
<label for="profilePicture">Foto de Perfil:</label>
|
||||
<input type="file" @change="handleProfilePictureChange" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="firstName">Nome:</label>
|
||||
<input v-model="userForm.firstName" type="text" id="firstName" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="lastName">Sobrenome:</label>
|
||||
<input v-model="userForm.lastName" type="text" id="lastName" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="userId">ID:</label>
|
||||
<input v-model="userForm.userId" type="text" id="userId" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="email">E-mail:</label>
|
||||
<input v-model="userForm.email" type="email" id="email" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="phone">Telefone:</label>
|
||||
<input v-model="userForm.phone" type="text" id="phone" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="group">Grupo:</label>
|
||||
<input v-model="userForm.group" type="text" id="group" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="role">Cargo:</label>
|
||||
<input v-model="userForm.role" type="text" id="role" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="permissions">Permissão:</label>
|
||||
<select v-model="userForm.permissions" id="permissions" required>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">Usuário</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="shift">Turno:</label>
|
||||
<input v-model="userForm.shift" type="text" id="shift" required />
|
||||
</div>
|
||||
<button type="submit">Cadastrar</button>
|
||||
</form>
|
||||
<p v-if="userRegistered" class="success-message">Cadastro de usuário concluído!</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- User Registration -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-icon left>mdi-account-plus</v-icon>
|
||||
Cadastro de Usuário
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-form ref="userForm" v-model="userFormValid" lazy-validation @submit.prevent="handleUserSubmit">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-file-input
|
||||
v-model="userForm.profilePicture"
|
||||
label="Foto de Perfil"
|
||||
prepend-icon="mdi-camera"
|
||||
accept="image/*"
|
||||
@change="handleProfilePictureChange"
|
||||
></v-file-input>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="userForm.firstName"
|
||||
:rules="[v => !!v || 'Nome é obrigatório']"
|
||||
label="Nome"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="userForm.lastName"
|
||||
:rules="[v => !!v || 'Sobrenome é obrigatório']"
|
||||
label="Sobrenome"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="userForm.userId"
|
||||
:rules="[v => !!v || 'ID é obrigatório']"
|
||||
label="ID"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="userForm.email"
|
||||
:rules="[v => !!v || 'E-mail é obrigatório', v => /.+@.+\..+/.test(v) || 'E-mail inválido']"
|
||||
label="E-mail"
|
||||
type="email"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="userForm.phone"
|
||||
:rules="[v => !!v || 'Telefone é obrigatório']"
|
||||
label="Telefone"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="userForm.group"
|
||||
:rules="[v => !!v || 'Grupo é obrigatório']"
|
||||
label="Grupo"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="userForm.role"
|
||||
:rules="[v => !!v || 'Cargo é obrigatório']"
|
||||
label="Cargo"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-select
|
||||
v-model="userForm.permissions"
|
||||
:items="['Admin', 'Usuário']"
|
||||
:rules="[v => !!v || 'Permissão é obrigatória']"
|
||||
label="Permissão"
|
||||
required
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
v-model="userForm.shift"
|
||||
:rules="[v => !!v || 'Turno é obrigatório']"
|
||||
label="Turno"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!userFormValid"
|
||||
>
|
||||
Cadastrar Usuário
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Snackbars for Feedback -->
|
||||
<v-snackbar v-model="cameraRegistered" color="success">
|
||||
Cadastro de câmera concluído!
|
||||
<template v-slot:actions>
|
||||
<v-btn text @click="cameraRegistered = false">Fechar</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
|
||||
<v-snackbar v-model="userRegistered" color="success">
|
||||
Cadastro de usuário concluído!
|
||||
<template v-slot:actions>
|
||||
<v-btn text @click="userRegistered = false">Fechar</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RegisterUserCamView',
|
||||
data() {
|
||||
return {
|
||||
// Dados do Formulário de Cadastro de Câmera
|
||||
cameraFormValid: false,
|
||||
userFormValid: false,
|
||||
cameraForm: {
|
||||
modelName: '',
|
||||
id: '',
|
||||
@ -107,8 +239,8 @@ export default {
|
||||
date: '',
|
||||
time: ''
|
||||
},
|
||||
// Dados do Formulário de Cadastro de Usuário
|
||||
userForm: {
|
||||
profilePicture: null,
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
userId: '',
|
||||
@ -119,63 +251,36 @@ export default {
|
||||
permissions: '',
|
||||
shift: ''
|
||||
},
|
||||
// Variáveis de controle de sucesso
|
||||
cameraRegistered: false,
|
||||
userRegistered: false,
|
||||
// Arrays para armazenar os cadastros
|
||||
recentCameras: [],
|
||||
recentCameras: []
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleCameraSubmit() {
|
||||
// Valida se todos os campos foram preenchidos
|
||||
if (this.isFormValid(this.cameraForm)) {
|
||||
this.recentCameras.push({ ...this.cameraForm });
|
||||
if (this.$refs.cameraForm.validate()) {
|
||||
this.recentCameras.unshift({ ...this.cameraForm });
|
||||
|
||||
// Limit to 3 recent cameras
|
||||
if (this.recentCameras.length > 3) {
|
||||
this.recentCameras.shift(); // Mantém apenas os 3 últimos cadastros
|
||||
this.recentCameras.pop();
|
||||
}
|
||||
|
||||
this.cameraRegistered = true;
|
||||
this.resetCameraForm();
|
||||
this.$refs.cameraForm.reset();
|
||||
}
|
||||
},
|
||||
handleUserSubmit() {
|
||||
// Valida se todos os campos foram preenchidos
|
||||
if (this.isFormValid(this.userForm)) {
|
||||
if (this.$refs.userForm.validate()) {
|
||||
this.userRegistered = true;
|
||||
this.resetUserForm();
|
||||
this.$refs.userForm.reset();
|
||||
}
|
||||
},
|
||||
isFormValid(form) {
|
||||
return Object.values(form).every(value => value !== '');
|
||||
},
|
||||
resetCameraForm() {
|
||||
this.cameraForm = {
|
||||
modelName: '',
|
||||
id: '',
|
||||
responsible: '',
|
||||
description: '',
|
||||
date: '',
|
||||
time: ''
|
||||
};
|
||||
},
|
||||
resetUserForm() {
|
||||
this.userForm = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
userId: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
group: '',
|
||||
role: '',
|
||||
permissions: '',
|
||||
shift: ''
|
||||
};
|
||||
},
|
||||
handleProfilePictureChange(event) {
|
||||
const file = event.target.files[0];
|
||||
if (file) {
|
||||
handleProfilePictureChange(files) {
|
||||
if (files && files.length) {
|
||||
const file = files[0];
|
||||
console.log("Foto de perfil selecionada:", file.name);
|
||||
// Aqui você pode realizar o upload da foto
|
||||
// Additional file handling logic can be added here
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -183,40 +288,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
color: green;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
.v-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
@ -58,7 +58,7 @@
|
||||
<v-spacer></v-spacer>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<template #item_actions="{ item }">
|
||||
<v-btn icon color="primary" @click="viewDetails(item)">
|
||||
<v-icon>mdi-eye</v-icon>
|
||||
</v-btn>
|
||||
|
||||
@ -1,166 +1,218 @@
|
||||
<!-- ProfilePage.vue -->
|
||||
<template>
|
||||
<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>
|
||||
<h1 class="profile-title">{{ $t('profile.title') }}</h1>
|
||||
<p class="profile-subtitle">{{ $t('profile.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="saveProfile" class="profile-form">
|
||||
<form
|
||||
@submit.prevent="submitProfile"
|
||||
class="profile-form"
|
||||
novalidate
|
||||
aria-labelledby="profile-form-title"
|
||||
>
|
||||
<div class="profile-grid">
|
||||
<!-- Coluna da Foto -->
|
||||
<!-- Photo Section with Advanced Upload -->
|
||||
<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
|
||||
class="profile-photo-wrapper"
|
||||
:class="{ 'has-error': photoError }"
|
||||
>
|
||||
<img
|
||||
:src="previewPhotoUrl || defaultPhoto"
|
||||
:alt="$t('profile.photoAlt')"
|
||||
class="profile-photo"
|
||||
>
|
||||
<div
|
||||
class="photo-upload-overlay"
|
||||
@click="openFileDialog"
|
||||
@keydown.enter="openFileDialog"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
:aria-label="$t('profile.changePhoto')"
|
||||
>
|
||||
<i class="fas fa-camera"></i>
|
||||
<span>{{ $t('profile.changePhoto') }}</span>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
@change="handlePhotoUpload"
|
||||
accept="image/*"
|
||||
ref="photoInput"
|
||||
class="photo-input"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
@change="handlePhotoUpload"
|
||||
class="hidden-file-input"
|
||||
>
|
||||
<button type="button" @click="triggerPhotoUpload" class="upload-btn">
|
||||
<i class="fas fa-camera"></i>
|
||||
Alterar Foto
|
||||
</button>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="photoError" class="error-text">
|
||||
{{ photoError }}
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- Coluna das Informações -->
|
||||
<!-- Profile Information Section -->
|
||||
<div class="profile-information">
|
||||
<!-- Basic Information -->
|
||||
<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>
|
||||
<h2 class="section-heading">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
{{ $t('profile.basicInfo') }}
|
||||
</h2>
|
||||
|
||||
<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>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Nome</label>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="firstName">{{ $t('profile.firstName') }}</label>
|
||||
<input
|
||||
id="firstName"
|
||||
v-model.trim="profileData.firstName"
|
||||
type="text"
|
||||
v-model="profileData.firstName"
|
||||
:class="{ 'is-invalid': v$.firstName.$error }"
|
||||
@blur="v$.firstName.$touch()"
|
||||
required
|
||||
class="form-input"
|
||||
placeholder="Digite seu nome"
|
||||
>
|
||||
<div
|
||||
v-if="v$.firstName.$error"
|
||||
class="validation-error"
|
||||
>
|
||||
{{ $t('validation.requiredField') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Sobrenome</label>
|
||||
<div class="form-field">
|
||||
<label for="lastName">{{ $t('profile.lastName') }}</label>
|
||||
<input
|
||||
id="lastName"
|
||||
v-model.trim="profileData.lastName"
|
||||
type="text"
|
||||
v-model="profileData.lastName"
|
||||
:class="{ 'is-invalid': v$.lastName.$error }"
|
||||
@blur="v$.lastName.$touch()"
|
||||
required
|
||||
class="form-input"
|
||||
placeholder="Digite seu sobrenome"
|
||||
>
|
||||
<div
|
||||
v-if="v$.lastName.$error"
|
||||
class="validation-error"
|
||||
>
|
||||
{{ $t('validation.requiredField') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Seção de Contato -->
|
||||
<div class="section-title">
|
||||
<i class="fas fa-address-card"></i>
|
||||
<h2>Informações de Contato</h2>
|
||||
</div>
|
||||
<!-- Contact Information -->
|
||||
<div class="info-section">
|
||||
<h2 class="section-heading">
|
||||
<i class="fas fa-envelope"></i>
|
||||
{{ $t('profile.contactInfo') }}
|
||||
</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Email</label>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="email">{{ $t('profile.email') }}</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model.trim="profileData.email"
|
||||
type="email"
|
||||
v-model="profileData.email"
|
||||
:class="{ 'is-invalid': v$.email.$error }"
|
||||
@blur="v$.email.$touch()"
|
||||
required
|
||||
class="form-input"
|
||||
placeholder="seu@email.com"
|
||||
>
|
||||
<div
|
||||
v-if="v$.email.$error"
|
||||
class="validation-error"
|
||||
>
|
||||
{{ $t('validation.invalidEmail') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Telefone</label>
|
||||
<div class="form-field">
|
||||
<label for="phone">{{ $t('profile.phone') }}</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
v-model="profileData.phone"
|
||||
type="tel"
|
||||
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">
|
||||
<!-- Professional Information -->
|
||||
<div class="info-section">
|
||||
<h2 class="section-heading">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
{{ $t('profile.professionalInfo') }}
|
||||
</h2>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label for="role">{{ $t('profile.role') }}</label>
|
||||
<select
|
||||
id="role"
|
||||
v-model="profileData.role"
|
||||
:class="{ 'is-invalid': v$.role.$error }"
|
||||
@blur="v$.role.$touch()"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>
|
||||
{{ $t('profile.selectRole') }}
|
||||
</option>
|
||||
<option
|
||||
v-for="role in roles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
>
|
||||
{{ role.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div
|
||||
v-if="v$.role.$error"
|
||||
class="validation-error"
|
||||
>
|
||||
{{ $t('validation.requiredField') }}
|
||||
</div>
|
||||
</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">
|
||||
<div class="form-field">
|
||||
<label for="group">{{ $t('profile.group') }}</label>
|
||||
<select
|
||||
id="group"
|
||||
v-model="profileData.group"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{{ $t('profile.selectGroup') }}
|
||||
</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">
|
||||
<!-- Action Buttons -->
|
||||
<div class="form-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
@click="resetForm"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
Cancelar
|
||||
{{ $t('profile.cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="save-btn">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary"
|
||||
:disabled="isSubmitting || !isFormValid"
|
||||
>
|
||||
<i class="fas fa-save"></i>
|
||||
Salvar Alterações
|
||||
{{ $t('profile.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -168,8 +220,28 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from 'vue'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { required, email } from '@vuelidate/validators'
|
||||
import useProfileService from '@/services/profileService'
|
||||
|
||||
export default {
|
||||
name: 'ProfilePage',
|
||||
setup() {
|
||||
const profileService = useProfileService()
|
||||
const v$ = useVuelidate()
|
||||
const previewPhotoUrl = ref(null)
|
||||
const photoError = ref(null)
|
||||
const isSubmitting = ref(false)
|
||||
|
||||
return {
|
||||
v$,
|
||||
previewPhotoUrl,
|
||||
photoError,
|
||||
isSubmitting,
|
||||
profileService
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaultPhoto: '/path/to/default-avatar.png',
|
||||
@ -181,321 +253,208 @@ export default {
|
||||
email: '',
|
||||
phone: '',
|
||||
role: '',
|
||||
group: '',
|
||||
permission: '',
|
||||
shift: ''
|
||||
group: ''
|
||||
},
|
||||
roles: [
|
||||
{ id: 1, name: 'Funcionário' },
|
||||
{ id: 2, name: 'Estagiário' },
|
||||
{ id: 3, name: 'Gerente' }
|
||||
{ id: 1, name: 'Employee' },
|
||||
{ id: 2, name: 'Intern' },
|
||||
{ id: 3, name: 'Manager' }
|
||||
],
|
||||
groups: [
|
||||
{ id: 1, name: 'Beta' },
|
||||
{ id: 2, name: 'Alfa' },
|
||||
{ id: 2, name: 'Alpha' },
|
||||
{ id: 3, name: 'Omega' }
|
||||
],
|
||||
permissions: [
|
||||
{ id: 1, name: 'Administrador' },
|
||||
{ id: 2, name: 'Editor' },
|
||||
{ id: 3, name: 'Visualizador' }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isFormValid() {
|
||||
return !this.v$.$error &&
|
||||
this.profileData.firstName &&
|
||||
this.profileData.lastName &&
|
||||
this.profileData.email &&
|
||||
this.profileData.role
|
||||
}
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
profileData: {
|
||||
firstName: { required },
|
||||
lastName: { required },
|
||||
email: { required, email },
|
||||
role: { required }
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadProfile() {
|
||||
try {
|
||||
const response = await fetch('/api/profile');
|
||||
const data = await response.json();
|
||||
this.profileData = { ...data };
|
||||
const profile = await this.profileService.getProfile()
|
||||
this.profileData = { ...profile }
|
||||
this.previewPhotoUrl = profile.photoUrl
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar perfil:', error);
|
||||
this.handleError(error)
|
||||
}
|
||||
},
|
||||
triggerPhotoUpload() {
|
||||
this.$refs.photoInput.click();
|
||||
openFileDialog() {
|
||||
this.$refs.photoInput.click()
|
||||
},
|
||||
handlePhotoUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
const file = event.target.files[0]
|
||||
this.photoError = null
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
||||
const validTypes = ['image/jpeg', 'image/png', 'image/webp']
|
||||
|
||||
if (file.size > maxSize) {
|
||||
this.photoError = this.$t('validation.photoSizeTooLarge')
|
||||
return
|
||||
}
|
||||
|
||||
if (!validTypes.includes(file.type)) {
|
||||
this.photoError = this.$t('validation.invalidPhotoType')
|
||||
return
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (e) => {
|
||||
this.profileData.photoUrl = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
this.previewPhotoUrl = e.target.result
|
||||
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)
|
||||
});
|
||||
async submitProfile() {
|
||||
this.v$.$touch()
|
||||
|
||||
this.$emit('profile-updated');
|
||||
this.showNotification('Perfil atualizado com sucesso!', 'success');
|
||||
if (this.v$.$invalid) return
|
||||
|
||||
this.isSubmitting = true
|
||||
|
||||
try {
|
||||
await this.profileService.updateProfile(this.profileData)
|
||||
this.$emit('profile-updated')
|
||||
this.$toast.success(this.$t('profile.updateSuccess'))
|
||||
} catch (error) {
|
||||
console.error('Erro ao salvar perfil:', error);
|
||||
this.showNotification('Erro ao salvar perfil. Tente novamente.', 'error');
|
||||
this.handleError(error)
|
||||
} finally {
|
||||
this.isSubmitting = false
|
||||
}
|
||||
},
|
||||
resetForm() {
|
||||
this.loadProfile();
|
||||
this.v$.$reset()
|
||||
this.loadProfile()
|
||||
this.previewPhotoUrl = this.profileData.photoUrl
|
||||
},
|
||||
showNotification(message,_type) {
|
||||
// Implementar sistema de notificação de sua preferência
|
||||
alert(message);
|
||||
handleError(error) {
|
||||
console.error('Profile error:', error)
|
||||
this.$toast.error(
|
||||
error.response?.data?.message ||
|
||||
this.$t('profile.updateError')
|
||||
)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadProfile();
|
||||
this.loadProfile()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-container {
|
||||
max-width: 1200px;
|
||||
max-width: 800px;
|
||||
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;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.profile-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 300px 1fr;
|
||||
gap: 30px;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.photo-section {
|
||||
padding: 30px;
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.photo-wrapper {
|
||||
.profile-photo-wrapper {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 20px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.profile-photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.photo-overlay {
|
||||
.photo-upload-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 {
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(0,0,0,0.5);
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0 0 100px 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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 {
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
.form-field {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-field label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-field input,
|
||||
.form-field select {
|
||||
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;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background: #2980b9;
|
||||
.is-invalid {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
.validation-error {
|
||||
color: #dc3545;
|
||||
font-size: 0.8rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: flex-end;
|
||||
padding: 30px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #eee;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.save-btn,
|
||||
.cancel-btn {
|
||||
padding: 12px 24px;
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #2ecc71;
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: #27ae60;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #e74c3c;
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
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;
|
||||
}
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@ -1,22 +1,23 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import path from 'path'
|
||||
import json from '@rollup/plugin-json'
|
||||
|
||||
// Correção para o __dirname não definido em Vite
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname } from 'path'
|
||||
|
||||
// Definindo __dirname manualmente
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
return {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
port: env.VITE_PORT || 5173,
|
||||
strictPort: true,
|
||||
fs: {
|
||||
strict: false,
|
||||
@ -35,11 +36,19 @@ export default defineConfig({
|
||||
extensions: ['.js', '.json', '.jsx', '.mjs', '.ts', '.tsx', '.vue'],
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['vue', 'vue-router', 'vuetify', 'msw'],
|
||||
include: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'vuetify',
|
||||
'msw',
|
||||
'@vuelidate/core',
|
||||
'@vuelidate/validators'
|
||||
],
|
||||
},
|
||||
define: {
|
||||
'process.env': {
|
||||
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
|
||||
NODE_ENV: JSON.stringify(env.NODE_ENV || 'development'),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user