diff --git a/.gitignore b/.gitignore index ac0ef07..78b3d6a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ build/ .settings .springBeans .sts4-cache +.gigaide bin/ !**/src/main/**/bin/ !**/src/test/**/bin/ diff --git a/src/main/java/ru/vyatsu/qr_access_admin/door/component/DoorEditor.java b/src/main/java/ru/vyatsu/qr_access_admin/door/component/DoorEditor.java index 6936e1a..dca9bb5 100644 --- a/src/main/java/ru/vyatsu/qr_access_admin/door/component/DoorEditor.java +++ b/src/main/java/ru/vyatsu/qr_access_admin/door/component/DoorEditor.java @@ -1,9 +1,5 @@ package ru.vyatsu.qr_access_admin.door.component; -import ru.vyatsu.qr_access_admin.door.entity.DoorEntity; -import ru.vyatsu.qr_access_admin.unit.model.UnitComboBoxModel; -import ru.vyatsu.qr_access_admin.unit.entity.UnitRepository; -import ru.vyatsu.qr_access_admin.unit.mapper.UnitEntityUnitComboBoxModelMapper; import com.vaadin.flow.component.Composite; import com.vaadin.flow.component.Key; import com.vaadin.flow.component.button.Button; @@ -12,31 +8,41 @@ import com.vaadin.flow.component.combobox.ComboBox; import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.textfield.IntegerField; +import com.vaadin.flow.component.textfield.TextField; import com.vaadin.flow.data.binder.BeanValidationBinder; import com.vaadin.flow.data.binder.Binder; import lombok.Getter; import lombok.Setter; +import ru.vyatsu.qr_access_admin.door.entity.DoorEntity; +import ru.vyatsu.qr_access_admin.door.schedule.component.ScheduleEditor; +import ru.vyatsu.qr_access_admin.door.schedule.entity.ScheduleEntity; +import ru.vyatsu.qr_access_admin.door.schedule.entity.ScheduleRepository; +import ru.vyatsu.qr_access_admin.unit.entity.UnitRepository; +import ru.vyatsu.qr_access_admin.unit.mapper.UnitEntityUnitComboBoxModelMapper; +import ru.vyatsu.qr_access_admin.unit.model.UnitComboBoxModel; +import java.util.Collection; import java.util.Map; import java.util.stream.Collectors; public class DoorEditor extends Composite { - private final UnitEntityUnitComboBoxModelMapper unitMapper = new UnitEntityUnitComboBoxModelMapper(); - public interface SaveListener { - void onSave(DoorEntity door); + void onSave(DoorEntity door, Collection schedule); } public interface DeleteListener { - void onDelete(DoorEntity door); + void onDelete(DoorEntity door) throws Exception; } public interface CancelListener { void onCancel(); } - private volatile DoorEntity currentDoor; + private DoorEntity currentDoor; + + private final ScheduleEditor scheduleEditor; @Getter @Setter @@ -53,18 +59,22 @@ public class DoorEditor extends Composite { public void setCurrentDoor(DoorEntity door) { this.currentDoor = door; binder.setBean(door); + scheduleEditor.setCurrentDoorId(door.getId()); + scheduleEditor.refreshScheduleView(); } - public DoorEditor(UnitRepository unitRepository) { + public DoorEditor(UnitRepository unitRepository, ScheduleRepository scheduleRepository) { ComboBox unitField = new ComboBox<>("Устройство"); unitField.setRequired(true); unitField.setPageSize(100); Map units = unitRepository.findAll() .stream() - .map(unitMapper::toModel) + .map(new UnitEntityUnitComboBoxModelMapper()::toModel) .collect(Collectors.toMap(UnitComboBoxModel::clientId, unit -> unit)); unitField.setItems(units.values()); unitField.setItemLabelGenerator(UnitComboBoxModel::clientName); + IntegerField countField = new IntegerField("Количество мест"); + TextField descriptionField = new TextField("Описание"); var save = new Button("Сохранить", VaadinIcon.CHECK.create()); var cancel = new Button("Отмена"); @@ -73,17 +83,27 @@ public class DoorEditor extends Composite { binder.forField(unitField) .withConverter(UnitComboBoxModel::clientId, units::get, "Invalid value") .bind("unitId"); + binder.forField(countField).bind("count"); + binder.forField(descriptionField).bind("description"); save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); - save.addClickListener(e -> saveListener.onSave(currentDoor)); + + scheduleEditor = new ScheduleEditor(scheduleRepository); + + save.addClickListener(e -> saveListener.onSave(currentDoor, scheduleEditor.getNewSchedule())); save.addClickShortcut(Key.ENTER); delete.addThemeVariants(ButtonVariant.LUMO_ERROR); - delete.addClickListener(e -> deleteListener.onDelete(currentDoor)); + delete.addClickListener(e -> { + try { + deleteListener.onDelete(currentDoor); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); cancel.addClickListener(e -> cancelListener.onCancel()); - getContent().add(unitField, new HorizontalLayout(save, cancel, delete)); + getContent().add(unitField, countField, descriptionField, scheduleEditor, new HorizontalLayout(save, cancel, delete)); } - } diff --git a/src/main/java/ru/vyatsu/qr_access_admin/door/entity/DoorEntity.java b/src/main/java/ru/vyatsu/qr_access_admin/door/entity/DoorEntity.java index 6b9928c..09cc660 100644 --- a/src/main/java/ru/vyatsu/qr_access_admin/door/entity/DoorEntity.java +++ b/src/main/java/ru/vyatsu/qr_access_admin/door/entity/DoorEntity.java @@ -16,4 +16,10 @@ public class DoorEntity { @Column private String unitId; + + @Column + private String description; + + @Column + private int count; } diff --git a/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/component/ScheduleEditor.java b/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/component/ScheduleEditor.java new file mode 100644 index 0000000..7a5a412 --- /dev/null +++ b/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/component/ScheduleEditor.java @@ -0,0 +1,203 @@ +package ru.vyatsu.qr_access_admin.door.schedule.component; + +import com.vaadin.flow.component.Composite; +import com.vaadin.flow.component.button.Button; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Span; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.component.timepicker.TimePicker; +import com.vaadin.flow.data.binder.BeanValidationBinder; +import lombok.Setter; +import org.springframework.util.Assert; +import ru.vyatsu.qr_access_admin.door.schedule.entity.ScheduleEntity; +import ru.vyatsu.qr_access_admin.door.schedule.entity.ScheduleRepository; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +import static com.vaadin.flow.component.icon.VaadinIcon.ARROW_LEFT; +import static com.vaadin.flow.component.icon.VaadinIcon.ARROW_RIGHT; + +public class ScheduleEditor extends Composite { + + private Week chosenWeek; + @Setter + private String currentDoorId; + private Map newSchedule = new HashMap<>(); + private Set queriedWeeks = new HashSet<>(); + private final ScheduleRepository repository; + private final VerticalLayout daysEditorWrapper = new VerticalLayout(); + private final Div currentWeek; + + public ScheduleEditor(ScheduleRepository repository) { + this.repository = repository; + this.chosenWeek = Week.current(); + currentWeek = new Div(chosenWeek.toString()); + currentWeek.getStyle().set("align-content", "center"); + refreshScheduleView(); + + var weekChanger = new HorizontalLayout(); + + var rightArrowButton = new Button(ARROW_RIGHT.create(), e -> { + daysEditorWrapper.removeAll(); + this.chosenWeek = new Week(this.chosenWeek.dateMonday.plusDays(7), this.chosenWeek.dateSunday.plusDays(7)); + currentWeek.setText(chosenWeek.toString()); + List foundSchedule; + if (queriedWeeks.contains(chosenWeek)) { + foundSchedule = getEditedScheduleEntriesByWeek(chosenWeek); + } else { + foundSchedule = repository.findByDoorIdAndDateGreaterThanEqualAndDateLessThanEqualOrderByDateAsc(currentDoorId, chosenWeek.dateMonday, chosenWeek.dateSunday); + fillAbsentDays(foundSchedule, currentDoorId, chosenWeek); + } + foundSchedule.forEach(se -> { + HorizontalLayout scheduleEntry = new HorizontalLayout(); + var entryDate = new Span(se.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - E"))); + entryDate.getStyle().set("align-content", "center"); + var timePickerFrom = new TimePicker("Время начала работы"); + var timePickerTo = new TimePicker("Время окончания работы"); + var binder = new BeanValidationBinder<>(ScheduleEntity.class); + binder.setBean(se); + binder.forField(timePickerFrom).bind("startTime"); + binder.forField(timePickerTo).bind("endTime"); + newSchedule.put(se.getDate(), se); + scheduleEntry.add(entryDate, timePickerFrom, timePickerTo); + daysEditorWrapper.add(scheduleEntry); + }); + queriedWeeks.add(chosenWeek); + }); + var leftArrowButton = new Button(ARROW_LEFT.create(), e -> { + daysEditorWrapper.removeAll(); + this.chosenWeek = new Week(this.chosenWeek.dateMonday.minusDays(7), this.chosenWeek.dateSunday.minusDays(7)); + currentWeek.setText(chosenWeek.toString()); + List foundSchedule; + if (queriedWeeks.contains(chosenWeek)) { + foundSchedule = getEditedScheduleEntriesByWeek(chosenWeek); + } else { + foundSchedule = repository.findByDoorIdAndDateGreaterThanEqualAndDateLessThanEqualOrderByDateAsc(currentDoorId, chosenWeek.dateMonday, chosenWeek.dateSunday); + fillAbsentDays(foundSchedule, currentDoorId, chosenWeek); + } + foundSchedule.forEach(se -> { + HorizontalLayout scheduleEntry = new HorizontalLayout(); + var entryDate = new Span(se.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - E"))); + entryDate.getStyle().set("align-content", "center"); + var timePickerFrom = new TimePicker("Время начала работы"); + var timePickerTo = new TimePicker("Время окончания работы"); + var binder = new BeanValidationBinder<>(ScheduleEntity.class); + binder.setBean(se); + binder.forField(timePickerFrom).bind("startTime"); + binder.forField(timePickerTo).bind("endTime"); + newSchedule.put(se.getDate(), se); + scheduleEntry.add(entryDate, timePickerFrom, timePickerTo); + daysEditorWrapper.add(scheduleEntry); + }); + queriedWeeks.add(chosenWeek); + }); + weekChanger.add(leftArrowButton, currentWeek, rightArrowButton); + + getContent().add(weekChanger, daysEditorWrapper); + } + + public void refreshScheduleView() { + this.chosenWeek = Week.current(); + currentWeek.setText(chosenWeek.toString()); + newSchedule = new HashMap<>(); + queriedWeeks = new HashSet<>(); + daysEditorWrapper.removeAll(); + List schedule = repository.findByDoorIdAndDateGreaterThanEqualAndDateLessThanEqualOrderByDateAsc(currentDoorId, chosenWeek.dateMonday, chosenWeek.dateSunday); + fillAbsentDays(schedule, currentDoorId, chosenWeek); + schedule.forEach(se -> { + HorizontalLayout scheduleEntry = new HorizontalLayout(); + var entryDate = new Span(se.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - E"))); + entryDate.getStyle().set("align-content", "center"); + var timePickerFrom = new TimePicker("Время начала работы"); + var timePickerTo = new TimePicker("Время окончания работы"); + var binder = new BeanValidationBinder<>(ScheduleEntity.class); + binder.setBean(se); + binder.forField(timePickerFrom).bind("startTime"); + binder.forField(timePickerTo).bind("endTime"); + newSchedule.put(se.getDate(), se); + scheduleEntry.add(entryDate, timePickerFrom, timePickerTo); + daysEditorWrapper.add(scheduleEntry); + }); + queriedWeeks.add(chosenWeek); + } + + public Collection getNewSchedule() { + return Collections.unmodifiableCollection(newSchedule.values()); + } + + private List getEditedScheduleEntriesByWeek(Week chosenWeek) { + var i = chosenWeek.iterator(); + List result = new ArrayList<>(); + while (i.hasNext()) { + result.add(newSchedule.get(i.next())); + } + return result; + } + + private void fillAbsentDays(List schedule, String currentDoorId, Week week) { + Set existDates = schedule.stream() + .map(ScheduleEntity::getDate) + .collect(Collectors.toSet()); + Iterator iter = week.iterator(); + for (int i = 0; iter.hasNext(); i++) { + LocalDate currDate = iter.next(); + if (!existDates.contains(currDate)) { + ScheduleEntity newEntity = new ScheduleEntity(); + newEntity.setId(UUID.randomUUID().toString()); + newEntity.setDoorId(currentDoorId); + newEntity.setStartTime(LocalTime.MIN); + newEntity.setEndTime(LocalTime.MAX); + newEntity.setDate(currDate); + schedule.add(i, newEntity); + } + } + } + + record Week(LocalDate dateMonday, LocalDate dateSunday) implements Iterable { + public Week { + Objects.requireNonNull(dateMonday, "dateMonday cannot be null"); + Objects.requireNonNull(dateSunday, "dateSunday cannot be null"); + + Assert.state(dateMonday.getDayOfWeek().equals(DayOfWeek.MONDAY), "dateMonday must be Monday"); + Assert.state(dateSunday.getDayOfWeek().equals(DayOfWeek.SUNDAY), "dateSunday must be Sunday"); + + Assert.state(dateSunday.isAfter(dateMonday), "dateMonday should be less than dateSunday"); + } + + public static Week current() { + LocalDate now = LocalDate.now(); + LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1); + LocalDate sunday = now.plusDays(7 - now.getDayOfWeek().getValue()); + return new Week(monday, sunday); + } + + @Override + public String toString() { + DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy"); + return dateMonday.format(dateFormat).concat(" - ").concat(dateSunday.format(dateFormat)); + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + private LocalDate current = dateMonday.minusDays(1); + + @Override + public boolean hasNext() { + return current.isBefore(dateSunday); + } + + @Override + public LocalDate next() { + return current = current.plusDays(1); + } + }; + } + } +} diff --git a/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/entity/ScheduleEntity.java b/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/entity/ScheduleEntity.java new file mode 100644 index 0000000..576c8d2 --- /dev/null +++ b/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/entity/ScheduleEntity.java @@ -0,0 +1,32 @@ +package ru.vyatsu.qr_access_admin.door.schedule.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDate; +import java.time.LocalTime; + +@Entity +@Table(name = "schedule") +@Getter +@Setter +public class ScheduleEntity { + @Id + private String id; + + @Column + private String doorId; + + @Column + private LocalTime startTime; + + @Column + private LocalTime endTime; + + @Column + private LocalDate date; +} diff --git a/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/entity/ScheduleRepository.java b/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/entity/ScheduleRepository.java new file mode 100644 index 0000000..77d5f95 --- /dev/null +++ b/src/main/java/ru/vyatsu/qr_access_admin/door/schedule/entity/ScheduleRepository.java @@ -0,0 +1,12 @@ +package ru.vyatsu.qr_access_admin.door.schedule.entity; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.time.LocalDate; +import java.util.List; + +public interface ScheduleRepository extends JpaRepository { + List findByDoorIdAndDateGreaterThanEqualAndDateLessThanEqualOrderByDateAsc(String doorId, LocalDate dateStart, LocalDate dateEnd); + + void deleteAllByDoorId(String doorId); +} \ No newline at end of file diff --git a/src/main/java/ru/vyatsu/qr_access_admin/door/view/DoorView.java b/src/main/java/ru/vyatsu/qr_access_admin/door/view/DoorView.java index 08decbc..2213a04 100644 --- a/src/main/java/ru/vyatsu/qr_access_admin/door/view/DoorView.java +++ b/src/main/java/ru/vyatsu/qr_access_admin/door/view/DoorView.java @@ -1,10 +1,5 @@ package ru.vyatsu.qr_access_admin.door.view; -import ru.vyatsu.qr_access_admin.common.MainLayout; -import ru.vyatsu.qr_access_admin.door.component.DoorEditor; -import ru.vyatsu.qr_access_admin.door.entity.DoorEntity; -import ru.vyatsu.qr_access_admin.door.entity.DoorRepository; -import ru.vyatsu.qr_access_admin.unit.entity.UnitRepository; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.icon.VaadinIcon; @@ -12,6 +7,16 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.Route; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Query; +import ru.vyatsu.qr_access_admin.common.MainLayout; +import ru.vyatsu.qr_access_admin.door.component.DoorEditor; +import ru.vyatsu.qr_access_admin.door.entity.DoorEntity; +import ru.vyatsu.qr_access_admin.door.entity.DoorRepository; +import ru.vyatsu.qr_access_admin.door.schedule.entity.ScheduleEntity; +import ru.vyatsu.qr_access_admin.door.schedule.entity.ScheduleRepository; +import ru.vyatsu.qr_access_admin.unit.entity.UnitRepository; import java.util.List; @@ -20,15 +25,19 @@ import java.util.List; public class DoorView extends VerticalLayout { private final Grid grid; private final DoorRepository repository; + private final ScheduleRepository scheduleRepository; private final DoorEditor editor; + private final EntityManagerFactory emf; - public DoorView(DoorRepository repository, UnitRepository unitRepository) { + public DoorView(DoorRepository repository, UnitRepository unitRepository, ScheduleRepository scheduleRepository, EntityManagerFactory emf) { this.repository = repository; + this.scheduleRepository = scheduleRepository; + this.emf = emf; var addButton = new Button("Добавить дверь", VaadinIcon.PLUS.create()); grid = new Grid<>(DoorEntity.class); - editor = new DoorEditor(unitRepository); + editor = new DoorEditor(unitRepository, scheduleRepository); var actionsLayout = new HorizontalLayout(addButton); add(actionsLayout, grid, editor); @@ -62,14 +71,26 @@ public class DoorView extends VerticalLayout { private void configureEditor() { editor.setVisible(false); - editor.setSaveListener(door -> { - repository.save(door); + editor.setSaveListener((door, schedule) -> { + DoorEntity savedDoor = repository.save(door); + for (ScheduleEntity se : schedule) { + se.setDoorId(savedDoor.getId()); + } + scheduleRepository.saveAll(schedule); refreshDoorsGrid(); editDoor(null); }); editor.setDeleteListener(door -> { - repository.deleteById(door.getId()); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Query scheduleDeleteQuery = em.createQuery("delete from ScheduleEntity s where s.doorId = :doorId"); + scheduleDeleteQuery.setParameter("doorId", door.getId()); + scheduleDeleteQuery.executeUpdate(); + Query doorDeleteQuery = em.createQuery("delete from DoorEntity d where d.id = :id"); + doorDeleteQuery.setParameter("id", door.getId()); + doorDeleteQuery.executeUpdate(); + em.getTransaction().commit(); refreshDoorsGrid(); editDoor(null); });