TimeSlotService.java
/*
* Copyright (c) 2002-2022, City of Paris
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright notice
* and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice
* and the following disclaimer in the documentation and/or other materials
* provided with the distribution.
*
* 3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* License 1.0
*/
package fr.paris.lutece.plugins.appointment.service;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import fr.paris.lutece.plugins.appointment.business.planning.TimeSlot;
import fr.paris.lutece.plugins.appointment.business.planning.TimeSlotHome;
import fr.paris.lutece.plugins.appointment.business.planning.WeekDefinition;
import fr.paris.lutece.plugins.appointment.business.planning.WeekDefinitionHome;
import fr.paris.lutece.plugins.appointment.business.planning.WorkingDay;
import fr.paris.lutece.plugins.appointment.business.rule.ReservationRule;
import fr.paris.lutece.plugins.appointment.business.rule.ReservationRuleHome;
import fr.paris.lutece.plugins.appointment.service.listeners.WeekDefinitionManagerListener;
/**
* Service class for the time slot
*
* @author Laurent Payen
*
*/
public final class TimeSlotService
{
/**
* Private constructor - this class does not need to be instantiated
*/
private TimeSlotService( )
{
}
/**
* Build a list of timeSlot Object from a starting time to an endingTime
*
* @param nIdWorkingDay
* the workingDay Id
* @param startingTime
* the starting time
* @param endingTime
* the ending time
* @param nDuration
* the duration of the slot
* @param nMaxCapacity
* the maximum capacity of the slot
* @return the list of TimeSlot built
*/
public static List<TimeSlot> generateListTimeSlot( int nIdWorkingDay, LocalTime startingTime, LocalTime endingTime, int nDuration, int nMaxCapacity,
boolean forceTimeSlotCreationWithMinTime )
{
List<TimeSlot> listTimeSlot = new ArrayList<>( );
LocalDateTime tempStartingDateTime = LocalDate.now( ).atTime( startingTime );
LocalDateTime tempEndingDateTime = LocalDate.now( ).atTime( startingTime.plusMinutes( nDuration ) );
LocalDateTime endingDateTime = LocalDate.now( ).atTime( endingTime );
while ( !tempEndingDateTime.isAfter( endingDateTime ) )
{
listTimeSlot.add( generateTimeSlot( nIdWorkingDay, tempStartingDateTime.toLocalTime( ), tempEndingDateTime.toLocalTime( ),
Boolean.TRUE.booleanValue( ), nMaxCapacity ) );
tempStartingDateTime = tempEndingDateTime;
tempEndingDateTime = tempEndingDateTime.plusMinutes( nDuration );
}
if ( forceTimeSlotCreationWithMinTime )
{
tempStartingDateTime = tempEndingDateTime.minusMinutes( nDuration );
if ( tempStartingDateTime.isBefore( endingDateTime ) )
{
listTimeSlot.add( generateTimeSlot( nIdWorkingDay, tempStartingDateTime.toLocalTime( ), endingTime, Boolean.FALSE, nMaxCapacity ) );
}
}
return listTimeSlot;
}
/**
* Save a time slot
*
* @param timeSlot
* the time slot to save
* @return the time slot saved
*/
public static TimeSlot saveTimeSlot( TimeSlot timeSlot )
{
TimeSlot timeSlotSaved = null;
if ( timeSlot.getIdTimeSlot( ) == 0 )
{
timeSlotSaved = TimeSlotService.createTimeSlot( timeSlot );
}
else
{
timeSlotSaved = TimeSlotService.updateTimeSlot( timeSlot );
}
return timeSlotSaved;
}
/**
* Create a time slot in db
*
* @param timeSlot
* the time slot to create
* @return the time slot created
*/
public static TimeSlot createTimeSlot( TimeSlot timeSlot )
{
return TimeSlotHome.create( timeSlot );
}
/**
* Build a timeSlot with all its values
*
* @param nIdWorkingDay
* the workingDay Id
* @param startingTime
* the starting time
* @param endingTime
* the ending time
* @param isOpen
* true if the slot is open
* @param nMaxCapacity
* the maximum capacity of the slot
* @return the timeSLot built
*/
public static TimeSlot generateTimeSlot( int nIdWorkingDay, LocalTime startingTime, LocalTime endingTime, boolean isOpen, int nMaxCapacity )
{
TimeSlot timeSlot = new TimeSlot( );
timeSlot.setIdWorkingDay( nIdWorkingDay );
timeSlot.setIsOpen( isOpen );
timeSlot.setStartingTime( startingTime );
timeSlot.setEndingTime( endingTime );
timeSlot.setMaxCapacity( nMaxCapacity );
return timeSlot;
}
/**
* Find the time slots of a working day
*
* @param nIdWorkingDay
* the working day Id
* @return the list of the timeSlot of this workingDay
*/
public static List<TimeSlot> findListTimeSlotByWorkingDay( int nIdWorkingDay )
{
return TimeSlotHome.findByIdWorkingDay( nIdWorkingDay );
}
/**
* Find a timeSlot with its primary key
*
* @param nIdTimeSlot
* the timeSlot Id
* @return the timeSlot found
*/
public static TimeSlot findTimeSlotById( int nIdTimeSlot )
{
return TimeSlotHome.findByPrimaryKey( nIdTimeSlot );
}
/**
* Update a timeSLot in database
*
* @param timeSlot
* the timeSlot to update
* @param bEndingTimeHasChanged
* if the ending time has changed, need to regenerate and update all the next time slots
* @param previousEndingTime
* the previous ending time of the current time slot
* @param bShifSlot
* true if the user has decided to shift the next slots
*/
public static void updateTimeSlot( TimeSlot timeSlot, boolean bEndingTimeHasChanged, LocalTime previousEndingTime, boolean bShifSlot )
{
WorkingDay workingDay = WorkingDayService.findWorkingDayById( timeSlot.getIdWorkingDay( ) );
ReservationRule reservationRule = ReservationRuleHome.findByPrimaryKey( workingDay.getIdReservationRule( ) );
List<WeekDefinition> listWeek = WeekDefinitionHome.findByReservationRule( reservationRule.getIdReservationRule( ) );
int nDuration = reservationRule.getDurationAppointments( );
if ( bEndingTimeHasChanged )
{
if ( !bShifSlot )
{
updateTimeSlotWithoutShift( timeSlot, workingDay, reservationRule, nDuration );
}
else
{
updateTimeSlotWithShift( timeSlot, workingDay, reservationRule, nDuration, previousEndingTime );
}
}
else
{
saveTimeSlot( timeSlot );
}
if ( CollectionUtils.isNotEmpty( listWeek ) )
{
WeekDefinitionManagerListener.notifyListenersListWeekDefinitionChanged( reservationRule.getIdForm( ), listWeek );
}
}
/**
* Update a time slot with shifting the next
*
* @param timeSlot
* the time slot modified
* @param workingDay
* the working day
* @param reservationRule
* the reservation rule
* @param nDuration
* the duration of a time slot
* @param previousEndingTime
* the previous ending time
*/
private static void updateTimeSlotWithShift( TimeSlot timeSlot, WorkingDay workingDay, ReservationRule reservationRule, int nDuration,
LocalTime previousEndingTime )
{
// We want to shift all the next time slots
// Get all the time slots of the day
List<TimeSlot> listOfAllTimeSlotsOfThisWorkingDay = findListTimeSlotByWorkingDay( workingDay.getIdWorkingDay( ) );
// Remove the current time slot and all the time slots before it
listOfAllTimeSlotsOfThisWorkingDay = listOfAllTimeSlotsOfThisWorkingDay.stream( )
.filter( timeSlotToKeep -> timeSlotToKeep.getStartingTime( ).isAfter( timeSlot.getStartingTime( ) ) ).collect( Collectors.toList( ) );
// Need to delete all the time slots until the new end of this
// time slot
List<TimeSlot> listTimeSlotToDelete = listOfAllTimeSlotsOfThisWorkingDay.stream( )
.filter( timeSlotToDelete -> timeSlotToDelete.getStartingTime( ).isAfter( timeSlot.getStartingTime( ) )
&& !timeSlotToDelete.getEndingTime( ).isAfter( timeSlot.getEndingTime( ) ) )
.collect( Collectors.toList( ) );
deleteListTimeSlot( listTimeSlotToDelete );
listOfAllTimeSlotsOfThisWorkingDay.removeAll( listTimeSlotToDelete );
// Need to order the list of time slot to shift according to the
// shift
// if the new ending time is before the previous ending time,
// the list has to be ordered in chronological order ascending
// and the first time slot to shift is the closest to the
// current
// time slot
// (because we have an integrity constraint for the time slot,
// it
// can't have the same starting or ending time as another time
// slot
List<TimeSlot> listTimeSlotToShift = new ArrayList<>( );
listTimeSlotToShift.addAll( listOfAllTimeSlotsOfThisWorkingDay );
listTimeSlotToShift = listTimeSlotToShift.stream( )
.sorted( ( timeSlot1, timeSlot2 ) -> timeSlot1.getStartingTime( ).compareTo( timeSlot2.getStartingTime( ) ) ).collect( Collectors.toList( ) );
boolean bNewEndingTimeIsAfterThePreviousTime = false;
// Need to know the ending time of the day
LocalTime endingTimeOfTheDay = WorkingDayService.getMaxEndingTimeOfAWorkingDay( workingDay );
long timeToAdd = 0;
long timeToSubstract = 0;
if ( previousEndingTime.isBefore( timeSlot.getEndingTime( ) ) )
{
bNewEndingTimeIsAfterThePreviousTime = true;
// Need to find the next available time slot, to know how to
// add to the starting time of the next time slot to match
// with
// the new end of the current time slot
if ( CollectionUtils.isNotEmpty( listTimeSlotToShift ) )
{
TimeSlot nextTimeSlot = listTimeSlotToShift.stream( ).min( ( t1, t2 ) -> t1.getStartingTime( ).compareTo( t2.getStartingTime( ) ) )
.orElse( listTimeSlotToShift.get( 0 ) );
if ( timeSlot.getEndingTime( ).isAfter( nextTimeSlot.getStartingTime( ) ) )
{
timeToAdd = nextTimeSlot.getStartingTime( ).until( timeSlot.getEndingTime( ), ChronoUnit.MINUTES );
}
else
{
timeToAdd = timeSlot.getEndingTime( ).until( nextTimeSlot.getStartingTime( ), ChronoUnit.MINUTES );
}
Collections.reverse( listTimeSlotToShift );
}
else
{
timeToAdd = previousEndingTime.until( timeSlot.getEndingTime( ), ChronoUnit.MINUTES );
}
}
else
{
timeToSubstract = timeSlot.getEndingTime( ).until( previousEndingTime, ChronoUnit.MINUTES );
}
updateTimeSlot( timeSlot );
// Need to set the new starting and ending time of all the time
// slots
// to shift and update them
for ( TimeSlot timeSlotToShift : listTimeSlotToShift )
{
// If the new ending time is after the previous time
if ( bNewEndingTimeIsAfterThePreviousTime )
{
// If the starting time + the time to add is before the
// ending time of the day
if ( timeSlotToShift.getStartingTime( ).plus( timeToAdd, ChronoUnit.MINUTES ).isBefore( endingTimeOfTheDay ) )
{
timeSlotToShift.setStartingTime( timeSlotToShift.getStartingTime( ).plus( timeToAdd, ChronoUnit.MINUTES ) );
// if the ending time is after the ending time of
// the day, we set the new ending time to the ending
// time of the day
if ( timeSlotToShift.getEndingTime( ).plus( timeToAdd, ChronoUnit.MINUTES ).isAfter( endingTimeOfTheDay ) )
{
timeSlotToShift.setEndingTime( endingTimeOfTheDay );
}
else
{
timeSlotToShift.setEndingTime( timeSlotToShift.getEndingTime( ).plus( timeToAdd, ChronoUnit.MINUTES ) );
}
updateTimeSlot( timeSlotToShift );
}
else
{
// Delete this slot (the slot can not be after the
// ending time of the day)
deleteTimeSlot( timeSlotToShift );
}
}
else
{
// The new ending time is before the previous ending
// time
timeSlotToShift.setStartingTime( timeSlotToShift.getStartingTime( ).minus( timeToSubstract, ChronoUnit.MINUTES ) );
timeSlotToShift.setEndingTime( timeSlotToShift.getEndingTime( ).minus( timeToSubstract, ChronoUnit.MINUTES ) );
updateTimeSlot( timeSlotToShift );
}
}
if ( !bNewEndingTimeIsAfterThePreviousTime )
{
// If the slots have been shift earlier,
// there is no slot(s) between the last slot created
// and the ending time of the day, need to create it(them)
List<TimeSlot> listTimeSlotToAdd = generateListTimeSlot( timeSlot.getIdWorkingDay( ), endingTimeOfTheDay.minusMinutes( timeToSubstract ),
endingTimeOfTheDay, nDuration, reservationRule.getMaxCapacityPerSlot( ), Boolean.TRUE );
createListTimeSlot( listTimeSlotToAdd );
}
}
/**
* Update a time slot without shifting the next time slots
*
* @param timeSlot
* the time slot modified
* @param workingDay
* the working day
* @param reservationRule
* the reservation rule
* @param nDuration
* the duration of a time slot
*/
private static void updateTimeSlotWithoutShift( TimeSlot timeSlot, WorkingDay workingDay, ReservationRule reservationRule, int nDuration )
{
List<TimeSlot> listTimeSlotToCreate = new ArrayList<>( );
LocalTime maxEndingTime = WorkingDayService.getMaxEndingTimeOfAWorkingDay( workingDay );
// Find all the time slot after the starting time of the new
// time
// slot
List<TimeSlot> listAllTimeSlotsAfterThisTimeSlot = findListTimeSlotAfterThisTimeSlot( timeSlot );
// Need to delete all the time slots impacted (the ones with the
// starting time before the ending time of the new time slot)
List<TimeSlot> listAllTimeSlotsToUpdate = listAllTimeSlotsAfterThisTimeSlot.stream( )
.filter( x -> x.getStartingTime( ).isBefore( timeSlot.getEndingTime( ) ) ).collect( Collectors.toList( ) );
for ( TimeSlot tSlot : listAllTimeSlotsToUpdate )
{
listAllTimeSlotsAfterThisTimeSlot.remove( tSlot );
// If the new ending time is after the previous time
if ( tSlot.getEndingTime( ).isAfter( timeSlot.getEndingTime( ) ) )
{
tSlot.setStartingTime( timeSlot.getEndingTime( ) );
updateTimeSlot( tSlot );
listAllTimeSlotsAfterThisTimeSlot.add( tSlot );
}
else
{
deleteTimeSlot( tSlot );
}
}
// Need to find the next time slot (the one with the closest
// starting time of the ending time of the new time slot)
TimeSlot nextTimeSlot = null;
if ( CollectionUtils.isNotEmpty( listAllTimeSlotsAfterThisTimeSlot ) )
{
nextTimeSlot = listAllTimeSlotsAfterThisTimeSlot.stream( ).min( ( t1, t2 ) -> t1.getStartingTime( ).compareTo( t2.getStartingTime( ) ) )
.orElse( null );
}
if ( nextTimeSlot != null )
{
maxEndingTime = nextTimeSlot.getStartingTime( );
}
// and to regenerate time slots between this two ones, with the
// good
// rules
// for the slot capacity
listTimeSlotToCreate.addAll( generateListTimeSlot( timeSlot.getIdWorkingDay( ), timeSlot.getEndingTime( ), maxEndingTime, nDuration,
reservationRule.getMaxCapacityPerSlot( ), Boolean.TRUE ) );
TimeSlotHome.update( timeSlot );
createListTimeSlot( listTimeSlotToCreate );
}
/**
* Update a time slot
*
* @param timeSlot
* the time slot to update
*/
public static TimeSlot updateTimeSlot( TimeSlot timeSlot )
{
return TimeSlotHome.update( timeSlot );
}
/**
* Create in database the slots given
*
* @param listSlotToCreate
* the list of slots to create in database
*/
public static void createListTimeSlot( List<TimeSlot> listTimeSlotToCreate )
{
if ( CollectionUtils.isNotEmpty( listTimeSlotToCreate ) )
{
for ( TimeSlot timeSlotTemp : listTimeSlotToCreate )
{
TimeSlotHome.create( timeSlotTemp );
}
}
}
/**
* Create in database the slots given
*
* @param listSlotToCreate
* the list of slots to create in database
*/
public static void updateListTimeSlot( List<TimeSlot> listTimeSlotToCUpdate )
{
if ( CollectionUtils.isNotEmpty( listTimeSlotToCUpdate ) )
{
for ( TimeSlot timeSlotTemp : listTimeSlotToCUpdate )
{
TimeSlotHome.update( timeSlotTemp );
}
}
}
/**
* Find the next time slots of a given time slot
*
* @param timeSlot
* the time slot
* @return a list of the next time slots
*/
public static List<TimeSlot> findListTimeSlotAfterThisTimeSlot( TimeSlot timeSlot )
{
return TimeSlotService.findListTimeSlotByWorkingDay( timeSlot.getIdWorkingDay( ) ).stream( )
.filter( x -> x.getStartingTime( ).isAfter( timeSlot.getStartingTime( ) ) ).collect( Collectors.toList( ) );
}
/**
* Delete in database time slots
*
* @param listTimeSlot
* the list of time slots to delete
*/
public static void deleteListTimeSlot( List<TimeSlot> listTimeSlot )
{
for ( TimeSlot timeSlot : listTimeSlot )
{
deleteTimeSlot( timeSlot );
}
}
/**
* Delete in database time slot
*
* @param timeSlot
* the time slot to delete
*/
public static void deleteTimeSlot( TimeSlot timeSlot )
{
TimeSlotHome.delete( timeSlot.getIdTimeSlot( ) );
}
/**
* Get the time slots of a list of working days
*
* @param listWorkingDay
* the list of the working days
* @param dateInWeek
* the date in the week
* @return the list of the time slots
*/
public static List<TimeSlot> getListTimeSlotOfAListOfWorkingDay( List<WorkingDay> listWorkingDay, LocalDate dateInWeek )
{
List<TimeSlot> listTimeSlot = new ArrayList<>( );
for ( WorkingDay workingDay : listWorkingDay )
{
for ( TimeSlot timeSlot : workingDay.getListTimeSlot( ) )
{
// Need to add the current date to the hour
timeSlot.setStartingDateTime( dateInWeek.with( DayOfWeek.of( workingDay.getDayOfWeek( ) ) ).atTime( timeSlot.getStartingTime( ) ) );
timeSlot.setEndingDateTime( dateInWeek.with( DayOfWeek.of( workingDay.getDayOfWeek( ) ) ).atTime( timeSlot.getEndingTime( ) ) );
listTimeSlot.add( timeSlot );
}
}
return listTimeSlot;
}
/**
* Return an ordered and filtered list of time slots after a given time
*
* @param listTimeSlot
* the list of time slot to sort and filter
* @param time
* the time
* @return the list ordered and filtered
*/
public static List<TimeSlot> getNextTimeSlotsInAListOfTimeSlotAfterALocalTime( List<TimeSlot> listTimeSlot, LocalTime time )
{
return listTimeSlot.stream( ).filter( x -> x.getStartingTime( ).isAfter( time ) || x.getStartingTime( ).equals( time ) )
.collect( Collectors.toList( ) );
}
/**
* Returns the time slot in a list of time slot with the given starting time
*
* @param listTimeSlot
* the list of time slots
* @param timeToSearch
* the starting time to search
* @return the time slot found
*/
public static TimeSlot getTimeSlotInListOfTimeSlotWithStartingTime( List<TimeSlot> listTimeSlot, LocalTime timeToSearch )
{
return listTimeSlot.stream( ).filter( x -> timeToSearch.equals( x.getStartingTime( ) ) ).findFirst( ).orElse( null );
}
}