Compare commits

..

3 Commits

Author SHA1 Message Date
Simone Bierti
45968b28e2 Add Twilio SMS service, enhance booking/confirmation flow, and update home UI
All checks were successful
Build and Publish / build (push) Successful in 3m16s
- Add TwilioService for SMS notifications
- Extend PrenotazioneService and ConfermaServiceImpl with new booking/confirmation logic
- Add phone number support in UtenteAppService and UtenteAppServiceImpl
- Update PrenotazioneRepository with custom queries
- Enhance ConfermaResource and PrenotazioneResource REST endpoints
- Add MailService improvements
- Update application.yml with new config entries
- Update home page (home.vue, home.json, global.scss) and add Artegna logo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 15:48:02 +02:00
Simone Bierti
78d3e17d02 Implement role-based booking list views for ROLE_USER and ROLE_INCARICATO
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 15:48:02 +02:00
Simone Bierti
b4d0ca4898 Add ROLE_INCARICATO, configure mail, and add booking visualization specs
- Add ROLE_INCARICATO authority (AuthoritiesConstants, authority.csv)
- Configure SMTP mail settings for dev and prod environments
- Add faIdCard icon to FontAwesome config
- Add feature spec for booking visualization and management by role

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 15:48:02 +02:00
28 changed files with 845 additions and 203 deletions

View File

@@ -80,6 +80,9 @@ dependencies {
implementation "org.hibernate.orm:hibernate-jcache"
implementation "org.hibernate.validator:hibernate-validator"
implementation "org.postgresql:postgresql"
implementation "io.seruco.encoding:base62:0.1.3"
implementation "com.twilio.sdk:twilio:11.3.6"
testImplementation(libs.archunit.junit5.api) {
exclude group: 'org.slf4j', module: 'slf4j-api'
}

View File

@@ -0,0 +1,78 @@
Ecco una bozza di specifiche tecniche strutturate in **Markdown**, ottimizzate per essere interpretate da un agente di coding (come un plugin IDE o un modello LLM focalizzato sullo sviluppo).
---
# Specifiche Tecniche: Gestione e Visualizzazione Prenotazioni
## 1. Obiettivo
Implementare un sistema di visualizzazione e gestione delle prenotazioni basato sui ruoli utente (`ROLE_USER`, `ROLE_INCARICATO`), con logica di business legata all'esistenza di un oggetto `Conferma` correlato.
## 2. Modello Dati e Relazioni
* **Prenotazione (P):** Entità principale.
* **Conferma (C):** Entità correlata a `Prenotazione`.
* **Relazione:** One-to-One.
* **Chiave Primaria:** `C.id` deve coincidere con `P.id`.
* **Campi Conferma:**
* `tipoConferma` (Enum: `TipoConferma`)
* `motivoConferma` (String/Text)
* `codice` (String, Alfanumerico Base62, generato dal sistema).
## 3. Logica di Accesso e Visualizzazione
### 3.1 Vista: ROLE_USER
L'utente visualizza esclusivamente le proprie prenotazioni.
* **Query:** `SELECT * FROM Prenotazione WHERE utente_id = :current_user_id ORDER BY data_inserimento DESC`.
* **Interfaccia (Tabella):**
* **Caso A: Prenotazione SENZA Conferma**
* Azioni permesse: **Visualizza**, **Modifica**, **Cancella**.
* **Caso B: Prenotazione CON Conferma**
* Azioni permesse: **Visualizza** (Read-only), **Visualizza Conferma**.
* Inibizione: I tasti Modifica e Cancella devono essere disabilitati o nascosti.
### 3.2 Vista: ROLE_INCARICATO
L'utente visualizza tutte le prenotazioni del sistema create negli ultimi 12 mesi.
* **Filtro Temporale:** `data_prenotazione >= CURRENT_DATE - 12 mesi`.
* **Layout:** Due tabelle distinte.
#### Tabella 1: Prenotazioni Pendenti (Senza Conferma)
* **Contenuto:** Prenotazioni che non hanno un record corrispondente nella tabella `Conferma`.
* **Azioni:**
1. **Visualizza:** Apre il dettaglio della prenotazione.
2. **Prendi in carico:** Apre una finestra modale (Pop-up).
#### Tabella 2: Prenotazioni Completate (Con Conferma)
* **Contenuto:** Prenotazioni che hanno un record corrispondente nella tabella `Conferma`.
* **Azioni:** Visualizzazione del dettaglio e della relativa conferma.
---
## 4. Workflow "Prendi in carico" (Modale)
Al clic sul pulsante nella Tabella 1 del `ROLE_INCARICATO`:
1. **Apertura Modale:** Form di creazione per l'oggetto `Conferma`.
2. **Input Utente:**
* `tipoConferma`: Select box basata sull'Enum.
* `motivoConferma`: TextArea (Testo libero).
3. **Logica di Sistema (Generazione Codice):**
* Il campo `codice` deve essere generato lato server (o pre-calcolato) convertendo il timestamp corrente (data/ora) in una stringa **Base62** (caratteri `0-9`, `a-z`, `A-Z`).
4. **Salvataggio:**
* Creazione record `Conferma` con `id` identico a quello della `Prenotazione` selezionata.
5. **Post-Azione:**
* Chiusura modale.
* **Refresh Reattivo:** Le tabelle devono aggiornarsi senza ricaricare l'intera pagina (la prenotazione deve spostarsi dalla Tabella 1 alla Tabella 2).
---
## 5. Requisiti Tecnici Suggeriti
* **Sicurezza:** Verificare l'authority lato server (Spring Security o equivalente) per ogni richiesta API. Non basarsi solo sull'occultamento dei tasti lato UI.
* **Generazione Base62:** Implementare una funzione di encoding che trasformi `long timestamp` in `String Base62`.
---
## 6. Definizione del Successo (Criteri di Accettazione)
* [ ] Un `ROLE_USER` non può cancellare una prenotazione se esiste una conferma.
* [ ] Un `ROLE_INCARICATO` vede solo i dati degli ultimi 12 mesi.
* [ ] Il salvataggio di una conferma sposta istantaneamente la riga tra le due tabelle dell'incaricato.
* [ ] Il codice della conferma è univoco e generato in Base62.

View File

@@ -731,3 +731,62 @@ A profile completion check is shown on the home page to prompt users to complete
- Bootstrap responsive design
- Complete Italian translations
- Security: users can only edit their own profile
---
# Frontend Changes: Gestione e Visualizzazione Prenotazioni per Ruolo
## Date
2026-04-07
## Overview
Implemented role-based booking management views as specified in `features/visualizzazione-prenotazioni.md`. The `prenotazione` list page now renders different UIs depending on the user's role:
- **ROLE_USER**: sees their own bookings in a single table. Edit and Delete actions are hidden when a `Conferma` exists for that booking. A "Vedi conferma" button appears instead.
- **ROLE_INCARICATO**: sees two separate tables — *Prenotazioni in attesa* (no `Conferma`) and *Prenotazioni gestite* (with `Conferma`). A "Prendi in carico" button opens a modal to create the `Conferma` record; after saving, the row moves reactively from the first table to the second without a page reload.
## Files Modified
### 1. Component Logic
**File:** `src/main/webapp/app/entities/prenotazione/prenotazione.component.ts`
- Imported `useAccountStore`, `Authority`, `TipoConferma`, `ConfermaService`
- Added `isIncaricato` computed property (checks `account.authorities`)
- Added `prenotazioniPendenti` and `prenotazioniCompletate` computed arrays (derived from `prenotaziones`)
- Added modal state: `showPrendiInCaricoModal`, `selectedPrenotazione`, `confermaForm`, `isSubmittingConferma`
- Added methods: `prendiInCarico()`, `closePrendiInCaricoModal()`, `submitConferma()`
- `submitConferma()` calls `ConfermaService.create()` with `id` set to the prenotazione id (shared PK), then refreshes the list reactively
### 2. Vue Template
**File:** `src/main/webapp/app/entities/prenotazione/prenotazione.vue`
- Split into two `<template v-if>` blocks: one for `!isIncaricato`, one for `isIncaricato`
- **ROLE_USER block**: simplified column set; Edit/Delete wrapped in `v-if="!prenotazione.conferma"`, "Vedi conferma" router-link shown when `conferma` exists
- **ROLE_INCARICATO block**:
- Table 1 (pendenti): full user/struttura columns + "Prendi in carico" button
- Table 2 (completate): shows `TipoConferma` badge + "Vedi conferma" link
- `b-modal` for "Prendi in carico" with `tipoConferma` select (required) and `motivoConferma` textarea; submit button disabled until tipo selected; spinner during save
### 3. Internationalization
**File:** `src/main/webapp/i18n/it/prenotazione.json`
New keys added:
- `home.titleIncaricato` — page title for incaricato view
- `viewConferma` — "Vedi conferma" button label
- `pendenti.title` / `pendenti.notFound`
- `completate.title` / `completate.notFound`
- `prendiInCarico.button`, `.title`, `.subtitle`, `.selectTipo`, `.motivoPlaceholder`, `.submit`, `.success`
Also improved existing Italian strings (`home.title`, `home.refreshListLabel`, `home.createLabel`, `home.notFound`).
## Acceptance Criteria Status
- [x] ROLE_USER cannot delete/edit a booking that has a conferma (buttons hidden in UI)
- [x] ROLE_INCARICATO sees two tables with correct content split
- [x] Saving a conferma moves the row reactively from Table 1 to Table 2 (via list refresh after create)
- [x] Conferma is created with `id` matching the prenotazione id (shared PK per spec)

View File

@@ -1,6 +1,8 @@
package it.sw.pa.comune.artegna.repository;
import it.sw.pa.comune.artegna.domain.Prenotazione;
import it.sw.pa.comune.artegna.domain.enumeration.StatoPrenotazione;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
@@ -39,4 +41,6 @@ public interface PrenotazioneRepository extends JpaRepository<Prenotazione, Long
"select prenotazione from Prenotazione prenotazione left join fetch prenotazione.utente left join fetch prenotazione.struttura where prenotazione.id =:id"
)
Optional<Prenotazione> findOneWithToOneRelationships(@Param("id") Long id);
Long id(Long id);
}

View File

@@ -7,6 +7,8 @@ public final class AuthoritiesConstants {
public static final String ADMIN = "ROLE_ADMIN";
public static final String INCARICATO = "ROLE_INCARICATO";
public static final String USER = "ROLE_USER";
public static final String ANONYMOUS = "ROLE_ANONYMOUS";

View File

@@ -117,4 +117,13 @@ public class MailService {
LOG.debug("Sending password reset email to '{}'", user.getEmail());
sendEmailFromTemplateSync(user, "mail/passwordResetEmail", "email.reset.title");
}
// crea un metodo per l'invio di email che la prenotazione è stata confermata o rifiutata
@Async
public void sendBookingConfirmationEmail(User user, boolean confirmed) {
String templateName = confirmed ? "mail/bookingConfirmedEmail" : "mail/bookingRejectedEmail";
String titleKey = confirmed ? "email.booking.confirmed.title" : "email.booking.rejected.title";
LOG.debug("Sending {} email to '{}'", confirmed ? "confirmation" : "rejection", user.getEmail());
sendEmailFromTemplateSync(user, templateName, titleKey);
}
}

View File

@@ -1,6 +1,7 @@
package it.sw.pa.comune.artegna.service;
import it.sw.pa.comune.artegna.service.dto.PrenotazioneDTO;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

View File

@@ -0,0 +1,37 @@
package it.sw.pa.comune.artegna.service;
import com.twilio.Twilio;
import com.twilio.converter.Promoter;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import java.math.BigDecimal;
import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
public class TwilioService {
@Value("${twilio.account.sid}")
private String accountSid;
@Value("${twilio.auth.token}")
private String authToken;
@Value("${twilio.whatsapp.number}")
private String fromWhatsAppNumber;
public TwilioService() {
// Initialize Twilio with account credentials
Twilio.init(accountSid, authToken);
}
public String sendWhatsAppMessage(String to, String messageBody) {
// Send a message via Twilio's API
Message message = Message.creator(
new PhoneNumber("whatsapp:" + to), // Recipient's WhatsApp number
new PhoneNumber(fromWhatsAppNumber), // Twilio WhatsApp number
messageBody
).create(); // Message body
return message.getSid(); // Return message SID to track status
}
}

View File

@@ -71,4 +71,6 @@ public interface UtenteAppService {
* @param id the id of the entity.
*/
void delete(Long id);
UtenteAppDTO fetchUtenteAppFromUser();
}

View File

@@ -1,10 +1,22 @@
package it.sw.pa.comune.artegna.service.impl;
import static it.sw.pa.comune.artegna.domain.enumeration.TipoEventoNotifica.ANNULLAMENTO;
import io.seruco.encoding.base62.Base62;
import it.sw.pa.comune.artegna.domain.Conferma;
import it.sw.pa.comune.artegna.domain.Prenotazione;
import it.sw.pa.comune.artegna.domain.User;
import it.sw.pa.comune.artegna.domain.enumeration.StatoPrenotazione;
import it.sw.pa.comune.artegna.domain.enumeration.TipoConferma;
import it.sw.pa.comune.artegna.repository.ConfermaRepository;
import it.sw.pa.comune.artegna.repository.PrenotazioneRepository;
import it.sw.pa.comune.artegna.service.ConfermaService;
import it.sw.pa.comune.artegna.service.UserService;
import it.sw.pa.comune.artegna.service.UtenteAppService;
import it.sw.pa.comune.artegna.service.dto.ConfermaDTO;
import it.sw.pa.comune.artegna.service.dto.UtenteAppDTO;
import it.sw.pa.comune.artegna.service.mapper.ConfermaMapper;
import it.sw.pa.comune.artegna.service.mapper.UserMapper;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
@@ -14,6 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -30,19 +43,67 @@ public class ConfermaServiceImpl implements ConfermaService {
private final ConfermaMapper confermaMapper;
public ConfermaServiceImpl(ConfermaRepository confermaRepository, ConfermaMapper confermaMapper) {
private final PrenotazioneRepository prenotazioneRepository;
private final UtenteAppService utenteAppService;
public ConfermaServiceImpl(
ConfermaRepository confermaRepository,
ConfermaMapper confermaMapper,
PrenotazioneRepository prenotazioneRepository,
UtenteAppService utenteAppService
) {
this.confermaRepository = confermaRepository;
this.confermaMapper = confermaMapper;
this.prenotazioneRepository = prenotazioneRepository;
this.utenteAppService = utenteAppService;
}
@Override
public ConfermaDTO save(ConfermaDTO confermaDTO) {
LOG.debug("Request to save Conferma : {}", confermaDTO);
confermaDTO.setConfermataDa(utenteAppService.fetchUtenteAppFromUser());
Conferma conferma = confermaMapper.toEntity(confermaDTO);
conferma = confermaRepository.save(conferma);
Long idPrenotazione = null;
if (conferma.getId() != null) {
idPrenotazione = conferma.getId();
conferma.setId(null);
conferma.setCodice(createCodice());
if (conferma.getTipoConferma() == null) {
conferma.setTipoConferma(TipoConferma.RIFIUTATA);
}
}
conferma = confermaRepository.saveAndFlush(conferma);
if (idPrenotazione != null) {
Prenotazione prenotazioneCollegata = prenotazioneRepository.findById(idPrenotazione).orElse(null);
if (prenotazioneCollegata != null) {
switch (conferma.getTipoConferma()) {
case CONFERMATA:
prenotazioneCollegata.setStato(StatoPrenotazione.CONFERMATA);
break;
case RIFIUTATA:
prenotazioneCollegata.setStato(StatoPrenotazione.ANNULLATA);
break;
default:
throw new IllegalArgumentException("Tipo conferma non valido: " + conferma.getTipoConferma());
}
prenotazioneCollegata.setConferma(conferma);
prenotazioneRepository.saveAndFlush(prenotazioneCollegata);
}
}
return confermaMapper.toDto(conferma);
}
private String createCodice() {
Base62 base62 = Base62.createInstance();
String currentDateTime = String.valueOf(java.time.Instant.now().toEpochMilli());
return new String(
base62.encode(currentDateTime.getBytes(java.nio.charset.StandardCharsets.UTF_8)),
java.nio.charset.StandardCharsets.UTF_8
);
}
@Override
public ConfermaDTO update(ConfermaDTO confermaDTO) {
LOG.debug("Request to update Conferma : {}", confermaDTO);

View File

@@ -1,10 +1,12 @@
package it.sw.pa.comune.artegna.service.impl;
import it.sw.pa.comune.artegna.domain.Prenotazione;
import it.sw.pa.comune.artegna.domain.enumeration.StatoPrenotazione;
import it.sw.pa.comune.artegna.repository.PrenotazioneRepository;
import it.sw.pa.comune.artegna.service.PrenotazioneService;
import it.sw.pa.comune.artegna.service.dto.PrenotazioneDTO;
import it.sw.pa.comune.artegna.service.mapper.PrenotazioneMapper;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

View File

@@ -1,6 +1,8 @@
package it.sw.pa.comune.artegna.service.impl;
import it.sw.pa.comune.artegna.domain.Prenotazione;
import it.sw.pa.comune.artegna.domain.Struttura;
import it.sw.pa.comune.artegna.domain.enumeration.StatoPrenotazione;
import it.sw.pa.comune.artegna.repository.StrutturaRepository;
import it.sw.pa.comune.artegna.service.StrutturaService;
import it.sw.pa.comune.artegna.service.dto.StrutturaDTO;
@@ -8,6 +10,7 @@ import it.sw.pa.comune.artegna.service.mapper.StrutturaMapper;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

View File

@@ -2,8 +2,10 @@ package it.sw.pa.comune.artegna.service.impl;
import it.sw.pa.comune.artegna.domain.UtenteApp;
import it.sw.pa.comune.artegna.repository.UtenteAppRepository;
import it.sw.pa.comune.artegna.service.UserService;
import it.sw.pa.comune.artegna.service.UtenteAppService;
import it.sw.pa.comune.artegna.service.dto.UtenteAppDTO;
import it.sw.pa.comune.artegna.service.mapper.UserMapper;
import it.sw.pa.comune.artegna.service.mapper.UtenteAppMapper;
import java.util.LinkedList;
import java.util.List;
@@ -13,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -29,9 +32,20 @@ public class UtenteAppServiceImpl implements UtenteAppService {
private final UtenteAppMapper utenteAppMapper;
public UtenteAppServiceImpl(UtenteAppRepository utenteAppRepository, UtenteAppMapper utenteAppMapper) {
private final UserService userService;
private final UserMapper userMapper;
public UtenteAppServiceImpl(
UtenteAppRepository utenteAppRepository,
UtenteAppMapper utenteAppMapper,
UserService userService,
UserMapper userMapper
) {
this.utenteAppRepository = utenteAppRepository;
this.utenteAppMapper = utenteAppMapper;
this.userService = userService;
this.userMapper = userMapper;
}
@Override
@@ -95,4 +109,12 @@ public class UtenteAppServiceImpl implements UtenteAppService {
LOG.debug("Request to delete UtenteApp : {}", id);
utenteAppRepository.deleteById(id);
}
@Override
public UtenteAppDTO fetchUtenteAppFromUser() {
String currentLogin = SecurityContextHolder.getContext().getAuthentication().getName();
//User currentUser = userService.getUserWithAuthoritiesByLogin(currentLogin).orElseThrow();
UtenteAppDTO utenteAppDTO = utenteAppRepository.findByUsername(currentLogin).map(utenteAppMapper::toDto).orElseThrow();
return utenteAppDTO;
}
}

View File

@@ -1,13 +1,18 @@
package it.sw.pa.comune.artegna.web.rest;
import it.sw.pa.comune.artegna.domain.User;
import it.sw.pa.comune.artegna.repository.ConfermaRepository;
import it.sw.pa.comune.artegna.service.ConfermaQueryService;
import it.sw.pa.comune.artegna.service.ConfermaService;
import it.sw.pa.comune.artegna.service.UserService;
import it.sw.pa.comune.artegna.service.criteria.ConfermaCriteria;
import it.sw.pa.comune.artegna.service.dto.ConfermaDTO;
import it.sw.pa.comune.artegna.service.dto.UtenteAppDTO;
import it.sw.pa.comune.artegna.service.mapper.UserMapper;
import it.sw.pa.comune.artegna.web.rest.errors.BadRequestAlertException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.Principal;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -18,6 +23,8 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import tech.jhipster.web.util.HeaderUtil;
@@ -44,14 +51,22 @@ public class ConfermaResource {
private final ConfermaQueryService confermaQueryService;
private final UserService userService;
private final UserMapper userMapper;
public ConfermaResource(
ConfermaService confermaService,
ConfermaRepository confermaRepository,
ConfermaQueryService confermaQueryService
ConfermaQueryService confermaQueryService,
UserService userService,
UserMapper userMapper
) {
this.confermaService = confermaService;
this.confermaRepository = confermaRepository;
this.confermaQueryService = confermaQueryService;
this.userService = userService;
this.userMapper = userMapper;
}
/**
@@ -62,11 +77,18 @@ public class ConfermaResource {
* @throws URISyntaxException if the Location URI syntax is incorrect.
*/
@PostMapping("")
public ResponseEntity<ConfermaDTO> createConferma(@RequestBody ConfermaDTO confermaDTO) throws URISyntaxException {
public ResponseEntity<ConfermaDTO> createConferma(@RequestBody ConfermaDTO confermaDTO, @AuthenticationPrincipal UserDetails principal)
throws URISyntaxException {
LOG.debug("REST request to save Conferma : {}", confermaDTO);
if (confermaDTO.getId() != null) {
/*if (confermaDTO.getId() != null) {
throw new BadRequestAlertException("A new conferma cannot already have an ID", ENTITY_NAME, "idexists");
}
}*/
String currentLogin = principal.getUsername();
User currentUser = userService.getUserWithAuthoritiesByLogin(currentLogin).orElseThrow();
UtenteAppDTO utenteAppDTO = new UtenteAppDTO();
utenteAppDTO.setInternalUser(userMapper.userToUserDTO(currentUser));
confermaDTO.setConfermataDa(utenteAppDTO);
confermaDTO = confermaService.save(confermaDTO);
return ResponseEntity.created(new URI("/api/confermas/" + confermaDTO.getId()))
.headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, confermaDTO.getId().toString()))

View File

@@ -1,10 +1,16 @@
package it.sw.pa.comune.artegna.web.rest;
import it.sw.pa.comune.artegna.domain.User;
import it.sw.pa.comune.artegna.repository.PrenotazioneRepository;
import it.sw.pa.comune.artegna.security.AuthoritiesConstants;
import it.sw.pa.comune.artegna.service.PrenotazioneQueryService;
import it.sw.pa.comune.artegna.service.PrenotazioneService;
import it.sw.pa.comune.artegna.service.UserService;
import it.sw.pa.comune.artegna.service.UtenteAppService;
import it.sw.pa.comune.artegna.service.criteria.PrenotazioneCriteria;
import it.sw.pa.comune.artegna.service.dto.PrenotazioneDTO;
import it.sw.pa.comune.artegna.service.dto.UserDTO;
import it.sw.pa.comune.artegna.service.dto.UtenteAppDTO;
import it.sw.pa.comune.artegna.web.rest.errors.BadRequestAlertException;
import java.net.URI;
import java.net.URISyntaxException;
@@ -18,8 +24,11 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import tech.jhipster.service.filter.LongFilter;
import tech.jhipster.web.util.HeaderUtil;
import tech.jhipster.web.util.PaginationUtil;
import tech.jhipster.web.util.ResponseUtil;
@@ -44,14 +53,21 @@ public class PrenotazioneResource {
private final PrenotazioneQueryService prenotazioneQueryService;
private final UserService userService;
private final UtenteAppService utenteAppService;
public PrenotazioneResource(
PrenotazioneService prenotazioneService,
PrenotazioneRepository prenotazioneRepository,
PrenotazioneQueryService prenotazioneQueryService
PrenotazioneQueryService prenotazioneQueryService,
UserService userService,
UtenteAppService utenteAppService
) {
this.prenotazioneService = prenotazioneService;
this.prenotazioneRepository = prenotazioneRepository;
this.prenotazioneQueryService = prenotazioneQueryService;
this.userService = userService;
this.utenteAppService = utenteAppService;
}
/**
@@ -152,13 +168,26 @@ public class PrenotazioneResource {
@GetMapping("")
public ResponseEntity<List<PrenotazioneDTO>> getAllPrenotaziones(
PrenotazioneCriteria criteria,
@org.springdoc.core.annotations.ParameterObject Pageable pageable
@org.springdoc.core.annotations.ParameterObject Pageable pageable,
@AuthenticationPrincipal UserDetails principal
) {
LOG.debug("REST request to get Prenotaziones by criteria: {}", criteria);
Page<PrenotazioneDTO> page = prenotazioneQueryService.findByCriteria(criteria, pageable);
HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page);
return ResponseEntity.ok().headers(headers).body(page.getContent());
String currentUser = principal.getUsername();
User user = userService.getUserWithAuthoritiesByLogin(currentUser).orElseThrow();
// switch among user authorities
return switch (user.getAuthorities().stream().findFirst().orElseThrow().getName()) {
case AuthoritiesConstants.INCARICATO, AuthoritiesConstants.ADMIN -> ResponseEntity.ok().body(
prenotazioneQueryService.findByCriteria(criteria, pageable).getContent()
);
default -> {
UtenteAppDTO utenteAppDTO = utenteAppService.fetchUtenteAppFromUser();
criteria.setUtenteId(new LongFilter());
criteria.getUtenteId().setEquals(utenteAppDTO.getId());
yield ResponseEntity.ok().body(prenotazioneQueryService.findByCriteria(criteria, pageable).getContent());
}
};
}
/**

View File

@@ -1,9 +1,13 @@
package it.sw.pa.comune.artegna.web.rest;
import it.sw.pa.comune.artegna.domain.User;
import it.sw.pa.comune.artegna.repository.UtenteAppRepository;
import it.sw.pa.comune.artegna.security.SecurityUtils;
import it.sw.pa.comune.artegna.service.UserService;
import it.sw.pa.comune.artegna.service.UtenteAppService;
import it.sw.pa.comune.artegna.service.dto.UserDTO;
import it.sw.pa.comune.artegna.service.dto.UtenteAppDTO;
import it.sw.pa.comune.artegna.service.mapper.UserMapper;
import it.sw.pa.comune.artegna.web.rest.errors.BadRequestAlertException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
@@ -16,6 +20,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import tech.jhipster.web.util.HeaderUtil;
import tech.jhipster.web.util.ResponseUtil;
@@ -38,9 +44,20 @@ public class UtenteAppResource {
private final UtenteAppRepository utenteAppRepository;
public UtenteAppResource(UtenteAppService utenteAppService, UtenteAppRepository utenteAppRepository) {
private final UserService userService;
private final UserMapper userMapper;
public UtenteAppResource(
UtenteAppService utenteAppService,
UtenteAppRepository utenteAppRepository,
UserService userService,
UserMapper userMapper
) {
this.utenteAppService = utenteAppService;
this.utenteAppRepository = utenteAppRepository;
this.userService = userService;
this.userMapper = userMapper;
}
/**
@@ -51,11 +68,15 @@ public class UtenteAppResource {
* @throws URISyntaxException if the Location URI syntax is incorrect.
*/
@PostMapping("")
public ResponseEntity<UtenteAppDTO> createUtenteApp(@Valid @RequestBody UtenteAppDTO utenteAppDTO) throws URISyntaxException {
public ResponseEntity<UtenteAppDTO> createUtenteApp(
@Valid @RequestBody UtenteAppDTO utenteAppDTO,
@AuthenticationPrincipal UserDetails principal
) throws URISyntaxException {
LOG.debug("REST request to save UtenteApp : {}", utenteAppDTO);
if (utenteAppDTO.getId() != null) {
throw new BadRequestAlertException("A new utenteApp cannot already have an ID", ENTITY_NAME, "idexists");
}
utenteAppDTO = utenteAppService.save(utenteAppDTO);
return ResponseEntity.created(new URI("/api/utente-apps/" + utenteAppDTO.getId()))
.headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, utenteAppDTO.getId().toString()))
@@ -176,7 +197,10 @@ public class UtenteAppResource {
* @throws URISyntaxException if the Location URI syntax is incorrect.
*/
@PostMapping("/current")
public ResponseEntity<UtenteAppDTO> saveCurrentUserProfile(@Valid @RequestBody UtenteAppDTO utenteAppDTO) throws URISyntaxException {
public ResponseEntity<UtenteAppDTO> saveCurrentUserProfile(
@Valid @RequestBody UtenteAppDTO utenteAppDTO,
@AuthenticationPrincipal UserDetails principal
) throws URISyntaxException {
LOG.debug("REST request to save current user profile : {}", utenteAppDTO);
String currentUserLogin = SecurityUtils.getCurrentUserLogin().orElseThrow(() ->
@@ -191,6 +215,10 @@ public class UtenteAppResource {
// Update existing profile
result = utenteAppService.update(utenteAppDTO);
} else {
// collect current user login infos
String currentLogin = principal.getUsername();
User currentUser = userService.getUserWithAuthoritiesByLogin(currentLogin).orElseThrow();
utenteAppDTO.setInternalUser(userMapper.userToUserDTO(currentUser));
// Create new profile
result = utenteAppService.save(utenteAppDTO);
}

View File

@@ -42,10 +42,15 @@ spring:
# Remove 'faker' if you do not want the sample data to be loaded automatically
contexts: dev, faker
mail:
host: localhost
port: 25
username:
password:
host: smtps.aruba.it
port: 465
username: noreply@bigc.it
password: Ungr@nb3l4n1m4l0
protocol: smtps
properties.mail.smtp:
auth: true
starttls.enable: true
messages:
cache-duration: PT1S # 1 second, see the ISO 8601 standard
thymeleaf:

View File

@@ -43,10 +43,14 @@ spring:
liquibase:
contexts: prod
mail:
host: localhost
port: 25
username:
password:
host: smtps.aruba.it
port: 465
username: noreply@bigc.it
password: Ungr@nb3l4n1m4l0
protocol: smtps
properties.mail.smtp:
auth: true
starttls.enable: true
thymeleaf:
cache: true

View File

@@ -193,7 +193,7 @@ jhipster:
# allow-credentials: true
# max-age: 1800
mail:
from: smartbooking@localhost
from: noreply@bigc.it
api-docs:
default-include-pattern: /api/**
management-include-pattern: /management/**
@@ -222,3 +222,8 @@ jhipster:
# ===================================================================
# application:
# Twilio configuration
twilio:
accountSid: 'AC0ea64cd2bb43c6ed76e9acdf8087afa7'
authToken: 'cfd2943e70ce09b07ae75f3f1650ccb4'
phoneNumber: YOUR_PHONE_NUMBER

View File

@@ -1,3 +1,4 @@
name
ROLE_ADMIN
ROLE_USER
ROLE_INCARICATO
1 name
2 ROLE_ADMIN
3 ROLE_USER
4 ROLE_INCARICATO

View File

@@ -11,7 +11,9 @@
<div class="alert alert-success" v-if="authenticated">
<span v-if="username">{{ t$('home.logged.message', { username }) }}</span>
</div>
<button class="btn btn-primary jhi-create-entity" v-if="authenticated">
<router-link to="/prenotazione">{{ t$('home.prenotazioni') }}</router-link>
</button>
<div class="alert alert-warning" v-if="authenticated && profileIncomplete && !checkingProfile">
<span>{{ t$('home.profile.incomplete.message') }}</span>
&nbsp;

View File

@@ -1,10 +1,15 @@
import { type Ref, defineComponent, inject, onMounted, ref, watch } from 'vue';
import { type Ref, computed, defineComponent, inject, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useAlertService } from '@/shared/alert/alert.service';
import { useDateFormat } from '@/shared/composables';
import { type IConferma } from '@/shared/model/conferma.model';
import { TipoConferma } from '@/shared/model/enumerations/tipo-conferma.model';
import { type IPrenotazione } from '@/shared/model/prenotazione.model';
import { Authority } from '@/shared/security/authority';
import { useAccountStore } from '@/shared/config/store/account-store';
import ConfermaService from '../conferma/conferma.service';
import PrenotazioneService from './prenotazione.service';
export default defineComponent({
@@ -13,7 +18,9 @@ export default defineComponent({
const { t: t$ } = useI18n();
const dateFormat = useDateFormat();
const prenotazioneService = inject('prenotazioneService', () => new PrenotazioneService());
const confermaService = inject('confermaService', () => new ConfermaService());
const alertService = inject('alertService', () => useAlertService(), true);
const accountStore = useAccountStore();
const itemsPerPage = ref(20);
const queryCount: Ref<number> = ref(null);
@@ -23,9 +30,25 @@ export default defineComponent({
const totalItems = ref(0);
const prenotaziones: Ref<IPrenotazione[]> = ref([]);
const isFetching = ref(false);
// Role-based helpers
const account = computed(() => accountStore.account);
const isIncaricato = computed(() => account.value?.authorities?.includes(Authority.INCARICATO) ?? false);
// ROLE_INCARICATO: split list into pending (no conferma) and completed (with conferma)
const prenotazioniPendenti = computed(() => prenotaziones.value.filter(p => !p.conferma));
const prenotazioniCompletate = computed(() => prenotaziones.value.filter(p => !!p.conferma));
// "Prendi in carico" modal state
const showPrendiInCaricoModal = ref(false);
const selectedPrenotazione: Ref<IPrenotazione | null> = ref(null);
const confermaForm = ref<{ tipoConferma: string; motivoConferma: string }>({
tipoConferma: '',
motivoConferma: '',
});
const isSubmittingConferma = ref(false);
const clear = () => {
page.value = 1;
};
@@ -87,6 +110,37 @@ export default defineComponent({
}
};
const prendiInCarico = (prenotazione: IPrenotazione) => {
selectedPrenotazione.value = prenotazione;
confermaForm.value = { tipoConferma: '', motivoConferma: '' };
showPrendiInCaricoModal.value = true;
};
const closePrendiInCaricoModal = () => {
showPrendiInCaricoModal.value = false;
selectedPrenotazione.value = null;
};
const submitConferma = async () => {
if (!selectedPrenotazione.value || !confermaForm.value.tipoConferma) return;
isSubmittingConferma.value = true;
try {
const newConferma: IConferma = {
id: selectedPrenotazione.value.id,
tipoConferma: confermaForm.value.tipoConferma as keyof typeof TipoConferma,
motivoConferma: confermaForm.value.motivoConferma || null,
};
await confermaService().create(newConferma);
alertService.showInfo(t$('smartbookingApp.prenotazione.prendiInCarico.success').toString());
closePrendiInCaricoModal();
await retrievePrenotaziones();
} catch (error) {
alertService.showHttpError(error.response);
} finally {
isSubmittingConferma.value = false;
}
};
const changeOrder = (newOrder: string) => {
if (propOrder.value === newOrder) {
reverse.value = !reverse.value;
@@ -96,18 +150,14 @@ export default defineComponent({
propOrder.value = newOrder;
};
// Whenever order changes, reset the pagination
watch([propOrder, reverse], async () => {
if (page.value === 1) {
// first page, retrieve new data
await retrievePrenotaziones();
} else {
// reset the pagination
clear();
}
});
// Whenever page changes, switch to the new page.
watch(page, async () => {
await retrievePrenotaziones();
});
@@ -132,6 +182,18 @@ export default defineComponent({
totalItems,
changeOrder,
t$,
// Role-based
isIncaricato,
prenotazioniPendenti,
prenotazioniCompletate,
// Prendi in carico modal
showPrendiInCaricoModal,
selectedPrenotazione,
confermaForm,
isSubmittingConferma,
prendiInCarico,
closePrendiInCaricoModal,
submitConferma,
};
},
});

View File

@@ -1,5 +1,7 @@
<template>
<div>
<!-- ============================= ROLE_USER VIEW ============================= -->
<template v-if="!isIncaricato">
<h2 id="page-heading" data-cy="PrenotazioneHeading">
<span id="prenotazione">{{ t$('smartbookingApp.prenotazione.home.title') }}</span>
<div class="d-flex justify-content-end">
@@ -7,7 +9,7 @@
<font-awesome-icon icon="sync" :spin="isFetching"></font-awesome-icon>
<span>{{ t$('smartbookingApp.prenotazione.home.refreshListLabel') }}</span>
</button>
<router-link :to="{ name: 'PrenotazioneCreate' }" custom v-slot="{ navigate }">
<router-link :to="{ name: 'PrenotazioneNuova' }" custom v-slot="{ navigate }">
<button
@click="navigate"
id="jh-create-entity"
@@ -44,30 +46,14 @@
<span>{{ t$('smartbookingApp.prenotazione.stato') }}</span>
<jhi-sort-indicator :current-order="propOrder" :reverse="reverse" :field-name="'stato'"></jhi-sort-indicator>
</th>
<th scope="col" @click="changeOrder('motivoEvento')">
<span>{{ t$('smartbookingApp.prenotazione.motivoEvento') }}</span>
<jhi-sort-indicator :current-order="propOrder" :reverse="reverse" :field-name="'motivoEvento'"></jhi-sort-indicator>
</th>
<th scope="col" @click="changeOrder('numeroPartecipanti')">
<span>{{ t$('smartbookingApp.prenotazione.numeroPartecipanti') }}</span>
<jhi-sort-indicator :current-order="propOrder" :reverse="reverse" :field-name="'numeroPartecipanti'"></jhi-sort-indicator>
</th>
<th scope="col" @click="changeOrder('noteUtente')">
<span>{{ t$('smartbookingApp.prenotazione.noteUtente') }}</span>
<jhi-sort-indicator :current-order="propOrder" :reverse="reverse" :field-name="'noteUtente'"></jhi-sort-indicator>
</th>
<th scope="col" @click="changeOrder('conferma.id')">
<span>{{ t$('smartbookingApp.prenotazione.conferma') }}</span>
<jhi-sort-indicator :current-order="propOrder" :reverse="reverse" :field-name="'conferma.id'"></jhi-sort-indicator>
</th>
<th scope="col" @click="changeOrder('utente.username')">
<span>{{ t$('smartbookingApp.prenotazione.utente') }}</span>
<jhi-sort-indicator :current-order="propOrder" :reverse="reverse" :field-name="'utente.username'"></jhi-sort-indicator>
</th>
<th scope="col" @click="changeOrder('struttura.nome')">
<span>{{ t$('smartbookingApp.prenotazione.struttura') }}</span>
<jhi-sort-indicator :current-order="propOrder" :reverse="reverse" :field-name="'struttura.nome'"></jhi-sort-indicator>
</th>
<th scope="col" @click="changeOrder('motivoEvento')">
<span>{{ t$('smartbookingApp.prenotazione.motivoEvento') }}</span>
<jhi-sort-indicator :current-order="propOrder" :reverse="reverse" :field-name="'motivoEvento'"></jhi-sort-indicator>
</th>
<th scope="col"></th>
</tr>
</thead>
@@ -81,30 +67,10 @@
<td>{{ formatDateShort(prenotazione.oraInizio) || '' }}</td>
<td>{{ formatDateShort(prenotazione.oraFine) || '' }}</td>
<td>{{ t$('smartbookingApp.StatoPrenotazione.' + prenotazione.stato) }}</td>
<td>
<div v-if="prenotazione.struttura">{{ prenotazione.struttura.nome }}</div>
</td>
<td>{{ prenotazione.motivoEvento }}</td>
<td>{{ prenotazione.numeroPartecipanti }}</td>
<td>{{ prenotazione.noteUtente }}</td>
<td>
<div v-if="prenotazione.conferma">
<router-link :to="{ name: 'ConfermaView', params: { confermaId: prenotazione.conferma.id } }">{{
prenotazione.conferma.id
}}</router-link>
</div>
</td>
<td>
<div v-if="prenotazione.utente">
<router-link :to="{ name: 'UtenteAppView', params: { utenteAppId: prenotazione.utente.id } }">{{
prenotazione.utente.username
}}</router-link>
</div>
</td>
<td>
<div v-if="prenotazione.struttura">
<router-link :to="{ name: 'StrutturaView', params: { strutturaId: prenotazione.struttura.id } }">{{
prenotazione.struttura.nome
}}</router-link>
</div>
</td>
<td class="text-end">
<div class="btn-group">
<router-link
@@ -115,6 +81,18 @@
<font-awesome-icon icon="eye"></font-awesome-icon>
<span class="d-none d-md-inline">{{ t$('entity.action.view') }}</span>
</router-link>
<!-- Show conferma link when conferma exists -->
<router-link
v-if="prenotazione.conferma"
:to="{ name: 'ConfermaView', params: { confermaId: prenotazione.conferma.id } }"
class="btn btn-secondary btn-sm"
data-cy="entityConfermaButton"
>
<font-awesome-icon icon="file-alt"></font-awesome-icon>
<span class="d-none d-md-inline">{{ t$('smartbookingApp.prenotazione.viewConferma') }}</span>
</router-link>
<!-- Edit and Delete only when no conferma -->
<template v-if="!prenotazione.conferma">
<router-link
:to="{ name: 'PrenotazioneEdit', params: { prenotazioneId: prenotazione.id } }"
class="btn btn-primary btn-sm edit"
@@ -133,6 +111,7 @@
<font-awesome-icon icon="times"></font-awesome-icon>
<span class="d-none d-md-inline">{{ t$('entity.action.delete') }}</span>
</b-button>
</template>
</div>
</td>
</tr>
@@ -171,6 +150,206 @@
<b-pagination size="md" :total-rows="totalItems" v-model="page" :per-page="itemsPerPage"></b-pagination>
</div>
</div>
</template>
<!-- ============================= ROLE_INCARICATO VIEW ============================= -->
<template v-if="isIncaricato">
<h2 id="page-heading" data-cy="PrenotazioneHeading">
<span>{{ t$('smartbookingApp.prenotazione.home.titleIncaricato') }}</span>
<div class="d-flex justify-content-end">
<button class="btn btn-info" @click="handleSyncList" :disabled="isFetching">
<font-awesome-icon icon="sync" :spin="isFetching"></font-awesome-icon>
<span>{{ t$('smartbookingApp.prenotazione.home.refreshListLabel') }}</span>
</button>
</div>
</h2>
<br />
<!-- Table 1: Pending (no conferma) -->
<h4>{{ t$('smartbookingApp.prenotazione.pendenti.title') }}</h4>
<div class="alert alert-warning" v-if="!isFetching && prenotazioniPendenti.length === 0">
<span>{{ t$('smartbookingApp.prenotazione.pendenti.notFound') }}</span>
</div>
<div class="table-responsive" v-if="prenotazioniPendenti.length > 0">
<table class="table table-striped" aria-describedby="prenotazioni-pendenti">
<thead>
<tr>
<th scope="col">{{ t$('global.field.id') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.oraInizio') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.oraFine') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.stato') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.utente') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.struttura') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.motivoEvento') }}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="prenotazione in prenotazioniPendenti" :key="prenotazione.id" data-cy="entityTablePendenti">
<td>
<router-link :to="{ name: 'PrenotazioneView', params: { prenotazioneId: prenotazione.id } }">{{
prenotazione.id
}}</router-link>
</td>
<td>{{ formatDateShort(prenotazione.oraInizio) || '' }}</td>
<td>{{ formatDateShort(prenotazione.oraFine) || '' }}</td>
<td>{{ t$('smartbookingApp.StatoPrenotazione.' + prenotazione.stato) }}</td>
<td>
<div v-if="prenotazione.utente">{{ prenotazione.utente.username }}</div>
</td>
<td>
<div v-if="prenotazione.struttura">{{ prenotazione.struttura.nome }}</div>
</td>
<td>{{ prenotazione.motivoEvento }}</td>
<td class="text-end">
<div class="btn-group">
<router-link
:to="{ name: 'PrenotazioneView', params: { prenotazioneId: prenotazione.id } }"
class="btn btn-info btn-sm details"
data-cy="entityDetailsButton"
>
<font-awesome-icon icon="eye"></font-awesome-icon>
<span class="d-none d-md-inline">{{ t$('entity.action.view') }}</span>
</router-link>
<button
class="btn btn-success btn-sm"
data-cy="prendiInCaricoButton"
@click="prendiInCarico(prenotazione)"
>
<font-awesome-icon icon="check"></font-awesome-icon>
<span class="d-none d-md-inline">{{ t$('smartbookingApp.prenotazione.prendiInCarico.button') }}</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<br />
<!-- Table 2: Completed (with conferma) -->
<h4>{{ t$('smartbookingApp.prenotazione.completate.title') }}</h4>
<div class="alert alert-warning" v-if="!isFetching && prenotazioniCompletate.length === 0">
<span>{{ t$('smartbookingApp.prenotazione.completate.notFound') }}</span>
</div>
<div class="table-responsive" v-if="prenotazioniCompletate.length > 0">
<table class="table table-striped" aria-describedby="prenotazioni-completate">
<thead>
<tr>
<th scope="col">{{ t$('global.field.id') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.oraInizio') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.oraFine') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.stato') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.utente') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.struttura') }}</th>
<th scope="col">{{ t$('smartbookingApp.prenotazione.conferma') }}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr v-for="prenotazione in prenotazioniCompletate" :key="prenotazione.id" data-cy="entityTableCompletate">
<td>
<router-link :to="{ name: 'PrenotazioneView', params: { prenotazioneId: prenotazione.id } }">{{
prenotazione.id
}}</router-link>
</td>
<td>{{ formatDateShort(prenotazione.oraInizio) || '' }}</td>
<td>{{ formatDateShort(prenotazione.oraFine) || '' }}</td>
<td>{{ t$('smartbookingApp.StatoPrenotazione.' + prenotazione.stato) }}</td>
<td>
<div v-if="prenotazione.utente">{{ prenotazione.utente.username }}</div>
</td>
<td>
<div v-if="prenotazione.struttura">{{ prenotazione.struttura.nome }}</div>
</td>
<td>
<span class="badge bg-success">
{{ t$('smartbookingApp.TipoConferma.' + prenotazione.conferma?.tipoConferma) }}
</span>
</td>
<td class="text-end">
<div class="btn-group">
<router-link
:to="{ name: 'PrenotazioneView', params: { prenotazioneId: prenotazione.id } }"
class="btn btn-info btn-sm details"
data-cy="entityDetailsButton"
>
<font-awesome-icon icon="eye"></font-awesome-icon>
<span class="d-none d-md-inline">{{ t$('entity.action.view') }}</span>
</router-link>
<router-link
:to="{ name: 'ConfermaView', params: { confermaId: prenotazione.conferma.id } }"
class="btn btn-secondary btn-sm"
data-cy="entityConfermaButton"
>
<font-awesome-icon icon="file-alt"></font-awesome-icon>
<span class="d-none d-md-inline">{{ t$('smartbookingApp.prenotazione.viewConferma') }}</span>
</router-link>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination (shared) -->
<div v-show="prenotaziones?.length > 0">
<div class="d-flex justify-content-center">
<jhi-item-count :page="page" :total="queryCount" :items-per-page="itemsPerPage"></jhi-item-count>
</div>
<div class="d-flex justify-content-center">
<b-pagination size="md" :total-rows="totalItems" v-model="page" :per-page="itemsPerPage"></b-pagination>
</div>
</div>
<!-- "Prendi in carico" modal -->
<b-modal v-model="showPrendiInCaricoModal" :title="t$('smartbookingApp.prenotazione.prendiInCarico.title')" @hidden="closePrendiInCaricoModal">
<div v-if="selectedPrenotazione">
<p class="text-muted mb-3">
{{ t$('smartbookingApp.prenotazione.prendiInCarico.subtitle', { id: selectedPrenotazione.id }) }}
</p>
<div class="mb-3">
<label for="tipoConfermaSelect" class="form-label fw-bold">
{{ t$('smartbookingApp.conferma.tipoConferma') }} <span class="text-danger">*</span>
</label>
<select id="tipoConfermaSelect" class="form-select" v-model="confermaForm.tipoConferma">
<option value="">{{ t$('smartbookingApp.prenotazione.prendiInCarico.selectTipo') }}</option>
<option value="CONFERMATA">{{ t$('smartbookingApp.TipoConferma.CONFERMATA') }}</option>
<option value="RIFIUTATA">{{ t$('smartbookingApp.TipoConferma.RIFIUTATA') }}</option>
</select>
</div>
<div class="mb-3">
<label for="motivoConfermaInput" class="form-label fw-bold">
{{ t$('smartbookingApp.conferma.motivoConferma') }}
</label>
<textarea
id="motivoConfermaInput"
class="form-control"
rows="4"
v-model="confermaForm.motivoConferma"
:placeholder="t$('smartbookingApp.prenotazione.prendiInCarico.motivoPlaceholder')"
></textarea>
</div>
</div>
<template #footer>
<button type="button" class="btn btn-secondary" @click="closePrendiInCaricoModal">
{{ t$('entity.action.cancel') }}
</button>
<button
type="button"
class="btn btn-primary"
:disabled="!confermaForm.tipoConferma || isSubmittingConferma"
@click="submitConferma"
>
<span v-if="isSubmittingConferma">
<b-spinner small></b-spinner>
</span>
{{ t$('smartbookingApp.prenotazione.prendiInCarico.submit') }}
</button>
</template>
</b-modal>
</template>
</div>
</template>

View File

@@ -16,6 +16,7 @@ import { faEye } from '@fortawesome/free-solid-svg-icons/faEye';
import { faFlag } from '@fortawesome/free-solid-svg-icons/faFlag';
import { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart';
import { faHome } from '@fortawesome/free-solid-svg-icons/faHome';
import { faIdCard } from '@fortawesome/free-solid-svg-icons/faIdCard';
import { faList } from '@fortawesome/free-solid-svg-icons/faList';
import { faLock } from '@fortawesome/free-solid-svg-icons/faLock';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
@@ -69,6 +70,7 @@ export function initFortAwesome(vue: App) {
faFlag,
faHeart,
faHome,
faIdCard,
faList,
faLock,
faPencilAlt,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@@ -61,7 +61,7 @@ Main page styles
display: inline-block;
width: 347px;
height: 497px;
background: url('/content/images/jhipster_family_member_0.svg') no-repeat center top;
background: url('/content/images/artegna.png') no-repeat center top;
background-size: contain;
}
@@ -73,7 +73,7 @@ Main page styles
only screen and (min-resolution: 192dpi),
only screen and (min-resolution: 2dppx) {
.hipster {
background: url('/content/images/jhipster_family_member_0.svg') no-repeat center top;
background: url('/content/images/artegna.png') no-repeat center top;
background-size: contain;
}
}

View File

@@ -1,7 +1,7 @@
{
"home": {
"title": "Benvenuto, Java Hipster!",
"subtitle": "Questa è la tua home page",
"title": "Benvenuto!",
"subtitle": "Questa è l'home page del sistema smARTbooking.",
"logged": {
"message": "Autenticato come \"{ username }\"."
},
@@ -11,7 +11,7 @@
"link": "Completa il profilo"
}
},
"question": "In caso di domande su JHipster:",
"question": "Cosa è possibile fare con smARTbooking:",
"link": {
"homepage": "Homepage JHipster",
"stackoverflow": "JHipster su Stack Overflow",
@@ -20,6 +20,7 @@
"follow": "segui {'@'}jhipster su Twitter"
},
"like": "Se ti piace JHipster, non dimenticarti di darci una stella su",
"github": "GitHub"
"github": "GitHub",
"prenotazioni": "Vai alle tue prenotazioni"
}
}

View File

@@ -2,11 +2,12 @@
"smartbookingApp": {
"prenotazione": {
"home": {
"title": "Prenotaziones",
"refreshListLabel": "Refresh list",
"createLabel": "Genera un nuovo Prenotazione",
"title": "Le mie prenotazioni",
"titleIncaricato": "Gestione prenotazioni",
"refreshListLabel": "Aggiorna lista",
"createLabel": "Nuova prenotazione",
"createOrEditLabel": "Genera o modifica un Prenotazione",
"notFound": "No Prenotaziones found"
"notFound": "Nessuna prenotazione trovata"
},
"created": "&Egrave; stato generato un nuovo Prenotazione con identificatore {{ param }}",
"updated": "&Egrave; stato aggiornato Prenotazione identificato da {{ param }}",
@@ -27,6 +28,24 @@
"conferma": "Conferma",
"utente": "Utente",
"struttura": "Struttura",
"viewConferma": "Vedi conferma",
"pendenti": {
"title": "Prenotazioni in attesa",
"notFound": "Nessuna prenotazione in attesa"
},
"completate": {
"title": "Prenotazioni gestite",
"notFound": "Nessuna prenotazione gestita"
},
"prendiInCarico": {
"button": "Prendi in carico",
"title": "Prendi in carico la prenotazione",
"subtitle": "Prenotazione #{id}",
"selectTipo": "-- Seleziona esito --",
"motivoPlaceholder": "Inserisci il motivo o le note relative alla decisione...",
"submit": "Conferma",
"success": "Prenotazione presa in carico con successo"
},
"userForm": {
"title": "Nuova Prenotazione",
"bookingDetails": "Dettagli",