Skip to content

Commit

Permalink
CSCEXAM-105 Allow admin to change reservation's room
Browse files Browse the repository at this point in the history
- also fix layout issue with dropdown-select component
- add some more functionality to dropdown-select component
- modernize some code in ReservationController
  • Loading branch information
lupari committed Oct 15, 2024
1 parent f026e80 commit ef45115
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 61 deletions.
38 changes: 14 additions & 24 deletions app/controllers/enrolment/ReservationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
import models.user.User;
import org.joda.time.DateTime;
import org.joda.time.format.ISODateTimeFormat;
import play.data.DynamicForm;
import play.libs.Json;
import play.mvc.Http;
import play.mvc.Result;
Expand Down Expand Up @@ -164,34 +163,28 @@ public CompletionStage<Result> removeReservation(long id, Http.Request request)
}

@Restrict({ @Group("ADMIN") })
public Result findAvailableMachines(Long reservationId) throws ExecutionException, InterruptedException {
public Result findAvailableMachines(Long reservationId, Long roomId)
throws ExecutionException, InterruptedException {
var reservation = DB.find(Reservation.class, reservationId);
if (reservation == null) {
if (reservation == null || DB.find(ExamRoom.class, roomId) == null) {
return notFound();
}
var props = PathProperties.parse("(id, name)");
var query = DB.createQuery(ExamMachine.class);
props.apply(query);

var candidates = query
.where()
.eq("room", reservation.getMachine().getRoom())
.ne("outOfService", true)
.ne("archived", true)
.findList();
var candidates = query.where().eq("room.id", roomId).ne("outOfService", true).ne("archived", true).findList();

var exam = getReservationExam(reservation);
var it = candidates.listIterator();
while (it.hasNext()) {
var machine = it.next();
if (exam.isPresent() && !machine.hasRequiredSoftware(exam.get())) {
it.remove();
}
if (machine.isReservedDuring(reservation.toInterval())) {
it.remove();
}
}
return ok(candidates, props);
var available = candidates
.stream()
.filter(c -> {
if (exam.isPresent() && !c.hasRequiredSoftware(exam.get())) {
return false;
}
return !c.isReservedDuring(reservation.toInterval());
})
.toList();
return ok(available, props);
}

@Restrict({ @Group("ADMIN") })
Expand All @@ -211,9 +204,6 @@ public Result updateMachine(Long reservationId, Http.Request request)
if (machine == null) {
return notFound();
}
if (!machine.getRoom().equals(reservation.getMachine().getRoom())) {
return forbidden("Not allowed to change to use a machine from a different room");
}
var exam = getReservationExam(reservation);
if (
exam.isEmpty() ||
Expand Down
2 changes: 1 addition & 1 deletion conf/routes
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ GET /app/reservations/exams c
GET /app/reservations controllers.enrolment.ReservationController.getReservations(state: java.util.Optional[String], ownerId: java.util.Optional[java.lang.Long], studentId: java.util.Optional[java.lang.Long], roomId: java.util.Optional[java.lang.Long], machineId: java.util.Optional[java.lang.Long], examId: java.util.Optional[java.lang.Long], start: java.util.Optional[String], end: java.util.Optional[String], externalRef: java.util.Optional[String], request: Request)
GET /app/events controllers.enrolment.ReservationController.getExaminationEvents(state: java.util.Optional[String], ownerId: java.util.Optional[java.lang.Long], studentId: java.util.Optional[java.lang.Long], examId: java.util.Optional[java.lang.Long], start: java.util.Optional[String], end: java.util.Optional[String], request: Request)
DELETE /app/reservations/:id controllers.enrolment.ReservationController.removeReservation(id: Long, request: Request)
GET /app/reservations/:id/machines controllers.enrolment.ReservationController.findAvailableMachines(id: Long)
GET /app/reservations/:reservationId/:roomId/machines controllers.enrolment.ReservationController.findAvailableMachines(reservationId: Long, roomId: Long)
PUT /app/reservations/:id/machine controllers.enrolment.ReservationController.updateMachine(id: Long, request: Request)

############### ExamMachines interface ###############
Expand Down
101 changes: 73 additions & 28 deletions ui/src/app/reservation/admin/change-machine-dialog.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import { HttpClient } from '@angular/common/http';
import type { OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Component, Input, ViewChild } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { ToastrService } from 'ngx-toastr';
import type { ExamMachine, Reservation } from 'src/app/reservation/reservation.model';
import { map } from 'rxjs';
import type { ExamMachine, ExamRoom, Reservation } from 'src/app/reservation/reservation.model';
import { DropdownSelectComponent } from 'src/app/shared/select/dropdown-select.component';
import { Option } from 'src/app/shared/select/select.model';

Expand All @@ -23,29 +24,50 @@ import { Option } from 'src/app/shared/select/select.model';
</h4>
</div>
<div class="modal-body">
<strong>{{ 'i18n_exam_machine' | translate }}</strong>
<xm-dropdown-select
[options]="availableMachineOptions"
(optionSelected)="machineChanged($event)"
(limitTo)="(0)"
placeholder="{{ 'i18n_select' | translate }}"
autofocus
></xm-dropdown-select>
</div>
<div class="d-flex flex-row-reverse flex-align-r m-3">
<button class="btn btn-sm btn-primary" (click)="ok()" [disabled]="!selection?.id">
{{ 'i18n_button_save' | translate }}
</button>
<button class="btn btn-sm btn-danger me-3" (click)="cancel()">
{{ 'i18n_button_cancel' | translate }}
</button>
<form>
<div class="form-group">
<label for="room">{{ 'i18n_examination_location' | translate }}</label>
<xm-dropdown-select
id="room"
[initial]="room"
[options]="availableRoomOptions"
[limitTo]="0"
[allowClearing]="false"
(optionSelected)="roomChanged($event)"
placeholder="{{ 'i18n_select' | translate }}"
></xm-dropdown-select>
</div>
<div class="form-group mt-2">
<label for="room">{{ 'i18n_exam_machine' | translate }}</label>
<xm-dropdown-select
#machineSelection
[options]="availableMachineOptions"
[limitTo]="0"
[allowClearing]="false"
(optionSelected)="machineChanged($event)"
placeholder="{{ 'i18n_select' | translate }}"
autofocus
></xm-dropdown-select>
</div>
</form>
<div class="d-flex flex-row-reverse flex-align-r m-3">
<button class="btn btn-sm btn-primary" (click)="ok()" [disabled]="!machine?.id">
{{ 'i18n_button_save' | translate }}
</button>
<button class="btn btn-sm btn-danger me-3" (click)="cancel()">
{{ 'i18n_button_cancel' | translate }}
</button>
</div>
</div>
`,
})
export class ChangeMachineDialogComponent implements OnInit {
@Input() reservation!: Reservation;
@ViewChild('machineSelection') machineSelection!: DropdownSelectComponent<ExamMachine, number>;

selection?: ExamMachine;
room!: Option<ExamRoom, number>;
availableRoomOptions: Option<ExamRoom, number>[] = [];
machine?: ExamMachine;
availableMachineOptions: Option<ExamMachine, number>[] = [];

constructor(
Expand All @@ -56,23 +78,36 @@ export class ChangeMachineDialogComponent implements OnInit {
) {}

ngOnInit() {
this.http.get<ExamMachine[]>(`/app/reservations/${this.reservation.id}/machines`).subscribe(
(resp) =>
(this.availableMachineOptions = resp.map((o) => {
return {
const room = this.reservation.machine.room;
this.room = { id: room.id, label: room.name, value: room };
this.http
.get<ExamRoom[]>('/app/rooms')
.pipe(map((rs) => rs.filter((r) => !r.outOfService)))
.subscribe(
(resp) =>
(this.availableRoomOptions = resp.map((o) => ({
id: o.id,
label: o.name,
value: o,
};
})),
);
}))),
);
this.setAvailableMachines();
}

machineChanged = (event: Option<ExamMachine, number> | undefined) => (this.selection = event?.value);
machineChanged = (event?: Option<ExamMachine, number>) => {
this.machine = event?.value;
};
roomChanged = (event?: Option<ExamRoom, number>) => {
const room = event?.value as ExamRoom;
this.room = { id: room.id, label: room.name, value: room };
delete this.machine;
this.machineSelection.clearSelection();
this.setAvailableMachines();
};

ok = () =>
this.http
.put<ExamMachine>(`/app/reservations/${this.reservation.id}/machine`, { machineId: this.selection?.id })
.put<ExamMachine>(`/app/reservations/${this.reservation.id}/machine`, { machineId: this.machine?.id })
.subscribe({
next: (resp) => {
this.toast.info(this.translate.instant('i18n_updated'));
Expand All @@ -82,4 +117,14 @@ export class ChangeMachineDialogComponent implements OnInit {
});

cancel = () => this.activeModal.dismiss();

private setAvailableMachines = () =>
this.http.get<ExamMachine[]>(`/app/reservations/${this.reservation.id}/${this.room.id}/machines`).subscribe(
(resp) =>
(this.availableMachineOptions = resp.map((o) => ({
id: o.id,
label: o.name,
value: o,
}))),
);
}
9 changes: 5 additions & 4 deletions ui/src/app/reservation/reservation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,14 @@ export class ReservationService {
keyboard: false,
});
modalRef.componentInstance.reservation = reservation;
modalRef.result
.then((machine: ExamMachine) => {
from(modalRef.result).subscribe({
next: (machine: ExamMachine) => {
if (machine) {
reservation.machine = machine;
}
})
.catch(noop);
},
error: noop,
});
};

cancelReservation = (reservation: Reservation): Promise<void> => {
Expand Down
18 changes: 14 additions & 4 deletions ui/src/app/shared/select/dropdown-select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// SPDX-License-Identifier: EUPL-1.2

import { NgClass, SlicePipe } from '@angular/common';
import { NgClass, NgIf, SlicePipe } from '@angular/common';
import type { OnChanges, OnInit } from '@angular/core';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';
Expand All @@ -26,8 +26,9 @@ import { Option } from './select.model';
</button>
<div ngbDropdownMenu class="xm-scrollable-menu" role="menu" aria-labelledby="dd1">
@if (!noSearch) {
<div class="input-group" ngbDropdownItem>
<div class="input-group p-1">
<input
type="text"
[(ngModel)]="searchFilter"
class="form-control"
(input)="filterOptions()"
Expand All @@ -40,11 +41,16 @@ import { Option } from './select.model';
</div>
</div>
}
<button ngbDropdownItem (click)="clearSelection(); d.close()">
<button type="button" ngbDropdownItem *ngIf="allowClearing" (click)="clearSelection(); d.close()">
<i class="bi-x text text-danger"></i>
</button>
@for (opt of filteredOptions; track $index) {
<button ngbDropdownItem [ngClass]="getClasses(opt)" (click)="selectOption(opt); d.close()">
<button
type="button"
ngbDropdownItem
[ngClass]="getClasses(opt)"
(click)="selectOption(opt); d.close()"
>
@if (!opt.isHeader) {
<span>
{{ opt.label || '' | translate | slice: 0 : 40 }}
Expand All @@ -61,6 +67,7 @@ import { Option } from './select.model';
imports: [
NgbDropdown,
NgbDropdownToggle,
NgIf,
NgClass,
NgbDropdownMenu,
FormsModule,
Expand All @@ -72,17 +79,20 @@ import { Option } from './select.model';
})
export class DropdownSelectComponent<V, I> implements OnInit, OnChanges {
@Input() options: Option<V, I>[] = []; // everything
@Input() initial?: Option<V, I>;
@Input() placeholder = 'i18n_choose';
@Input() limitTo?: number;
@Input() fullWidth = false;
@Input() noSearch = false;
@Input() allowClearing = true;
@Output() optionSelected = new EventEmitter<Option<V, I> | undefined>();
filteredOptions: Option<V, I>[] = []; // filtered
searchFilter = '';
selected?: Option<V, I>;

ngOnInit() {
this.filterOptions();
this.selected = this.initial;
}

ngOnChanges() {
Expand Down

0 comments on commit ef45115

Please sign in to comment.