Add facility availability configuration feature

Implement a comprehensive interface for administrators to configure facility opening hours and closures. The feature enables ADMIN and INCARICATO users to manage time-based availability using simple string time fields.

Key changes:
- Add StrutturaDisponibilitaConfig component with form and list view
- Use orarioInizio/orarioFine string fields for simplified time management
- Add INCARICATO role to authority enum
- Implement XOR validation for dataSpecifica vs giornoSettimana
- Add clock icon button to Struttura list for quick access
- Include comprehensive Italian translations

This implementation uses string-based time fields instead of Instant types, providing a simpler and more appropriate solution for managing recurring time slots.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-15 15:01:24 +01:00
parent b0f2420137
commit 2f04d07928
9 changed files with 639 additions and 2 deletions

View File

@@ -0,0 +1,135 @@
import { defineComponent, inject, ref, type Ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import useVuelidate from '@vuelidate/core';
import { required, helpers } from '@vuelidate/validators';
import StrutturaService from '../struttura/struttura.service';
import DisponibilitaService from '../disponibilita/disponibilita.service';
import { type IStruttura } from '@/shared/model/struttura.model';
import { type IDisponibilita, Disponibilita } from '@/shared/model/disponibilita.model';
import { GiornoSettimana } from '@/shared/model/enumerations/giorno-settimana.model';
import { TipoDisponibilita } from '@/shared/model/enumerations/tipo-disponibilita.model';
import { useAlertService } from '@/shared/alert/alert.service';
export default defineComponent({
compatConfig: { MODE: 3 },
name: 'StrutturaDisponibilitaConfig',
setup() {
const route = useRoute();
const router = useRouter();
const { t: t$ } = useI18n();
// Services
const strutturaService = inject('strutturaService', () => new StrutturaService());
const disponibilitaService = inject('disponibilitaService', () => new DisponibilitaService());
const alertService = inject('alertService', () => useAlertService(), true);
// State
const struttura: Ref<IStruttura | null> = ref(null);
const disponibilita: Ref<IDisponibilita> = ref(new Disponibilita());
const disponibilitas: Ref<IDisponibilita[]> = ref([]);
const isSaving = ref(false);
const isLoading = ref(true);
// Enums
const giornoSettimanaValues = ref(Object.keys(GiornoSettimana));
const tipoDisponibilitaValues = ref(Object.keys(TipoDisponibilita));
// Custom validator: exactly one of dataSpecifica OR giornoSettimana
const exactlyOne = helpers.withMessage(t$('smartbookingApp.disponibilita.validation.exactlyOneRequired'), (value, siblings) => {
const hasData = !!siblings.dataSpecifica;
const hasGiorno = !!siblings.giornoSettimana;
return (hasData && !hasGiorno) || (!hasData && hasGiorno);
});
// Validation rules
const validationRules = {
tipo: { required },
orarioInizio: { required },
orarioFine: { required },
dataSpecifica: { exactlyOne },
giornoSettimana: {},
note: {},
};
const v$ = useVuelidate(validationRules, disponibilita);
// Initialize with defaults
disponibilita.value.tipo = 'CHIUSURA';
// Load data
const loadData = async () => {
const strutturaId = Number(route.params.strutturaId);
try {
struttura.value = await strutturaService().find(strutturaId);
disponibilita.value.struttura = struttura.value;
// Load disponibilitas and filter by struttura
const dispRes = await disponibilitaService().retrieve();
disponibilitas.value = dispRes.data.filter(d => d.struttura?.id === strutturaId);
isLoading.value = false;
} catch (error) {
alertService().showHttpError(error.response);
}
};
const save = async () => {
await v$.value.$validate();
if (v$.value.$invalid) return;
isSaving.value = true;
try {
await disponibilitaService().create(disponibilita.value);
alertService().showSuccess(t$('smartbookingApp.disponibilita.created', { param: '' }));
// Reset form
disponibilita.value = new Disponibilita();
disponibilita.value.struttura = struttura.value;
disponibilita.value.tipo = 'CHIUSURA';
v$.value.$reset();
// Reload list
await loadData();
} catch (error) {
alertService().showHttpError(error.response);
} finally {
isSaving.value = false;
}
};
const cancel = () => {
router.push({ name: 'Struttura' });
};
const deleteDisponibilita = async (id: number) => {
try {
await disponibilitaService().delete(id);
alertService().showInfo(t$('smartbookingApp.disponibilita.deleted', { param: id }));
await loadData();
} catch (error) {
alertService().showHttpError(error.response);
}
};
onMounted(() => {
loadData();
});
return {
struttura,
disponibilita,
disponibilitas,
isSaving,
isLoading,
giornoSettimanaValues,
tipoDisponibilitaValues,
v$,
save,
cancel,
deleteDisponibilita,
t$,
};
},
});

View File

@@ -0,0 +1,173 @@
<template>
<div>
<!-- Header -->
<h2>
<span>{{ t$('smartbookingApp.struttura.disponibilitaConfig.title') }}</span>
</h2>
<!-- Loading state -->
<div v-if="isLoading" class="text-center">
<b-spinner></b-spinner>
</div>
<div v-else>
<!-- Struttura Details Section (read-only) -->
<div class="card mb-3">
<div class="card-header">
<h5>{{ t$('smartbookingApp.struttura.disponibilitaConfig.strutturaDetails') }}</h5>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-3">{{ t$('smartbookingApp.struttura.nome') }}</dt>
<dd class="col-sm-9">{{ struttura.nome }}</dd>
<dt class="col-sm-3">{{ t$('smartbookingApp.struttura.descrizione') }}</dt>
<dd class="col-sm-9">{{ struttura.descrizione }}</dd>
<dt class="col-sm-3">{{ t$('smartbookingApp.struttura.indirizzo') }}</dt>
<dd class="col-sm-9">{{ struttura.indirizzo }}</dd>
<dt class="col-sm-3">{{ t$('smartbookingApp.struttura.capienzaMax') }}</dt>
<dd class="col-sm-9">{{ struttura.capienzaMax }}</dd>
<dt class="col-sm-3">{{ t$('smartbookingApp.struttura.attiva') }}</dt>
<dd class="col-sm-9">{{ struttura.attiva }}</dd>
</dl>
</div>
</div>
<!-- Add Disponibilita Form -->
<div class="card mb-3">
<div class="card-header">
<h5>{{ t$('smartbookingApp.struttura.disponibilitaConfig.addDisponibilita') }}</h5>
</div>
<div class="card-body">
<form @submit.prevent="save">
<!-- Section 1: Tipo Disponibilita -->
<div class="mb-3">
<label for="tipo">{{ t$('smartbookingApp.disponibilita.tipo') }}</label>
<select
id="tipo"
v-model="v$.tipo.$model"
class="form-control"
:class="{ valid: !v$.tipo.$invalid, invalid: v$.tipo.$invalid }"
>
<option v-for="tipo in tipoDisponibilitaValues" :key="tipo" :value="tipo">
{{ t$('smartbookingApp.TipoDisponibilita.' + tipo) }}
</option>
</select>
</div>
<!-- Section 2: Time Range (String fields) -->
<div class="row mb-3">
<div class="col-md-6">
<label for="orarioInizio">{{ t$('smartbookingApp.disponibilita.orarioInizio') }}</label>
<input
id="orarioInizio"
type="time"
v-model="v$.orarioInizio.$model"
class="form-control"
:class="{ valid: !v$.orarioInizio.$invalid, invalid: v$.orarioInizio.$invalid }"
/>
</div>
<div class="col-md-6">
<label for="orarioFine">{{ t$('smartbookingApp.disponibilita.orarioFine') }}</label>
<input
id="orarioFine"
type="time"
v-model="v$.orarioFine.$model"
class="form-control"
:class="{ valid: !v$.orarioFine.$invalid, invalid: v$.orarioFine.$invalid }"
/>
</div>
</div>
<!-- Section 3: Data Specifica -->
<div class="mb-3">
<label for="dataSpecifica">{{ t$('smartbookingApp.disponibilita.dataSpecifica') }}</label>
<input
id="dataSpecifica"
type="date"
v-model="v$.dataSpecifica.$model"
class="form-control"
:class="{ valid: !v$.dataSpecifica.$invalid, invalid: v$.dataSpecifica.$invalid }"
/>
<small class="form-text text-muted">
{{ t$('smartbookingApp.disponibilita.help.dataSpecificaOrGiorno') }}
</small>
</div>
<!-- Section 4: Giorno Settimana -->
<div class="mb-3">
<label for="giornoSettimana">{{ t$('smartbookingApp.disponibilita.giornoSettimana') }}</label>
<select id="giornoSettimana" v-model="v$.giornoSettimana.$model" class="form-control">
<option :value="null">{{ t$('smartbookingApp.disponibilita.selectGiorno') }}</option>
<option v-for="giorno in giornoSettimanaValues" :key="giorno" :value="giorno">
{{ t$('smartbookingApp.GiornoSettimana.' + giorno) }}
</option>
</select>
</div>
<!-- Section 5: Note -->
<div class="mb-3">
<label for="note">{{ t$('smartbookingApp.disponibilita.note') }}</label>
<textarea id="note" v-model="v$.note.$model" class="form-control" rows="3"></textarea>
</div>
<!-- Buttons -->
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-secondary me-2" @click="cancel">
{{ t$('entity.action.cancel') }}
</button>
<button type="submit" class="btn btn-primary" :disabled="v$.$invalid || isSaving">
{{ t$('smartbookingApp.disponibilita.insertButton') }}
</button>
</div>
</form>
</div>
</div>
<!-- Existing Disponibilita List -->
<div class="card">
<div class="card-header">
<h5>{{ t$('smartbookingApp.struttura.disponibilitaConfig.existingDisponibilita') }}</h5>
</div>
<div class="card-body">
<div v-if="disponibilitas.length === 0" class="alert alert-warning">
{{ t$('smartbookingApp.struttura.disponibilitaConfig.noDisponibilita') }}
</div>
<table v-else class="table table-striped">
<thead>
<tr>
<th>{{ t$('smartbookingApp.disponibilita.tipo') }}</th>
<th>{{ t$('smartbookingApp.disponibilita.orarioInizio') }}</th>
<th>{{ t$('smartbookingApp.disponibilita.orarioFine') }}</th>
<th>{{ t$('smartbookingApp.disponibilita.dataSpecifica') }}</th>
<th>{{ t$('smartbookingApp.disponibilita.giornoSettimana') }}</th>
<th>{{ t$('smartbookingApp.disponibilita.note') }}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="disp in disponibilitas" :key="disp.id">
<td>{{ t$('smartbookingApp.TipoDisponibilita.' + disp.tipo) }}</td>
<td>{{ disp.orarioInizio }}</td>
<td>{{ disp.orarioFine }}</td>
<td>{{ disp.dataSpecifica }}</td>
<td>{{ disp.giornoSettimana ? t$('smartbookingApp.GiornoSettimana.' + disp.giornoSettimana) : '' }}</td>
<td>{{ disp.note }}</td>
<td>
<button class="btn btn-danger btn-sm" @click="deleteDisponibilita(disp.id)">
<font-awesome-icon icon="times"></font-awesome-icon>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" src="./struttura-disponibilita-config.component.ts"></script>

View File

@@ -82,6 +82,14 @@
<td>{{ formatDateShort(struttura.updatedAt) || '' }}</td>
<td class="text-end">
<div class="btn-group">
<router-link
:to="{ name: 'StrutturaDisponibilitaConfig', params: { strutturaId: struttura.id } }"
class="btn btn-info btn-sm"
data-cy="entityConfigButton"
>
<font-awesome-icon icon="clock"></font-awesome-icon>
<span class="d-none d-md-inline">{{ t$('smartbookingApp.struttura.configDisponibilita') }}</span>
</router-link>
<router-link
:to="{ name: 'StrutturaView', params: { strutturaId: struttura.id } }"
class="btn btn-info btn-sm details"

View File

@@ -25,6 +25,7 @@ const ConfermaDetails = () => import('@/entities/conferma/conferma-details.vue')
const Struttura = () => import('@/entities/struttura/struttura.vue');
const StrutturaUpdate = () => import('@/entities/struttura/struttura-update.vue');
const StrutturaDetails = () => import('@/entities/struttura/struttura-details.vue');
const StrutturaDisponibilitaConfig = () => import('@/entities/struttura/struttura-disponibilita-config.vue');
const UtenteApp = () => import('@/entities/utente-app/utente-app.vue');
const UtenteAppUpdate = () => import('@/entities/utente-app/utente-app-update.vue');
@@ -198,6 +199,12 @@ export default {
component: StrutturaDetails,
meta: { authorities: [Authority.USER] },
},
{
path: 'struttura/:strutturaId/disponibilita',
name: 'StrutturaDisponibilitaConfig',
component: StrutturaDisponibilitaConfig,
meta: { authorities: [Authority.ADMIN, Authority.INCARICATO] },
},
{
path: 'utente-app',
name: 'UtenteApp',

View File

@@ -9,6 +9,7 @@ import { faBars } from '@fortawesome/free-solid-svg-icons/faBars';
import { faBell } from '@fortawesome/free-solid-svg-icons/faBell';
import { faBook } from '@fortawesome/free-solid-svg-icons/faBook';
import { faCloud } from '@fortawesome/free-solid-svg-icons/faCloud';
import { faClock } from '@fortawesome/free-solid-svg-icons/faClock';
import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs';
import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase';
import { faEye } from '@fortawesome/free-solid-svg-icons/faEye';
@@ -61,6 +62,7 @@ export function initFortAwesome(vue: App) {
faBell,
faBook,
faCloud,
faClock,
faCogs,
faDatabase,
faEye,

View File

@@ -1,4 +1,5 @@
export enum Authority {
ADMIN = 'ROLE_ADMIN',
USER = 'ROLE_USER',
INCARICATO = 'ROLE_INCARICATO',
}

View File

@@ -26,7 +26,15 @@
"orarioFine": "Orario Fine",
"tipo": "Tipo",
"note": "Note",
"struttura": "Struttura"
"struttura": "Struttura",
"validation": {
"exactlyOneRequired": "Specificare esattamente uno tra Data Specifica o Giorno Settimana"
},
"insertButton": "Inserisci disponibilità",
"selectGiorno": "Seleziona un giorno",
"help": {
"dataSpecificaOrGiorno": "Compilare Data Specifica O Giorno Settimana (non entrambi)"
}
}
}
}

View File

@@ -27,7 +27,15 @@
"createdAt": "Created At",
"updatedAt": "Updated At",
"disponibilita": "Disponibilita",
"moduliLiberatorie": "Moduli Liberatorie"
"moduliLiberatorie": "Moduli Liberatorie",
"configDisponibilita": "Disponibilità",
"disponibilitaConfig": {
"title": "Configurazione Disponibilità",
"strutturaDetails": "Dettagli Struttura",
"addDisponibilita": "Aggiungi Disponibilità",
"existingDisponibilita": "Disponibilità Esistenti",
"noDisponibilita": "Nessuna disponibilità configurata"
}
}
}
}