Added schedule editing

This commit is contained in:
kashiuno 2025-02-14 18:17:51 +03:00
parent 13f3132947
commit c847830a7e
7 changed files with 320 additions and 25 deletions

1
.gitignore vendored
View File

@ -14,6 +14,7 @@ build/
.settings .settings
.springBeans .springBeans
.sts4-cache .sts4-cache
.gigaide
bin/ bin/
!**/src/main/**/bin/ !**/src/main/**/bin/
!**/src/test/**/bin/ !**/src/test/**/bin/

View File

@ -1,9 +1,5 @@
package ru.vyatsu.qr_access_admin.door.component; 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.Composite;
import com.vaadin.flow.component.Key; import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.button.Button; 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.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout; 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.BeanValidationBinder;
import com.vaadin.flow.data.binder.Binder; import com.vaadin.flow.data.binder.Binder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; 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.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class DoorEditor extends Composite<VerticalLayout> { public class DoorEditor extends Composite<VerticalLayout> {
private final UnitEntityUnitComboBoxModelMapper unitMapper = new UnitEntityUnitComboBoxModelMapper();
public interface SaveListener { public interface SaveListener {
void onSave(DoorEntity door); void onSave(DoorEntity door, Collection<ScheduleEntity> schedule);
} }
public interface DeleteListener { public interface DeleteListener {
void onDelete(DoorEntity door); void onDelete(DoorEntity door) throws Exception;
} }
public interface CancelListener { public interface CancelListener {
void onCancel(); void onCancel();
} }
private volatile DoorEntity currentDoor; private DoorEntity currentDoor;
private final ScheduleEditor scheduleEditor;
@Getter @Getter
@Setter @Setter
@ -53,18 +59,22 @@ public class DoorEditor extends Composite<VerticalLayout> {
public void setCurrentDoor(DoorEntity door) { public void setCurrentDoor(DoorEntity door) {
this.currentDoor = door; this.currentDoor = door;
binder.setBean(door); binder.setBean(door);
scheduleEditor.setCurrentDoorId(door.getId());
scheduleEditor.refreshScheduleView();
} }
public DoorEditor(UnitRepository unitRepository) { public DoorEditor(UnitRepository unitRepository, ScheduleRepository scheduleRepository) {
ComboBox<UnitComboBoxModel> unitField = new ComboBox<>("Устройство"); ComboBox<UnitComboBoxModel> unitField = new ComboBox<>("Устройство");
unitField.setRequired(true); unitField.setRequired(true);
unitField.setPageSize(100); unitField.setPageSize(100);
Map<String, UnitComboBoxModel> units = unitRepository.findAll() Map<String, UnitComboBoxModel> units = unitRepository.findAll()
.stream() .stream()
.map(unitMapper::toModel) .map(new UnitEntityUnitComboBoxModelMapper()::toModel)
.collect(Collectors.toMap(UnitComboBoxModel::clientId, unit -> unit)); .collect(Collectors.toMap(UnitComboBoxModel::clientId, unit -> unit));
unitField.setItems(units.values()); unitField.setItems(units.values());
unitField.setItemLabelGenerator(UnitComboBoxModel::clientName); unitField.setItemLabelGenerator(UnitComboBoxModel::clientName);
IntegerField countField = new IntegerField("Количество мест");
TextField descriptionField = new TextField("Описание");
var save = new Button("Сохранить", VaadinIcon.CHECK.create()); var save = new Button("Сохранить", VaadinIcon.CHECK.create());
var cancel = new Button("Отмена"); var cancel = new Button("Отмена");
@ -73,17 +83,27 @@ public class DoorEditor extends Composite<VerticalLayout> {
binder.forField(unitField) binder.forField(unitField)
.withConverter(UnitComboBoxModel::clientId, units::get, "Invalid value") .withConverter(UnitComboBoxModel::clientId, units::get, "Invalid value")
.bind("unitId"); .bind("unitId");
binder.forField(countField).bind("count");
binder.forField(descriptionField).bind("description");
save.addThemeVariants(ButtonVariant.LUMO_PRIMARY); 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); save.addClickShortcut(Key.ENTER);
delete.addThemeVariants(ButtonVariant.LUMO_ERROR); 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()); cancel.addClickListener(e -> cancelListener.onCancel());
getContent().add(unitField, new HorizontalLayout(save, cancel, delete)); getContent().add(unitField, countField, descriptionField, scheduleEditor, new HorizontalLayout(save, cancel, delete));
} }
} }

View File

@ -16,4 +16,10 @@ public class DoorEntity {
@Column @Column
private String unitId; private String unitId;
@Column
private String description;
@Column
private int count;
} }

View File

@ -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<VerticalLayout> {
private Week chosenWeek;
@Setter
private String currentDoorId;
private Map<LocalDate, ScheduleEntity> newSchedule = new HashMap<>();
private Set<Week> 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<ScheduleEntity> 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<ScheduleEntity> 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<ScheduleEntity> 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<ScheduleEntity> getNewSchedule() {
return Collections.unmodifiableCollection(newSchedule.values());
}
private List<ScheduleEntity> getEditedScheduleEntriesByWeek(Week chosenWeek) {
var i = chosenWeek.iterator();
List<ScheduleEntity> result = new ArrayList<>();
while (i.hasNext()) {
result.add(newSchedule.get(i.next()));
}
return result;
}
private void fillAbsentDays(List<ScheduleEntity> schedule, String currentDoorId, Week week) {
Set<LocalDate> existDates = schedule.stream()
.map(ScheduleEntity::getDate)
.collect(Collectors.toSet());
Iterator<LocalDate> 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<LocalDate> {
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<LocalDate> 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);
}
};
}
}
}

View File

@ -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;
}

View File

@ -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<ScheduleEntity, String> {
List<ScheduleEntity> findByDoorIdAndDateGreaterThanEqualAndDateLessThanEqualOrderByDateAsc(String doorId, LocalDate dateStart, LocalDate dateEnd);
void deleteAllByDoorId(String doorId);
}

View File

@ -1,10 +1,5 @@
package ru.vyatsu.qr_access_admin.door.view; 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.button.Button;
import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.VaadinIcon; 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.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle; import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route; 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; import java.util.List;
@ -20,15 +25,19 @@ import java.util.List;
public class DoorView extends VerticalLayout { public class DoorView extends VerticalLayout {
private final Grid<DoorEntity> grid; private final Grid<DoorEntity> grid;
private final DoorRepository repository; private final DoorRepository repository;
private final ScheduleRepository scheduleRepository;
private final DoorEditor editor; 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.repository = repository;
this.scheduleRepository = scheduleRepository;
this.emf = emf;
var addButton = new Button("Добавить дверь", VaadinIcon.PLUS.create()); var addButton = new Button("Добавить дверь", VaadinIcon.PLUS.create());
grid = new Grid<>(DoorEntity.class); grid = new Grid<>(DoorEntity.class);
editor = new DoorEditor(unitRepository); editor = new DoorEditor(unitRepository, scheduleRepository);
var actionsLayout = new HorizontalLayout(addButton); var actionsLayout = new HorizontalLayout(addButton);
add(actionsLayout, grid, editor); add(actionsLayout, grid, editor);
@ -62,14 +71,26 @@ public class DoorView extends VerticalLayout {
private void configureEditor() { private void configureEditor() {
editor.setVisible(false); editor.setVisible(false);
editor.setSaveListener(door -> { editor.setSaveListener((door, schedule) -> {
repository.save(door); DoorEntity savedDoor = repository.save(door);
for (ScheduleEntity se : schedule) {
se.setDoorId(savedDoor.getId());
}
scheduleRepository.saveAll(schedule);
refreshDoorsGrid(); refreshDoorsGrid();
editDoor(null); editDoor(null);
}); });
editor.setDeleteListener(door -> { 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(); refreshDoorsGrid();
editDoor(null); editDoor(null);
}); });