View Javadoc
1   /*
2    * Copyright (c) 2002-2018, Mairie de Paris
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    *
9    *  1. Redistributions of source code must retain the above copyright notice
10   *     and the following disclaimer.
11   *
12   *  2. Redistributions in binary form must reproduce the above copyright notice
13   *     and the following disclaimer in the documentation and/or other materials
14   *     provided with the distribution.
15   *
16   *  3. Neither the name of 'Mairie de Paris' nor 'Lutece' nor the names of its
17   *     contributors may be used to endorse or promote products derived from
18   *     this software without specific prior written permission.
19   *
20   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21   * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22   * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
24   * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26   * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27   * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28   * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30   * POSSIBILITY OF SUCH DAMAGE.
31   *
32   * License 1.0
33   */
34  package fr.paris.lutece.plugins.appointment.service;
35  
36  import java.time.DayOfWeek;
37  import java.time.LocalDate;
38  import java.time.LocalDateTime;
39  import java.time.LocalTime;
40  import java.time.temporal.ChronoUnit;
41  import java.util.ArrayList;
42  import java.util.Collections;
43  import java.util.List;
44  import java.util.stream.Collectors;
45  
46  import org.apache.commons.collections.CollectionUtils;
47  
48  import fr.paris.lutece.plugins.appointment.business.planning.TimeSlot;
49  import fr.paris.lutece.plugins.appointment.business.planning.TimeSlotHome;
50  import fr.paris.lutece.plugins.appointment.business.planning.WeekDefinition;
51  import fr.paris.lutece.plugins.appointment.business.planning.WorkingDay;
52  import fr.paris.lutece.plugins.appointment.business.rule.ReservationRule;
53  import fr.paris.lutece.plugins.appointment.service.listeners.WeekDefinitionManagerListener;
54  
55  /**
56   * Service class for the time slot
57   * 
58   * @author Laurent Payen
59   *
60   */
61  public final class TimeSlotService
62  {
63  
64      /**
65       * Private constructor - this class does not need to be instantiated
66       */
67      private TimeSlotService( )
68      {
69      }
70  
71      /**
72       * Build a list of timeSlot Object from a starting time to an endingTime
73       * 
74       * @param nIdWorkingDay
75       *            the workingDay Id
76       * @param startingTime
77       *            the starting time
78       * @param endingTime
79       *            the ending time
80       * @param nDuration
81       *            the duration of the slot
82       * @param nMaxCapacity
83       *            the maximum capacity of the slot
84       * @return the list of TimeSlot built
85       */
86      public static List<TimeSlot> generateListTimeSlot( int nIdWorkingDay, LocalTime startingTime, LocalTime endingTime, int nDuration, int nMaxCapacity,
87              boolean forceTimeSlotCreationWithMinTime )
88      {
89          List<TimeSlot> listTimeSlot = new ArrayList<>( );
90          LocalDateTime tempStartingDateTime = LocalDate.now().atTime(startingTime);
91          LocalDateTime tempEndingDateTime = LocalDate.now().atTime(startingTime.plusMinutes( nDuration ));
92          LocalDateTime endingDateTime = LocalDate.now().atTime(endingTime);
93          while ( !tempEndingDateTime.isAfter( endingDateTime ) )
94          {
95              listTimeSlot.add( generateTimeSlot( nIdWorkingDay, tempStartingDateTime.toLocalTime(), tempEndingDateTime.toLocalTime(), Boolean.TRUE.booleanValue( ), nMaxCapacity ) );
96              tempStartingDateTime = tempEndingDateTime;
97              tempEndingDateTime = tempEndingDateTime.plusMinutes( nDuration );
98          }
99          if ( forceTimeSlotCreationWithMinTime )
100         {
101             tempStartingDateTime = tempEndingDateTime.minusMinutes( nDuration );
102             if ( tempStartingDateTime.isBefore( endingDateTime ) )
103             {
104                 listTimeSlot.add( generateTimeSlot( nIdWorkingDay, tempStartingDateTime.toLocalTime(), endingTime, Boolean.FALSE, nMaxCapacity ) );
105             }
106         }
107         return listTimeSlot;
108     }
109 
110     /**
111      * Save a time slot
112      * 
113      * @param timeSlot
114      *            the time slot to save
115      * @return the time slot saved
116      */
117     public static TimeSlot saveTimeSlot( TimeSlot timeSlot )
118     {
119         TimeSlot timeSlotSaved = null;
120         if ( timeSlot.getIdTimeSlot( ) == 0 )
121         {
122             timeSlotSaved = TimeSlotService.createTimeSlot( timeSlot );
123         }
124         else
125         {
126             timeSlotSaved = TimeSlotService.updateTimeSlot( timeSlot );
127         }
128         return timeSlotSaved;
129     }
130 
131     /**
132      * Create a time slot in db
133      * 
134      * @param timeSlot
135      *            the time slot to create
136      * @return the time slot created
137      */
138     public static TimeSlot createTimeSlot( TimeSlot timeSlot )
139     {
140         return TimeSlotHome.create( timeSlot );
141     }
142 
143     /**
144      * Build a timeSlot with all its values
145      * 
146      * @param nIdWorkingDay
147      *            the workingDay Id
148      * @param startingTime
149      *            the starting time
150      * @param endingTime
151      *            the ending time
152      * @param isOpen
153      *            true if the slot is open
154      * @param nMaxCapacity
155      *            the maximum capacity of the slot
156      * @return the timeSLot built
157      */
158     public static TimeSlot generateTimeSlot( int nIdWorkingDay, LocalTime startingTime, LocalTime endingTime, boolean isOpen, int nMaxCapacity )
159     {
160         TimeSlot timeSlot = new TimeSlot( );
161         timeSlot.setIdWorkingDay( nIdWorkingDay );
162         timeSlot.setIsOpen( isOpen );
163         timeSlot.setStartingTime( startingTime );
164         timeSlot.setEndingTime( endingTime );
165         timeSlot.setMaxCapacity( nMaxCapacity );
166         return timeSlot;
167     }
168 
169     /**
170      * Find the time slots of a working day
171      * 
172      * @param nIdWorkingDay
173      *            the working day Id
174      * @return the list of the timeSlot of this workingDay
175      */
176     public static List<TimeSlot> findListTimeSlotByWorkingDay( int nIdWorkingDay )
177     {
178         return TimeSlotHome.findByIdWorkingDay( nIdWorkingDay );
179     }
180 
181     /**
182      * Find a timeSlot with its primary key
183      * 
184      * @param nIdTimeSlot
185      *            the timeSlot Id
186      * @return the timeSlot found
187      */
188     public static TimeSlot findTimeSlotById( int nIdTimeSlot )
189     {
190         return TimeSlotHome.findByPrimaryKey( nIdTimeSlot );
191     }
192 
193     /**
194      * Update a timeSLot in database
195      * 
196      * @param timeSlot
197      *            the timeSlot to update
198      * @param bEndingTimeHasChanged
199      *            if the ending time has changed, need to regenerate and update all the next time slots
200      * @param previousEndingTime
201      *            the previous ending time of the current time slot
202      * @param bShifSlot
203      *            true if the user has decided to shift the next slots
204      */
205     public static void updateTimeSlot( TimeSlot timeSlot, boolean bEndingTimeHasChanged, LocalTime previousEndingTime, boolean bShifSlot )
206     {
207         WorkingDay workingDay = WorkingDayService.findWorkingDayById( timeSlot.getIdWorkingDay( ) );
208         WeekDefinition weekDefinition = WeekDefinitionService.findWeekDefinitionLightById( workingDay.getIdWeekDefinition( ) );
209         ReservationRule reservationRule = ReservationRuleService.findReservationRuleByIdFormAndClosestToDateOfApply( weekDefinition.getIdForm( ),
210                 weekDefinition.getDateOfApply( ) );
211         int nDuration = WorkingDayService.getMinDurationTimeSlotOfAWorkingDay( workingDay );
212         if ( bEndingTimeHasChanged )
213         {
214             if ( !bShifSlot )
215             {
216                 updateTimeSlotWithoutShift( timeSlot, workingDay, reservationRule, nDuration );
217             }
218             else
219             {
220                 updateTimeSlotWithShift( timeSlot, workingDay, reservationRule, nDuration, previousEndingTime );
221             }
222 
223         }
224         else
225         {
226             saveTimeSlot( timeSlot );
227         }
228 
229         WeekDefinitionManagerListener.notifyListenersWeekDefinitionChange( workingDay.getIdWeekDefinition( ) );
230     }
231 
232     /**
233      * Update a time slot with shifting the next
234      * 
235      * @param timeSlot
236      *            the time slot modified
237      * @param workingDay
238      *            the working day
239      * @param reservationRule
240      *            the reservation rule
241      * @param nDuration
242      *            the duration of a time slot
243      * @param previousEndingTime
244      *            the previous ending time
245      */
246     private static void updateTimeSlotWithShift( TimeSlot timeSlot, WorkingDay workingDay, ReservationRule reservationRule, int nDuration,
247             LocalTime previousEndingTime )
248     {
249         // We want to shift all the next time slots
250         // Get all the time slots of the day
251         List<TimeSlot> listOfAllTimeSlotsOfThisWorkingDay = findListTimeSlotByWorkingDay( workingDay.getIdWorkingDay( ) );
252         // Remove the current time slot and all the time slots before it
253         listOfAllTimeSlotsOfThisWorkingDay = listOfAllTimeSlotsOfThisWorkingDay.stream( )
254                 .filter( timeSlotToKeep -> timeSlotToKeep.getStartingTime( ).isAfter( timeSlot.getStartingTime( ) ) ).collect( Collectors.toList( ) );
255         // Need to delete all the time slots until the new end of this
256         // time slot
257         List<TimeSlot> listTimeSlotToDelete = listOfAllTimeSlotsOfThisWorkingDay
258                 .stream( )
259                 .filter(
260                         timeSlotToDelete -> timeSlotToDelete.getStartingTime( ).isAfter( timeSlot.getStartingTime( ) )
261                                 && !timeSlotToDelete.getEndingTime( ).isAfter( timeSlot.getEndingTime( ) ) ).collect( Collectors.toList( ) );
262         deleteListTimeSlot( listTimeSlotToDelete );
263         listOfAllTimeSlotsOfThisWorkingDay.removeAll( listTimeSlotToDelete );
264         // Need to order the list of time slot to shift according to the
265         // shift
266         // if the new ending time is before the previous ending time,
267         // the list has to be ordered in chronological order ascending
268         // and the first time slot to shift is the closest to the
269         // current
270         // time slot
271         // (because we have an integrity constraint for the time slot,
272         // it
273         // can't have the same starting or ending time as another time
274         // slot
275         List<TimeSlot> listTimeSlotToShift = new ArrayList<>( );
276         listTimeSlotToShift.addAll( listOfAllTimeSlotsOfThisWorkingDay );
277         listTimeSlotToShift = listTimeSlotToShift.stream( )
278                 .sorted( ( timeSlot1, timeSlot2 ) -> timeSlot1.getStartingTime( ).compareTo( timeSlot2.getStartingTime( ) ) ).collect( Collectors.toList( ) );
279         boolean bNewEndingTimeIsAfterThePreviousTime = false;
280         // Need to know the ending time of the day
281         LocalTime endingTimeOfTheDay = WorkingDayService.getMaxEndingTimeOfAWorkingDay( workingDay );
282 
283         long timeToAdd = 0;
284         long timeToSubstract = 0;
285         if ( previousEndingTime.isBefore( timeSlot.getEndingTime( ) ) )
286         {
287             bNewEndingTimeIsAfterThePreviousTime = true;
288             // Need to find the next available time slot, to know how to
289             // add to the starting time of the next time slot to match
290             // with
291             // the new end of the current time slot
292             if ( CollectionUtils.isNotEmpty( listTimeSlotToShift ) )
293             {
294                 TimeSlot nextTimeSlot = listTimeSlotToShift.stream( ).min( ( t1, t2 ) -> t1.getStartingTime( ).compareTo( t2.getStartingTime( ) ) ).get( );
295                 if ( timeSlot.getEndingTime( ).isAfter( nextTimeSlot.getStartingTime( ) ) )
296                 {
297                     timeToAdd = nextTimeSlot.getStartingTime( ).until( timeSlot.getEndingTime( ), ChronoUnit.MINUTES );
298                 }
299                 else
300                 {
301                     timeToAdd = timeSlot.getEndingTime( ).until( nextTimeSlot.getStartingTime( ), ChronoUnit.MINUTES );
302                 }
303                 Collections.reverse( listTimeSlotToShift );
304             }
305             else
306             {
307                 timeToAdd = previousEndingTime.until( timeSlot.getEndingTime( ), ChronoUnit.MINUTES );
308             }
309 
310         }
311         else
312         {
313             timeToSubstract = timeSlot.getEndingTime( ).until( previousEndingTime, ChronoUnit.MINUTES );
314         }
315         updateTimeSlot( timeSlot );
316         // Need to set the new starting and ending time of all the time
317         // slots
318         // to shift and update them
319         for ( TimeSlot timeSlotToShift : listTimeSlotToShift )
320         {
321             // If the new ending time is after the previous time
322             if ( bNewEndingTimeIsAfterThePreviousTime )
323             {
324                 // If the starting time + the time to add is before the
325                 // ending time of the day
326                 if ( timeSlotToShift.getStartingTime( ).plus( timeToAdd, ChronoUnit.MINUTES ).isBefore( endingTimeOfTheDay ) )
327                 {
328                     timeSlotToShift.setStartingTime( timeSlotToShift.getStartingTime( ).plus( timeToAdd, ChronoUnit.MINUTES ) );
329                     // if the ending time is after the ending time of
330                     // the day, we set the new ending time to the ending
331                     // time of the day
332                     if ( timeSlotToShift.getEndingTime( ).plus( timeToAdd, ChronoUnit.MINUTES ).isAfter( endingTimeOfTheDay ) )
333                     {
334                         timeSlotToShift.setEndingTime( endingTimeOfTheDay );
335                     }
336                     else
337                     {
338                         timeSlotToShift.setEndingTime( timeSlotToShift.getEndingTime( ).plus( timeToAdd, ChronoUnit.MINUTES ) );
339                     }
340                     updateTimeSlot( timeSlotToShift );
341                 }
342                 else
343                 {
344                     // Delete this slot (the slot can not be after the
345                     // ending time of the day)
346                     deleteTimeSlot( timeSlotToShift );
347                 }
348             }
349             else
350             {
351                 // The new ending time is before the previous ending
352                 // time
353                 timeSlotToShift.setStartingTime( timeSlotToShift.getStartingTime( ).minus( timeToSubstract, ChronoUnit.MINUTES ) );
354                 timeSlotToShift.setEndingTime( timeSlotToShift.getEndingTime( ).minus( timeToSubstract, ChronoUnit.MINUTES ) );
355                 updateTimeSlot( timeSlotToShift );
356             }
357         }
358 
359         if ( !bNewEndingTimeIsAfterThePreviousTime )
360         {
361             // If the slots have been shift earlier,
362             // there is no slot(s) between the last slot created
363             // and the ending time of the day, need to create it(them)
364             List<TimeSlot> listTimeSlotToAdd = generateListTimeSlot( timeSlot.getIdWorkingDay( ), endingTimeOfTheDay.minusMinutes( timeToSubstract ),
365                     endingTimeOfTheDay, nDuration, reservationRule.getMaxCapacityPerSlot( ), Boolean.TRUE );
366             createListTimeSlot( listTimeSlotToAdd );
367         }
368 
369     }
370 
371     /**
372      * Update a time slot without shifting the next time slots
373      * 
374      * @param timeSlot
375      *            the time slot modified
376      * @param workingDay
377      *            the working day
378      * @param reservationRule
379      *            the reservation rule
380      * @param nDuration
381      *            the duration of a time slot
382      */
383     private static void updateTimeSlotWithoutShift( TimeSlot timeSlot, WorkingDay workingDay, ReservationRule reservationRule, int nDuration )
384     {
385         List<TimeSlot> listTimeSlotToCreate = new ArrayList<>( );
386         LocalTime maxEndingTime = WorkingDayService.getMaxEndingTimeOfAWorkingDay( workingDay );
387         // Find all the time slot after the starting time of the new
388         // time
389         // slot
390         List<TimeSlot> listAllTimeSlotsAfterThisTimeSlot = findListTimeSlotAfterThisTimeSlot( timeSlot );
391         // Need to delete all the time slots impacted (the ones with the
392         // starting time before the ending time of the new time slot)
393         List<TimeSlot> listAllTimeSlotsToDelete = listAllTimeSlotsAfterThisTimeSlot.stream( )
394                 .filter( x -> x.getStartingTime( ).isBefore( timeSlot.getEndingTime( ) ) ).collect( Collectors.toList( ) );
395         deleteListTimeSlot( listAllTimeSlotsToDelete );
396         // Need to find the next time slot (the one with the closest
397         // starting time of the ending time of the new time slot)
398         listAllTimeSlotsAfterThisTimeSlot.removeAll( listAllTimeSlotsToDelete );
399         TimeSlot nextTimeSlot = null;
400         if ( CollectionUtils.isNotEmpty( listAllTimeSlotsAfterThisTimeSlot ) )
401         {
402             nextTimeSlot = listAllTimeSlotsAfterThisTimeSlot.stream( ).min( ( t1, t2 ) -> t1.getStartingTime( ).compareTo( t2.getStartingTime( ) ) ).get( );
403         }
404         if ( nextTimeSlot != null )
405         {
406             maxEndingTime = nextTimeSlot.getStartingTime( );
407         }
408         // and to regenerate time slots between this two ones, with the
409         // good
410         // rules
411         // for the slot capacity
412         listTimeSlotToCreate.addAll( generateListTimeSlot( timeSlot.getIdWorkingDay( ), timeSlot.getEndingTime( ), maxEndingTime, nDuration,
413                 reservationRule.getMaxCapacityPerSlot( ), Boolean.TRUE ) );
414         TimeSlotHome.update( timeSlot );
415         createListTimeSlot( listTimeSlotToCreate );
416 
417     }
418 
419     /**
420      * Update a time slot
421      * 
422      * @param timeSlot
423      *            the time slot to update
424      */
425     public static TimeSlot updateTimeSlot( TimeSlot timeSlot )
426     {
427         return TimeSlotHome.update( timeSlot );
428     }
429 
430     /**
431      * Create in database the slots given
432      * 
433      * @param listSlotToCreate
434      *            the list of slots to create in database
435      */
436     public static void createListTimeSlot( List<TimeSlot> listTimeSlotToCreate )
437     {
438         if ( CollectionUtils.isNotEmpty( listTimeSlotToCreate ) )
439         {
440             for ( TimeSlot timeSlotTemp : listTimeSlotToCreate )
441             {
442                 TimeSlotHome.create( timeSlotTemp );
443             }
444         }
445     }
446 
447     /**
448      * Find the next time slots of a given time slot
449      * 
450      * @param timeSlot
451      *            the time slot
452      * @return a list of the next time slots
453      */
454     public static List<TimeSlot> findListTimeSlotAfterThisTimeSlot( TimeSlot timeSlot )
455     {
456         return TimeSlotService.findListTimeSlotByWorkingDay( timeSlot.getIdWorkingDay( ) ).stream( )
457                 .filter( x -> x.getStartingTime( ).isAfter( timeSlot.getStartingTime( ) ) ).collect( Collectors.toList( ) );
458     }
459 
460     /**
461      * Delete in database time slots
462      * 
463      * @param listTimeSlot
464      *            the list of time slots to delete
465      */
466     public static void deleteListTimeSlot( List<TimeSlot> listTimeSlot )
467     {
468         for ( TimeSlot timeSlot : listTimeSlot )
469         {
470             deleteTimeSlot( timeSlot );
471         }
472     }
473 
474     /**
475      * Delete in database time slot
476      * 
477      * @param timeSlot
478      *            the time slot to delete
479      */
480     public static void deleteTimeSlot( TimeSlot timeSlot )
481     {
482         TimeSlotHome.delete( timeSlot.getIdTimeSlot( ) );
483     }
484 
485     /**
486      * Get the time slots of a list of working days
487      * 
488      * @param listWorkingDay
489      *            the list of the working days
490      * @param dateInWeek
491      *            the date in the week
492      * @return the list of the time slots
493      */
494     public static List<TimeSlot> getListTimeSlotOfAListOfWorkingDay( List<WorkingDay> listWorkingDay, LocalDate dateInWeek )
495     {
496         List<TimeSlot> listTimeSlot = new ArrayList<>( );
497         for ( WorkingDay workingDay : listWorkingDay )
498         {
499             for ( TimeSlot timeSlot : workingDay.getListTimeSlot( ) )
500             {
501                 // Need to add the current date to the hour
502                 timeSlot.setStartingDateTime( dateInWeek.with( DayOfWeek.of( workingDay.getDayOfWeek( ) ) ).atTime( timeSlot.getStartingTime( ) ) );
503                 timeSlot.setEndingDateTime( dateInWeek.with( DayOfWeek.of( workingDay.getDayOfWeek( ) ) ).atTime( timeSlot.getEndingTime( ) ) );
504                 listTimeSlot.add( timeSlot );
505             }
506         }
507         return listTimeSlot;
508     }
509 
510     /**
511      * Return an ordered and filtered list of time slots after a given time
512      * 
513      * @param listTimeSlot
514      *            the list of time slot to sort and filter
515      * @param time
516      *            the time
517      * @return the list ordered and filtered
518      */
519     public static List<TimeSlot> getNextTimeSlotsInAListOfTimeSlotAfterALocalTime( List<TimeSlot> listTimeSlot, LocalTime time )
520     {
521         return listTimeSlot.stream( ).filter( x -> x.getStartingTime( ).isAfter( time ) || x.getStartingTime( ).equals( time ) ).collect( Collectors.toList( ) );
522     }
523 
524     /**
525      * Returns the time slot in a list of time slot with the given starting time
526      * 
527      * @param listTimeSlot
528      *            the list of time slots
529      * @param timeToSearch
530      *            the starting time to search
531      * @return the time slot found
532      */
533     public static TimeSlot getTimeSlotInListOfTimeSlotWithStartingTime( List<TimeSlot> listTimeSlot, LocalTime timeToSearch )
534     {
535         return listTimeSlot.stream( ).filter( x -> timeToSearch.equals( x.getStartingTime( ) ) ).findFirst( ).orElse( null );
536     }
537 }