package web.rechelper;
import web.reception.*;
import dao.DAO;
import domain.shedule.Prorumble;
import domain.shedule.SheduleException;
import domain.shedule.SheduleHoliday;
import domain.shedule.SheduleIndividualWork;
import domain.shedule.SheduleReception;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.log4j.Logger;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Property;
import utils.Converter;
import utils.Day;
import utils.WeekIndex;
/*
* приоритет расписаний
* - шаблон расписания врача (дата)
* - праздничные дни год/месяц
* - исключительные дни год/месяц/год
* - расписание врача (дата)
* - прогулы/отгулы/отпуск врача год/месяц/год
*
* Контроллер отображает недельное расписание врача. Входные данные
* - пациент
* - тип приема (работы врача)
* - ЛПУ
* - Врач
* - Неделя
*
* Логика выборки
* I. Вычисление расписания
* 0. Получаем текущую неделю
* 1. Получаем список прогулов врача за текущую неделю
* 2. Для каждого дня недели, если в этот день есть прогул отмечаем день как закрытый, иначе:
* 3. Получаем расписание врача за текущую неделю (только работы указанного типа),
* если оно еще не получено
* 4. Для выбранного дня недели, если есть работы, переходим к пункту II,
* если работ нет, значит врач работает по шаблону
* 5. Получаем исключения работы поликлинники за текущую неделю.
* если исключения есть, и они указывают что день нерабочий, отмечаем день как закрытый
* 6. Если исключений нет, получаем праздники за указанную неделю
* если в этот день праздник, отмечаем день как закрытый
* 7. Врач работает по шаблону. Если еще не получены, получаем:
* - весь шаблон расписания для данного врача
* - чередование недель в расписании врача
* - индекс текущей недели
* 10. Используем работы из шаблона врача
*
* II. Подготовка работ к отображению в вебе
* 1. Получить список записей на прием к данному врачу на всю неделю
* 2. Для каждой работы создаеть список "талонов", временных промежутков
* длительностью равной дефолтовой длительности приема по данной работе.
* 3. Отметить талоны, которые пересекаются по времени с существующими записями
* на прием как занятые
* 4. собрать модель [dayofweek][Ticket]
*/
public class ReceptionHelper {
protected static final Logger log = Logger.getLogger(IndexController.class);
private final DAO dao;
private final ReceptionDTO dto;
private final Date begin;
private final Date end;
private HashMap<Day, List<SheduleIndividualWork>> exWorks;
private HashMap<Integer, List<SheduleIndividualWork>> plWorks;
private Set<Day> prorumbles;
private HashMap<Day, List<SheduleReception>> receptions;
private HashMap<Day, List<SheduleException>> exceptions;
private HashMap<Integer, List<SheduleHoliday>> holidays;
public ReceptionHelper(DAO dao, ReceptionDTO dto, Date begin, Date end) {
this.dao = dao;
this.dto = dto;
this.begin = begin;
this.end = end;
}
public List<WeekDay> getModel() throws Exception {
List<WeekDay> weekModel = new LinkedList<WeekDay>();
Calendar today = GregorianCalendar.getInstance();
today.set(Calendar.HOUR_OF_DAY, 0);
today.set(Calendar.MINUTE, 0);
today.set(Calendar.MILLISECOND, 0);
//I. Вычисление расписания
Calendar dayCal = GregorianCalendar.getInstance();
dayCal.setTime(begin);
// 2. Для каждого дня недели,
while(dayCal.getTimeInMillis() <= end.getTime()) {
WeekDay weekDay = new WeekDay(dayCal);
weekModel.add(weekDay);
dayCal.add(Calendar.DAY_OF_MONTH, 1);
// log.debug("Analyze day " + weekDay);
if(dayCal.before(today)) {
// log.debug("Past detected for day " + weekDay);
continue;
}
//если в этот день есть прогул отмечаем день как закрытый, иначе:
if(isProrumbled(weekDay.getDay())) {
// log.debug("Prorumble detected for day " + weekDay);
continue;
}
//Получаем расписание врача за текущую неделю (только работы указанного типа)
//если нет работ - значит врач работает по шаблону, идем дальше
List<SheduleIndividualWork> dayExceptionalWorks = getExceptionalWorks(weekDay.getDay());
if(dayExceptionalWorks != null) {
// log.debug("Exceptional works detected, use it for day " + weekDay);
//если есть работы используем их
weekDay.setTickets(getDayTickets(weekDay.getDay(), dayExceptionalWorks));
continue;
}
//5. Получаем исключения работы поликлинники за текущую неделю.
//если исключения есть, и они указывают что день нерабочий, отмечаем день как закрытый
List<SheduleException> clinicExceptions = getClinicExceptions(weekDay.getDay());
if(isWeekend(clinicExceptions)) {
// log.debug("Weekend exceptional clinic day detected for day " + weekDay);
continue;
}
//6. Если исключений нет, получаем праздники за указанную неделю
//если в этот день праздник, отмечаем день как закрытый
if(clinicExceptions == null) {
List<SheduleHoliday> clinicHolidays = getClinicHolidays(weekDay.getDay());
if(clinicHolidays != null) {
// log.debug("Holiday clinic day detected for day " + weekDay);
continue;
}
}
//7. Врач работает по шаблону. получаем шаблон расписания для данного врача
List<SheduleIndividualWork> dayPlanWorks = getPlanWorks(weekDay.getDay());
if(dayPlanWorks != null) {
//10. Используем работы из шаблона врача в качестве индекса текущей недели
// log.debug("Planned works detected for day " + weekDay);
weekDay.setTickets(getDayTickets(weekDay.getDay(), dayPlanWorks));
}
}
// log.debug("Analyze of day complete.");
//II. Подготовка работ к отображению в вебе
//Отметить талоны, которые пересекаются по времени с существующими записями как занятые
Iterator<WeekDay> it = weekModel.iterator();
while(it.hasNext()) {
WeekDay wd = it.next();
if(wd.getTickets() == null) {
//если в этот день нет талонов, пропускаем
continue;
}
updateStatus(wd, getReceptions(wd.getDay()));
}
return weekModel;
}
/**
* Возвращает список записей на прием, осуществляемых в диапазоне дат
* @param timeBegin
* @param timeEnd
* @return
*/
private List<SheduleReception> getReceptions(Day day) {
if(receptions == null) {
receptions = getReceptions();
}
return receptions.get(day);
}
/**
* Возвращает список записей на прием в текущую неделю к указанному врачу
* @return
*/
private HashMap<Day, List<SheduleReception>> getReceptions() {
HashMap<Day, List<SheduleReception>> res = new HashMap<Day, List<SheduleReception>>();
DetachedCriteria criteria = DetachedCriteria.forClass(SheduleReception.class)
.add(Property.forName("collaborator").eq(dto.getCollaborator()))
.add(Property.forName("begin").ge(begin))
.add(Property.forName("begin").le(end));
List<SheduleReception> list = dao.getList(criteria, null, null);
for(SheduleReception sr: list) {
Day day = new Day(sr.getBegin());
List<SheduleReception> dayList = res.get(day);
if(dayList == null) {
dayList = new LinkedList<SheduleReception>();
res.put(day, dayList);
}
dayList.add(sr);
}
return res;
}
/**
* Возвращает булевое выражение, есть ли пропуски в указанном диапазоне дат
* @param prorumbles
* @param timeBegin
* @param timeEnd
* @return
*/
private boolean isProrumbled(Day day) {
return getProrumbles().contains(day);
}
private Set<Day> getProrumbles() {
if(prorumbles == null) {
prorumbles = new HashSet<Day>();
DetachedCriteria criteria = DetachedCriteria.forClass(Prorumble.class)
.add(Property.forName("collaborator").eq(dto.getCollaborator()))
.add(Property.forName("day").ge(begin.getTime()))
.add(Property.forName("day").le(end.getTime()));
for(Object o: dao.getList(criteria, null, null)) {
Prorumble pro = (Prorumble) o;
Day day = new Day(pro.getDay().getTime());
prorumbles.add(day);
}
}
return prorumbles;
}
/**
* исключения работы поликлинники в указанном дне текущей недели
* @param dayBegin
* @param dayEnd
* @return
*/
private List<SheduleException> getClinicExceptions(Day day) {
if(exceptions == null) {
exceptions = getClinicExceptions();
}
return exceptions.get(day);
}
private HashMap<Day,List<SheduleException>> getClinicExceptions() {
HashMap<Day, List<SheduleException>> res = new HashMap<Day, List<SheduleException>>();
DetachedCriteria criteria = DetachedCriteria.forClass(SheduleException.class)
.addOrder(Order.asc("day"))
.add(Property.forName("lpu").ge(dto.getLpu()))
.add(Property.forName("day").ge(begin))
.add(Property.forName("day").le(end));
for(Object o: dao.getList(criteria, null, null)) {
SheduleException se = (SheduleException) o;
Day day = new Day(se.getDay());
List<SheduleException> exlist = res.get(day);
if(exlist == null) {
exlist = new LinkedList<SheduleException>();
res.put(day, exlist);
}
exlist.add(se);
}
return res;
}
/**
* плановые праздники в указанном диапазоне дат
* @param dayBegin
* @param dayEnd
* @return
*/
private List<SheduleHoliday> getClinicHolidays(Day day) {
if(holidays == null) {
holidays = getClinicHolidays();
}
return holidays.get((day.getMonth()+1)*100 + day.getDay());
}
private HashMap<Integer, List<SheduleHoliday>> getClinicHolidays() {
HashMap<Integer, List<SheduleHoliday>> res = new HashMap<Integer, List<SheduleHoliday>>();
List<SheduleHoliday> list = dao.getList(SheduleHoliday.class);
for (SheduleHoliday se : list) {
int hash = se.getMonth() * 100 + se.getDay();
List<SheduleHoliday> holList = res.get(hash);
if (holList == null) {
holList = new LinkedList<SheduleHoliday>();
res.put(hash, holList);
}
holList.add(se);
}
return res;
}
/**
* возвращает отсортированные по времени начала работы
* @param dow
* @return
*/
private List<SheduleIndividualWork> getPlanWorks(Day day) {
if(plWorks == null) {
plWorks = getPlanWorks();
}
WeekIndex weekIndex = new WeekIndex(day);
int weekPeriod = dto.getCollaborator().getWeekPeriod();
int planWeekIndex = weekIndex.getIndex() % (weekPeriod + 1);
int dayOfWeek = day.getDayOfWeek().getEuropean();
int hash = planWeekIndex*7 + dayOfWeek + 1;
return plWorks.get(hash);
}
private HashMap<Integer, List<SheduleIndividualWork>> getPlanWorks() {
HashMap<Integer, List<SheduleIndividualWork>> works =
new HashMap<Integer, List<SheduleIndividualWork>>();
DetachedCriteria criteria = DetachedCriteria.forClass(SheduleIndividualWork.class)
.addOrder(Order.asc("timeBegin"))
.add(Property.forName("collaborator").eq(dto.getCollaborator()));
for(Object o: dao.getList(criteria, null, null)) {
SheduleIndividualWork siw = (SheduleIndividualWork) o;
Calendar cal = GregorianCalendar.getInstance();
cal.setTime(siw.getTimeBegin());
int day = cal.get(Calendar.DAY_OF_MONTH);
List<SheduleIndividualWork> dayList = works.get(day);
if(dayList == null) {
dayList = new LinkedList<SheduleIndividualWork>();
works.put(day, dayList);
}
dayList.add(siw);
}
return works;
}
private List<SheduleIndividualWork> getExceptionalWorks(Day day) {
return getExceptionalWorks().get(day);
}
private HashMap<Day, List<SheduleIndividualWork>> getExceptionalWorks() {
if(exWorks == null) {
exWorks = new HashMap<Day, List<SheduleIndividualWork>>();
DetachedCriteria criteria = DetachedCriteria.forClass(SheduleIndividualWork.class)
.add(Property.forName("collaborator").eq(dto.getCollaborator()))
.add(Property.forName("timeBegin").ge(begin))
.add(Property.forName("timeBegin").le(end));
for(Object o: dao.getList(criteria, null, null)) {
SheduleIndividualWork siw = (SheduleIndividualWork) o;
Day day = new Day(siw.getTimeBegin());
List<SheduleIndividualWork> dayList = exWorks.get(day);
if(dayList == null) {
dayList = new LinkedList<SheduleIndividualWork>();
exWorks.put(day, dayList);
}
dayList.add(siw);
}
}
return exWorks;
}
/**
* Возвращает, являются ли один из ВСЕ элементы указанного списка - НЕ рабочим днем
* @param clinicExceptions
* @return
*/
private static boolean isWeekend(List<SheduleException> clinicExceptions) {
if(clinicExceptions == null) {
return false;
}
for(SheduleException se: clinicExceptions) {
if(se.getWorking()) {
return false;
}
}
return true;
}
private void optimizeWorks(final Day day, List<SheduleIndividualWork> exWorks) {
//makeTime чтоб потом не мучаться
for (SheduleIndividualWork s : exWorks) {
s.setTimeBegin(makeTime(day, s.getTimeBegin()).getTime());
s.setTimeEnd(makeTime(day, s.getTimeEnd()).getTime());
}
//Сортировка по началу работы
Collections.sort(exWorks, new Comparator<SheduleIndividualWork>() {
@Override
public int compare(SheduleIndividualWork o1, SheduleIndividualWork o2) {
//Calendar c1 = makeTime(day, o1.getTimeBegin());
//Calendar c2 = makeTime(day, o2.getTimeBegin());
return o1.getTimeBegin().compareTo(o2.getTimeBegin());
}
});
//цикл до предпоследнего элемента - последний объединять не надо (вылетит outofbounds)
for (int i = 0; i < exWorks.size() - 1; i++) {
SheduleIndividualWork current = exWorks.get(i);
if(current == null) {
continue;
}
SheduleIndividualWork next = null;
Integer nextIndex = null;
for (int j = i+1; j < exWorks.size(); j++) {
if (exWorks.get(j) != null) {
next = exWorks.get(j);
nextIndex = j;
break;
}
}
if (next != null) {
Date currentEnd = current.getTimeEnd();
Date nextBegin = next.getTimeBegin();
if (!nextBegin.after(currentEnd)) {
//пересекаются либо идут встык
if (current.getServiceDuration() == next.getServiceDuration()) {
//разбивка совпадает - объединяем
current.setTimeEnd(next.getTimeEnd());
exWorks.set(nextIndex, null); //убили
} else {
//разбивка не совпадает - обрезаем
current.setTimeEnd(nextBegin);
}
}
}
}
//удаляем убитые
for (Iterator<SheduleIndividualWork> it = exWorks.iterator(); it.hasNext();) {
SheduleIndividualWork work = it.next();
if (work == null) {
it.remove();
}
}
}
/**
* Для каждой работы создаеть список "талонов", временных промежутков
* длительностью равной дефолтовой длительности приема по данной работе.
* @param exWorks
* @return
*/
private List<Ticket> getDayTickets(Day day, List<SheduleIndividualWork> exWorks) {
//объединяем перекрывающиеся талоны, поскольку типы работ должны быть одинаковы,
//объединяем только работы с одинаковым временем приема
//todo потестить
optimizeWorks(day, exWorks);
List<Ticket> res = new LinkedList<Ticket>();
for(SheduleIndividualWork siw: exWorks) {
//гранулируем время начала и конца работы
Calendar beginCal = makeTime(day, siw.getTimeBegin());
Calendar endCal = makeTime(day, siw.getTimeEnd());
int step = siw.getServiceDuration();
if(step == 0) {
step = 20;
}
// log.trace("Ticketing: " + begin.getTime() + "," + end.getTime() + " step " + step);
do {
Ticket ticket = new Ticket(beginCal, step);
if (!res.contains(ticket)) {
res.add(ticket);
}
beginCal.add(Calendar.MINUTE, step);
} while (endCal.after(beginCal));
}
Collections.sort(res);
return res;
}
private Calendar makeTime(Day day, Date time) {
Calendar cal = GregorianCalendar.getInstance();
cal.setTime(time);
int hours = cal.get(Calendar.HOUR_OF_DAY);
int minutes = cal.get(Calendar.MINUTE);
if((minutes % 5) > 0) {
minutes = (int) (Math.round(minutes / 5.0) * 5);
if(minutes == 60) {
minutes = 0;
hours++;
}
}
if(hours >= 24) {
hours = 23;
minutes = 59;
}
Calendar res = day.getCalendar();
res.set(Calendar.HOUR_OF_DAY, hours);
res.set(Calendar.MINUTE, minutes);
res.set(Calendar.SECOND, 0);
res.set(Calendar.MILLISECOND, 0);
return res;
}
/**
* Обновляет статусы талонов в соответствии с существующими записями на прием
* @param dayModel
* @param dayReceptions acn be null
*/
private void updateStatus(WeekDay wd, List<SheduleReception> dayReceptions) {
List<Ticket> dayModel = wd.getTickets();
//отмечаем сегодняшние просроченные талоны
Calendar now = GregorianCalendar.getInstance();
if(wd.getDay().getCalendar().before(now)) {
for(Ticket ticket: dayModel) {
if(ticket.getCalendar().before(now)) {
ticket.setBuzy(true);
}
}
}
//отмечаем зянятые
if(dayReceptions != null) {
for(SheduleReception reception: dayReceptions) {
long recBeg = reception.getBegin().getTime();
long recEnd = recBeg + reception.getDuration() *60*1000;
for(Ticket ticket: dayModel) {
if(ticket.isBuzy()) {
continue;
}
long tickBeg = ticket.getTime();
long tickEnd = tickBeg + ticket.getDuration()*60*1000;
if ((recBeg - tickBeg) * (tickEnd - recBeg) > 0) {
ticket.setBuzy(true);
} else if ((recEnd - tickBeg) * (tickEnd - recEnd) > 0) {
ticket.setBuzy(true);
} else if ((tickBeg - recBeg) * (recEnd - tickBeg) > 0) {
ticket.setBuzy(true);
} else if ((tickEnd - recBeg) * (recEnd - tickEnd) > 0) {
ticket.setBuzy(true);
} else if ((tickBeg == recBeg) && (tickEnd == recEnd)) {
ticket.setBuzy(true);
}
}
}
}
//обновляем информацию о первом свободном
for(Ticket ticket: dayModel) {
if(!ticket.isBuzy()) {
wd.setFirstFree(ticket);
break;
}
}
}
}