gestione profilo utente

This commit is contained in:
2025-12-17 11:15:29 +01:00
parent 2f04d07928
commit 9c58336cb9
13 changed files with 720 additions and 1 deletions

View File

@@ -0,0 +1,134 @@
import { type Ref, computed, defineComponent, inject, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useVuelidate } from '@vuelidate/core';
import { email, maxLength, minLength, required } from '@vuelidate/validators';
import { type IUtenteApp } from '@/shared/model/utente-app.model';
import { Ruolo } from '@/shared/model/enumerations/ruolo.model';
import UtenteAppService from '@/entities/utente-app/utente-app.service';
import { useStore } from '@/store';
const validations = {
utenteApp: {
nome: {
required,
minLength: minLength(1),
maxLength: maxLength(50),
},
cognome: {
required,
minLength: minLength(1),
maxLength: maxLength(50),
},
dataNascita: {
required,
},
luogoNascita: {
required,
minLength: minLength(1),
maxLength: maxLength(100),
},
residente: {
required,
minLength: minLength(1),
maxLength: maxLength(200),
},
telefono: {
required,
minLength: minLength(8),
maxLength: maxLength(20),
},
societa: {
maxLength: maxLength(100),
},
sede: {
maxLength: maxLength(200),
},
codfiscale: {
minLength: minLength(11),
maxLength: maxLength(16),
},
telefonoSoc: {
minLength: minLength(8),
maxLength: maxLength(20),
},
emailSoc: {
email,
minLength: minLength(5),
maxLength: maxLength(254),
},
},
};
export default defineComponent({
name: 'Profile',
validations,
setup() {
const store = useStore();
const utenteAppService = inject<UtenteAppService>('utenteAppService', () => new UtenteAppService());
const success: Ref<boolean> = ref(false);
const error: Ref<boolean> = ref(false);
const loading: Ref<boolean> = ref(true);
const utenteApp: Ref<IUtenteApp> = ref({});
const currentUser = computed(() => store.account);
onMounted(async () => {
try {
loading.value = true;
const result = await utenteAppService.getCurrentUser();
if (result) {
utenteApp.value = result;
} else {
// Initialize with user data
utenteApp.value = {
username: currentUser.value?.login,
email: currentUser.value?.email,
ruolo: Ruolo.USER,
attivo: true,
};
}
} catch (err) {
// If not found, initialize with user data
utenteApp.value = {
username: currentUser.value?.login,
email: currentUser.value?.email,
ruolo: Ruolo.USER,
attivo: true,
};
} finally {
loading.value = false;
}
});
return {
success,
error,
loading,
utenteApp,
currentUser,
v$: useVuelidate(),
t$: useI18n().t,
utenteAppService,
};
},
methods: {
async save() {
this.success = false;
this.error = false;
try {
await this.utenteAppService.saveCurrentUserProfile(this.utenteApp);
this.success = true;
this.error = false;
} catch (ex) {
this.success = false;
this.error = true;
}
},
},
});

View File

@@ -0,0 +1,316 @@
<template>
<div>
<div class="d-flex justify-content-center">
<div class="col-md-8">
<h2 id="profile-title">
<span v-html="t$('profile.title')"></span>
</h2>
<div class="alert alert-success" role="alert" v-if="success">
{{ t$('profile.messages.success') }}
</div>
<div class="alert alert-danger" role="alert" v-if="error">
{{ t$('profile.messages.error') }}
</div>
<div v-if="loading" class="text-center">
<div class="spinner-border" role="status">
<span class="visually-hidden">{{ t$('profile.messages.loading') }}</span>
</div>
</div>
<form name="form" id="profile-form" @submit.prevent="save()" v-if="!loading && utenteApp" novalidate>
<!-- Section 1: Personal Information -->
<div class="mb-4">
<h4>{{ t$('profile.sections.personal') }}</h4>
<div class="mb-3">
<label class="form-control-label" for="username">{{ t$('profile.form.username') }}</label>
<input
type="text"
class="form-control"
id="username"
name="username"
v-model="utenteApp.username"
disabled
data-cy="username"
/>
</div>
<div class="mb-3">
<label class="form-control-label" for="email">{{ t$('profile.form.email') }}</label>
<input type="email" class="form-control" id="email" name="email" v-model="utenteApp.email" disabled data-cy="email" />
</div>
<div class="mb-3">
<label class="form-control-label" for="nome"> {{ t$('profile.form.nome') }} <span class="text-danger">*</span> </label>
<input
type="text"
class="form-control"
id="nome"
name="nome"
:class="{ 'is-valid': !v$.utenteApp.nome.$invalid, 'is-invalid': v$.utenteApp.nome.$invalid }"
v-model="v$.utenteApp.nome.$model"
required
data-cy="nome"
/>
<div v-if="v$.utenteApp.nome.$anyDirty && v$.utenteApp.nome.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.nome.required.$invalid">
{{ t$('profile.messages.validate.nome.required') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.nome.minLength.$invalid">
{{ t$('profile.messages.validate.nome.minlength') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.nome.maxLength.$invalid">
{{ t$('profile.messages.validate.nome.maxlength') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="cognome"> {{ t$('profile.form.cognome') }} <span class="text-danger">*</span> </label>
<input
type="text"
class="form-control"
id="cognome"
name="cognome"
:class="{ 'is-valid': !v$.utenteApp.cognome.$invalid, 'is-invalid': v$.utenteApp.cognome.$invalid }"
v-model="v$.utenteApp.cognome.$model"
required
data-cy="cognome"
/>
<div v-if="v$.utenteApp.cognome.$anyDirty && v$.utenteApp.cognome.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.cognome.required.$invalid">
{{ t$('profile.messages.validate.cognome.required') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.cognome.minLength.$invalid">
{{ t$('profile.messages.validate.cognome.minlength') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.cognome.maxLength.$invalid">
{{ t$('profile.messages.validate.cognome.maxlength') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="dataNascita">
{{ t$('profile.form.dataNascita') }} <span class="text-danger">*</span>
</label>
<input
type="date"
class="form-control"
id="dataNascita"
name="dataNascita"
:class="{ 'is-valid': !v$.utenteApp.dataNascita.$invalid, 'is-invalid': v$.utenteApp.dataNascita.$invalid }"
v-model="v$.utenteApp.dataNascita.$model"
required
data-cy="dataNascita"
/>
<div v-if="v$.utenteApp.dataNascita.$anyDirty && v$.utenteApp.dataNascita.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.dataNascita.required.$invalid">
{{ t$('profile.messages.validate.dataNascita.required') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="luogoNascita">
{{ t$('profile.form.luogoNascita') }} <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
id="luogoNascita"
name="luogoNascita"
:class="{ 'is-valid': !v$.utenteApp.luogoNascita.$invalid, 'is-invalid': v$.utenteApp.luogoNascita.$invalid }"
v-model="v$.utenteApp.luogoNascita.$model"
required
data-cy="luogoNascita"
/>
<div v-if="v$.utenteApp.luogoNascita.$anyDirty && v$.utenteApp.luogoNascita.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.luogoNascita.required.$invalid">
{{ t$('profile.messages.validate.luogoNascita.required') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.luogoNascita.minLength.$invalid">
{{ t$('profile.messages.validate.luogoNascita.minlength') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.luogoNascita.maxLength.$invalid">
{{ t$('profile.messages.validate.luogoNascita.maxlength') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="residente">
{{ t$('profile.form.residente') }} <span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
id="residente"
name="residente"
:class="{ 'is-valid': !v$.utenteApp.residente.$invalid, 'is-invalid': v$.utenteApp.residente.$invalid }"
v-model="v$.utenteApp.residente.$model"
required
data-cy="residente"
/>
<div v-if="v$.utenteApp.residente.$anyDirty && v$.utenteApp.residente.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.residente.required.$invalid">
{{ t$('profile.messages.validate.residente.required') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.residente.minLength.$invalid">
{{ t$('profile.messages.validate.residente.minlength') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.residente.maxLength.$invalid">
{{ t$('profile.messages.validate.residente.maxlength') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="telefono">
{{ t$('profile.form.telefono') }} <span class="text-danger">*</span>
</label>
<input
type="tel"
class="form-control"
id="telefono"
name="telefono"
:class="{ 'is-valid': !v$.utenteApp.telefono.$invalid, 'is-invalid': v$.utenteApp.telefono.$invalid }"
v-model="v$.utenteApp.telefono.$model"
required
data-cy="telefono"
/>
<div v-if="v$.utenteApp.telefono.$anyDirty && v$.utenteApp.telefono.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.telefono.required.$invalid">
{{ t$('profile.messages.validate.telefono.required') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.telefono.minLength.$invalid">
{{ t$('profile.messages.validate.telefono.minlength') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.telefono.maxLength.$invalid">
{{ t$('profile.messages.validate.telefono.maxlength') }}
</small>
</div>
</div>
</div>
<!-- Section 2: Company/Organization Information (Optional) -->
<div class="mb-4">
<h4>{{ t$('profile.sections.company') }}</h4>
<p class="text-muted">{{ t$('profile.sections.companyDescription') }}</p>
<div class="mb-3">
<label class="form-control-label" for="societa">{{ t$('profile.form.societa') }}</label>
<input
type="text"
class="form-control"
id="societa"
name="societa"
:class="{ 'is-valid': !v$.utenteApp.societa.$invalid, 'is-invalid': v$.utenteApp.societa.$invalid }"
v-model="v$.utenteApp.societa.$model"
data-cy="societa"
/>
<div v-if="v$.utenteApp.societa.$anyDirty && v$.utenteApp.societa.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.societa.maxLength.$invalid">
{{ t$('profile.messages.validate.societa.maxlength') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="sede">{{ t$('profile.form.sede') }}</label>
<input
type="text"
class="form-control"
id="sede"
name="sede"
:class="{ 'is-valid': !v$.utenteApp.sede.$invalid, 'is-invalid': v$.utenteApp.sede.$invalid }"
v-model="v$.utenteApp.sede.$model"
data-cy="sede"
/>
<div v-if="v$.utenteApp.sede.$anyDirty && v$.utenteApp.sede.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.sede.maxLength.$invalid">
{{ t$('profile.messages.validate.sede.maxlength') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="codfiscale">{{ t$('profile.form.codfiscale') }}</label>
<input
type="text"
class="form-control"
id="codfiscale"
name="codfiscale"
:class="{ 'is-valid': !v$.utenteApp.codfiscale.$invalid, 'is-invalid': v$.utenteApp.codfiscale.$invalid }"
v-model="v$.utenteApp.codfiscale.$model"
data-cy="codfiscale"
/>
<div v-if="v$.utenteApp.codfiscale.$anyDirty && v$.utenteApp.codfiscale.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.codfiscale.minLength.$invalid">
{{ t$('profile.messages.validate.codfiscale.minlength') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.codfiscale.maxLength.$invalid">
{{ t$('profile.messages.validate.codfiscale.maxlength') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="telefonoSoc">{{ t$('profile.form.telefonoSoc') }}</label>
<input
type="tel"
class="form-control"
id="telefonoSoc"
name="telefonoSoc"
:class="{ 'is-valid': !v$.utenteApp.telefonoSoc.$invalid, 'is-invalid': v$.utenteApp.telefonoSoc.$invalid }"
v-model="v$.utenteApp.telefonoSoc.$model"
data-cy="telefonoSoc"
/>
<div v-if="v$.utenteApp.telefonoSoc.$anyDirty && v$.utenteApp.telefonoSoc.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.telefonoSoc.minLength.$invalid">
{{ t$('profile.messages.validate.telefonoSoc.minlength') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.telefonoSoc.maxLength.$invalid">
{{ t$('profile.messages.validate.telefonoSoc.maxlength') }}
</small>
</div>
</div>
<div class="mb-3">
<label class="form-control-label" for="emailSoc">{{ t$('profile.form.emailSoc') }}</label>
<input
type="email"
class="form-control"
id="emailSoc"
name="emailSoc"
:class="{ 'is-valid': !v$.utenteApp.emailSoc.$invalid, 'is-invalid': v$.utenteApp.emailSoc.$invalid }"
v-model="v$.utenteApp.emailSoc.$model"
data-cy="emailSoc"
/>
<div v-if="v$.utenteApp.emailSoc.$anyDirty && v$.utenteApp.emailSoc.$invalid">
<small class="form-text text-danger" v-if="v$.utenteApp.emailSoc.email.$invalid">
{{ t$('profile.messages.validate.emailSoc.email') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.emailSoc.minLength.$invalid">
{{ t$('profile.messages.validate.emailSoc.minlength') }}
</small>
<small class="form-text text-danger" v-if="v$.utenteApp.emailSoc.maxLength.$invalid">
{{ t$('profile.messages.validate.emailSoc.maxlength') }}
</small>
</div>
</div>
</div>
<button type="submit" :disabled="v$.utenteApp.$invalid" class="btn btn-primary" data-cy="submit">
{{ t$('profile.form.button') }}
</button>
</form>
</div>
</div>
</div>
</template>
<script lang="ts" src="./profile.component.ts"></script>

View File

@@ -1,18 +1,61 @@
import { type ComputedRef, defineComponent, inject } from 'vue';
import { type ComputedRef, type Ref, defineComponent, inject, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useLoginModal } from '@/account/login-modal';
import UtenteAppService from '@/entities/utente-app/utente-app.service';
export default defineComponent({
setup() {
const { showLogin } = useLoginModal();
const authenticated = inject<ComputedRef<boolean>>('authenticated');
const username = inject<ComputedRef<string>>('currentUsername');
const utenteAppService = inject<UtenteAppService>('utenteAppService', () => new UtenteAppService());
const profileIncomplete: Ref<boolean> = ref(false);
const checkingProfile: Ref<boolean> = ref(false);
const checkProfileCompletion = async () => {
if (!authenticated.value) {
profileIncomplete.value = false;
return;
}
try {
checkingProfile.value = true;
const utenteApp = await utenteAppService.getCurrentUser();
// Check if essential profile fields are missing
if (
!utenteApp ||
!utenteApp.nome ||
!utenteApp.cognome ||
!utenteApp.dataNascita ||
!utenteApp.luogoNascita ||
!utenteApp.residente ||
!utenteApp.telefono
) {
profileIncomplete.value = true;
} else {
profileIncomplete.value = false;
}
} catch (error) {
// If UtenteApp doesn't exist, profile is incomplete
profileIncomplete.value = true;
} finally {
checkingProfile.value = false;
}
};
onMounted(() => {
checkProfileCompletion();
});
return {
authenticated,
username,
showLogin,
profileIncomplete,
checkingProfile,
t$: useI18n().t,
};
},

View File

@@ -12,6 +12,12 @@
<span v-if="username">{{ t$('home.logged.message', { username }) }}</span>
</div>
<div class="alert alert-warning" v-if="authenticated && profileIncomplete && !checkingProfile">
<span>{{ t$('home.profile.incomplete.message') }}</span>
&nbsp;
<router-link class="alert-link" to="/account/profile">{{ t$('home.profile.incomplete.link') }}</router-link>
</div>
<div class="alert alert-warning" v-if="!authenticated">
<span>{{ t$('global.messages.info.authenticated.prefix') }}</span>
<a class="alert-link" @click="showLogin()">{{ t$('global.messages.info.authenticated.link') }}</a

View File

@@ -111,6 +111,10 @@
<span class="no-bold">{{ t$('global.menu.account.main') }}</span>
</span>
</template>
<b-dropdown-item data-cy="profile" to="/account/profile" v-if="authenticated" active-class="active">
<font-awesome-icon icon="id-card" />
<span>{{ t$('global.menu.account.profile') }}</span>
</b-dropdown-item>
<b-dropdown-item data-cy="settings" to="/account/settings" v-if="authenticated" active-class="active">
<font-awesome-icon icon="wrench" />
<span>{{ t$('global.menu.account.settings') }}</span>

View File

@@ -95,4 +95,17 @@ export default class UtenteAppService {
});
});
}
saveCurrentUserProfile(entity: IUtenteApp): Promise<IUtenteApp> {
return new Promise<IUtenteApp>((resolve, reject) => {
axios
.post(`${baseApiUrl}/current`, entity)
.then(res => {
resolve(res.data);
})
.catch(err => {
reject(err);
});
});
}
}

View File

@@ -15,6 +15,7 @@ import { initBootstrapVue } from '@/shared/config/config-bootstrap-vue';
import JhiItemCount from '@/shared/jhi-item-count.vue';
import JhiSortIndicator from '@/shared/sort/jhi-sort-indicator.vue';
import { useStore, useTranslationStore } from '@/store';
import UtenteAppService from '@/entities/utente-app/utente-app.service';
import App from './app.vue';
import router from './router';
@@ -120,6 +121,7 @@ const app = createApp({
provide('translationService', translationService);
provide('accountService', accountService);
provide('utenteAppService', new UtenteAppService());
// jhipster-needle-add-entity-service-to-main - JHipster will import entities services here
},
template: '<App/>',

View File

@@ -7,6 +7,7 @@ const ResetPasswordFinish = () => import('@/account/reset-password/finish/reset-
const ChangePassword = () => import('@/account/change-password/change-password.vue');
const Settings = () => import('@/account/settings/settings.vue');
const Sessions = () => import('@/account/sessions/sessions.vue');
const Profile = () => import('@/account/profile/profile.vue');
export default [
{
@@ -47,4 +48,10 @@ export default [
component: Settings,
meta: { authorities: [Authority.USER] },
},
{
path: '/account/profile',
name: 'Profile',
component: Profile,
meta: { authorities: [Authority.USER] },
},
];

View File

@@ -21,6 +21,7 @@
},
"account": {
"main": "Utente",
"profile": "Profilo",
"settings": "Impostazioni",
"password": "Password",
"sessions": "Sessioni",

View File

@@ -5,6 +5,12 @@
"logged": {
"message": "Autenticato come \"{ username }\"."
},
"profile": {
"incomplete": {
"message": "Il tuo profilo è incompleto. Per poter effettuare prenotazioni, completa il tuo profilo con le informazioni richieste.",
"link": "Completa il profilo"
}
},
"question": "In caso di domande su JHipster:",
"link": {
"homepage": "Homepage JHipster",

View File

@@ -0,0 +1,80 @@
{
"profile": {
"title": "Profilo Utente",
"sections": {
"personal": "Dati Personali",
"company": "Società / Associazione (opzionale)",
"companyDescription": "Compila questa sezione se intendi operare le prenotazioni per conto di una società o associazione."
},
"form": {
"username": "Nome utente",
"email": "Email",
"nome": "Nome",
"cognome": "Cognome",
"dataNascita": "Data di nascita",
"luogoNascita": "Luogo di nascita",
"residente": "Residente a",
"telefono": "Telefono",
"societa": "Nome società/associazione",
"sede": "Sede",
"codfiscale": "Codice fiscale (società)",
"telefonoSoc": "Telefono società",
"emailSoc": "Email società",
"button": "Salva profilo"
},
"messages": {
"success": "Il tuo profilo è stato salvato con successo!",
"error": "Si è verificato un errore durante il salvataggio del profilo. Riprova.",
"loading": "Caricamento...",
"validate": {
"nome": {
"required": "Il nome è obbligatorio.",
"minlength": "Il nome deve contenere almeno 1 carattere.",
"maxlength": "Il nome non può contenere più di 50 caratteri."
},
"cognome": {
"required": "Il cognome è obbligatorio.",
"minlength": "Il cognome deve contenere almeno 1 carattere.",
"maxlength": "Il cognome non può contenere più di 50 caratteri."
},
"dataNascita": {
"required": "La data di nascita è obbligatoria."
},
"luogoNascita": {
"required": "Il luogo di nascita è obbligatorio.",
"minlength": "Il luogo di nascita deve contenere almeno 1 carattere.",
"maxlength": "Il luogo di nascita non può contenere più di 100 caratteri."
},
"residente": {
"required": "La residenza è obbligatoria.",
"minlength": "La residenza deve contenere almeno 1 carattere.",
"maxlength": "La residenza non può contenere più di 200 caratteri."
},
"telefono": {
"required": "Il telefono è obbligatorio.",
"minlength": "Il telefono deve contenere almeno 8 caratteri.",
"maxlength": "Il telefono non può contenere più di 20 caratteri."
},
"societa": {
"maxlength": "Il nome della società non può contenere più di 100 caratteri."
},
"sede": {
"maxlength": "La sede non può contenere più di 200 caratteri."
},
"codfiscale": {
"minlength": "Il codice fiscale deve contenere almeno 11 caratteri.",
"maxlength": "Il codice fiscale non può contenere più di 16 caratteri."
},
"telefonoSoc": {
"minlength": "Il telefono della società deve contenere almeno 8 caratteri.",
"maxlength": "Il telefono della società non può contenere più di 20 caratteri."
},
"emailSoc": {
"email": "L'email della società non è valida.",
"minlength": "L'email della società deve contenere almeno 5 caratteri.",
"maxlength": "L'email della società non può contenere più di 254 caratteri."
}
}
}
}
}