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:
@@ -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$,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum Authority {
|
||||
ADMIN = 'ROLE_ADMIN',
|
||||
USER = 'ROLE_USER',
|
||||
INCARICATO = 'ROLE_INCARICATO',
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user