Commit 7bb300ee authored by Tomáš Biloš's avatar Tomáš Biloš
Browse files

Merge branch 'xmozolak/mission-create-edit' into 'main'

feat: Mission fe create and edit

See merge request !87
parents 86a8c882 0e776a9e
package cz.fi.muni.pa165.seminar4.group7.dto.mission;
import cz.fi.muni.pa165.seminar4.group7.dto.resource.ResourceDTO;
import lombok.Getter;
import lombok.Setter;
import java.sql.Date;
import java.util.List;
/**
* @author Tomáš Biloš
*/
@Getter
@Setter
public class MissionEditDTO {
private String id;
private String name;
private Date start;
private int durationInDays;
private String objective;
private String countryId;
private List<ResourceDTO> resources;
}
package cz.fi.muni.pa165.seminar4.group7.facade;
import cz.fi.muni.pa165.seminar4.group7.dto.mission.MissionDTO;
import cz.fi.muni.pa165.seminar4.group7.dto.mission.MissionEditDTO;
/**
* @author Milan Mozolak
......@@ -10,5 +11,5 @@ public interface MissionFacade {
/**
* Updates mission
*/
MissionDTO updateMission(MissionDTO agent);
MissionDTO updateMission(MissionEditDTO agent);
}
package cz.fi.muni.pa165.seminar4.group7.controller;
import cz.fi.muni.pa165.seminar4.group7.agent.AgentService;
import cz.fi.muni.pa165.seminar4.group7.dto.mission.MissionEditDTO;
import cz.fi.muni.pa165.seminar4.group7.mapping.BeanMappingService;
import cz.fi.muni.pa165.seminar4.group7.country.CountryService;
import cz.fi.muni.pa165.seminar4.group7.mission.MissionService;
......@@ -69,7 +70,7 @@ public class MissionController {
}
@PutMapping("/{id}")
public MissionDTO updateMission(@PathVariable("id") String id, @RequestBody MissionDTO missionDTO) {
public MissionDTO updateMission(@PathVariable("id") String id, @RequestBody MissionEditDTO missionDTO) {
if (!id.equals(missionDTO.getId())) {
throw new IllegalArgumentException("Provided data is inconsistent");
}
......
package cz.fi.muni.pa165.seminar4.group7.mission;
import cz.fi.muni.pa165.seminar4.group7.country.CountryService;
import cz.fi.muni.pa165.seminar4.group7.dto.mission.MissionDTO;
import cz.fi.muni.pa165.seminar4.group7.dto.mission.MissionEditDTO;
import cz.fi.muni.pa165.seminar4.group7.entity.Mission;
import cz.fi.muni.pa165.seminar4.group7.entity.Resource;
import cz.fi.muni.pa165.seminar4.group7.facade.MissionFacade;
......@@ -20,13 +22,17 @@ public class MissionFacadeImpl implements MissionFacade {
private final MissionService missionService;
public MissionFacadeImpl(BeanMappingService beanMappingService, MissionService missionService) {
private final CountryService countryService;
public MissionFacadeImpl(BeanMappingService beanMappingService, MissionService missionService,
CountryService countryService) {
this.beanMappingService = beanMappingService;
this.missionService = missionService;
this.countryService = countryService;
}
@Override
public MissionDTO updateMission(MissionDTO missionDTO) {
public MissionDTO updateMission(MissionEditDTO missionDTO) {
if (missionDTO.getId() == null) {
throw new IllegalArgumentException("Invalid null id");
}
......@@ -40,10 +46,16 @@ public class MissionFacadeImpl implements MissionFacade {
throw new IllegalArgumentException("Mission doesn't exist");
}
var country = countryService.findById(missionDTO.getCountryId());
if (country.isEmpty()) {
throw new IllegalArgumentException("Country doesn't exist");
}
var updatedMission = beanMappingService.map(missionDTO, Mission.class);
var mission = old.get().setName(updatedMission.getName()).setObjective(updatedMission.getObjective())
.setResources(createResources(updatedMission.getResources()));
var mission = old.get().setName(updatedMission.getName()).setStart(updatedMission.getStart())
.setDurationInDays(updatedMission.getDurationInDays()).setObjective(updatedMission.getObjective())
.setCountry(country.get()).setResources(createResources(updatedMission.getResources()));
missionService.update(mission);
return beanMappingService.map(mission, MissionDTO.class);
......
......@@ -18,6 +18,7 @@ import { LoginComponent } from './login/login.component';
import { MissionDetailComponent } from './mission/mission-detail.component';
import { MissionListComponent } from './mission/mission-list.component';
import { NotFoundComponent } from './not-found.component';
import {EditMissionComponent} from "./mission/edit-mission.component";
const routes: Routes = [
{
......@@ -50,7 +51,9 @@ const routes: Routes = [
{ path: 'country/create', component: CountryEditComponent },
{ path: 'countries', component: CountryListComponent },
{ path: 'country/:id', component: CountryDetailComponent },
{ path: 'mission/create', component: EditMissionComponent },
{ path: 'mission/:id', component: MissionDetailComponent },
{ path: 'mission/edit/:id', component: EditMissionComponent },
{ path: 'missions', component: MissionListComponent },
],
},
......
......@@ -37,6 +37,7 @@ import { MatNativeDateModule } from "@angular/material/core";
import { MatToolbarModule } from "@angular/material/toolbar";
import { NotFoundComponent } from './not-found.component';
import {AssignmentListComponent} from "./assignment/assignment-list.component";
import {EditMissionComponent} from "./mission/edit-mission.component";
@NgModule({
declarations: [
......@@ -50,6 +51,7 @@ import {AssignmentListComponent} from "./assignment/assignment-list.component";
CountryEditComponent,
MissionListComponent,
MissionDetailComponent,
EditMissionComponent,
HomeComponent,
NotFoundComponent,
AssignmentDetailComponent,
......
<mat-toolbar *ngIf="mission" color="primary" class="flex flex-row">
<button mat-icon-button routerLink="/auth/missions" aria-label="Cancel editing">
<mat-icon>close</mat-icon>
</button>
<span>Edit mission</span>
<span class="flex-1"></span>
<button mat-icon-button [matMenuTriggerFor]="menu" aria-label="Menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item>Delete</button>
</mat-menu>
</mat-toolbar>
<div *ngIf="mission" class="main-container">
<form [formGroup]="edit" class="flex flex-col px-4">
<mat-form-field>
<mat-label>Mission name</mat-label>
<input matInput formControlName="name" />
<mat-error>Must match {{ "[a-zA-Z]{3,}" }}</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Start date:</mat-label>
<input matInput type="Date" formControlName="start"/>
<mat-error>Must be valid date</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Duration in days</mat-label>
<input matInput formControlName="durationInDays" type="number" />
<mat-error>Must be at least 1 day</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Objective</mat-label>
<input matInput formControlName="objective" />
<mat-error>Must be filled</mat-error>
</mat-form-field>
<mat-form-field>
<mat-label>Country</mat-label>
<mat-select formControlName="country" [(value)]="mission.countryId">
<mat-option *ngFor="let country of countries" [value]="country.id">
{{ country.name }}
</mat-option>
</mat-select>
</mat-form-field>
</form>
<mat-divider></mat-divider>
<mat-list>
<div mat-subheader>Resources</div>
<mat-list-item *ngFor="let resource of mission.resources">
<mat-icon mat-list-icon>markunread_mailbox</mat-icon>
<h2 mat-line> {{resource.name}} </h2>
</mat-list-item>
<mat-list-item>
<mat-icon mat-list-icon></mat-icon>
<mat-form-field mat-line>
<mat-label>New resource</mat-label>
<input matInput #newResource />
</mat-form-field>
<button mat-icon-button (click)="addResource(newResource)">
<mat-icon>add</mat-icon>
</button>
</mat-list-item>
<mat-divider></mat-divider>
</mat-list>
<button color="accent" mat-fab class="!fixed right-8 bottom-20" aria-label="Save mission details"
disabled="{{ !edit.valid }}" (click)="saveMission()">
<mat-icon>done</mat-icon>
</button>
</div>
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import { environment } from 'src/environments/environment';
import { RequestState } from '../request-state.enum';
import {EditMission, Mission} from "./mission.interface";
import {Country} from "../country/country.interface";
@Component({
selector: 'edit-mission',
templateUrl: 'edit-mission.component.html',
})
export class EditMissionComponent implements OnInit {
countries: Country[] | null = null;
mission: EditMission = {
name: '',
start: '',
durationInDays: 0,
objective: '',
countryId: '',
resources: []
};
state: RequestState = RequestState.SUCCESS;
errorMessage: string = '';
edit = new FormGroup({
name: new FormControl('', [
Validators.pattern(/[a-zA-Z]{3,}/),
Validators.required,
]),
start: new FormControl( '', [Validators.required]),
durationInDays: new FormControl('', [Validators.min(0), Validators.required]),
objective: new FormControl('', [
Validators.required,
]),
country: new FormControl('', [
Validators.required,
]),
});
creatingNewMission = false;
resourceColumns = ['name'];
constructor(
private httpClient: HttpClient,
private route: ActivatedRoute,
private router: Router
) {}
async ngOnInit() {
const id = this.route.snapshot.params['id'];
if (id) {
this.creatingNewMission = false;
try {
this.state = RequestState.PENDING;
let originalMission = await firstValueFrom(
this.httpClient.get<Mission>(`${environment.apiUrl}/missions/${id}`)
);
this.mission = {
id: originalMission.id,
name: originalMission.name,
start: originalMission.start,
durationInDays: originalMission.durationInDays,
objective: originalMission.objective,
countryId: originalMission.country.id,
resources: originalMission.resources
};
this.edit.patchValue({
name: this.mission.name,
start: this.mission.start,
durationInDays: this.mission.durationInDays,
objective: this.mission.objective,
countryId: this.mission.countryId,
});
this.state = RequestState.SUCCESS;
} catch (error) {
this.errorMessage = 'Loading mission failed';
this.state = RequestState.ERROR;
}
} else {
this.creatingNewMission = true;
}
this.countries = await firstValueFrom(
this.httpClient.get<Country[]>(`${environment.apiUrl}/countries/`)
);
}
addResource(input: HTMLInputElement) {
const newResource = input.value
if (!newResource || newResource.length === 0) {
return;
}
if (!this.mission?.resources) {
this.mission!.resources = [];
}
this.mission!.resources.push({ name: newResource });
this.mission!.resources = [...this.mission!.resources];
input.value = ""
}
async saveMission() {
console.log(this.edit);
this.mission.name = this.edit.get('name')?.value;
this.mission.start = this.edit.get('start')?.value;
this.mission.durationInDays = this.edit.get('durationInDays')?.value;
this.mission.objective = this.edit.get('objective')?.value;
this.mission.countryId = this.edit.get('country')?.value;
console.log(this.mission);
if (this.creatingNewMission) {
this.mission = await firstValueFrom(
this.httpClient.post<EditMission>(`${environment.apiUrl}/missions`, this.mission)
);
this.router.navigate(['auth', 'missions']);
} else {
this.mission = await firstValueFrom(
this.httpClient.put<EditMission>(
`${environment.apiUrl}/missions/${this.mission.id}`,
this.mission
)
);
this.router.navigate(['auth', 'mission', this.mission.id]);
}
}
}
<div *ngIf="state === requestState.ERROR">
Error occured: {{ errorMessage }}
</div>
<div *ngIf="mission && state === requestState.SUCCESS" class="px-4">
<div *ngIf="!editing" class="text-gray-600">
<div class="text-xl text-black">{{ mission.name }}</div>
<span class="text-md">Objective</span>
<p class="mb-4 border-b-[1xp] border-gray-200">
{{ mission.objective }}
</p>
<span class="text-md">Start</span>
<p class="mb-4 border-b-[1xp] border-gray-200">
{{ mission.start }}
</p>
<mat-toolbar *ngIf="mission" color="primary" class="flex flex-row">
<button mat-icon-button routerLink="/auth/missions" aria-label="Go back">
<mat-icon>arrow_back</mat-icon>
</button>
<span class="text-md">Duration in days</span>
<p class="mb-4 border-b-[1xp] border-gray-200">
{{ mission.durationInDays }}
</p>
<span>Mission detail</span>
<span class="text-md">Country</span>
<p class="mb-4 border-b-[1xp] border-gray-200">
{{ mission.country.name }}
</p>
<span class="flex-1"></span>
<button mat-icon-button *ngIf="!editing" (click)="startEditing()">
<mat-icon>edit</mat-icon>
</button>
</div>
<div *ngIf="editing">
<mat-form-field>
<mat-label>Mission name</mat-label>
<input matInput [(ngModel)]="mission.name" />
</mat-form-field>
</div>
<button mat-icon-button [matMenuTriggerFor]="menu" aria-label="Example icon-button with a menu">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item>Delete</button>
</mat-menu>
</mat-toolbar>
<div *ngIf="editing">
<mat-form-field>
<mat-label>Mission objective</mat-label>
<input matInput [(ngModel)]="mission.objective" />
</mat-form-field>
</div>
<div *ngIf="state === requestState.ERROR">
Error occured: {{ errorMessage }}
</div>
<div *ngIf="editing">
<mat-form-field>
<mat-label>New resource</mat-label>
<textarea matInput #newResource></textarea>
</mat-form-field>
<button mat-icon-button (click)="addResource(newResource.value); newResource.value = ''">
<mat-icon>add</mat-icon>
</button>
</div>
<div *ngIf="mission && state === requestState.SUCCESS" class="main-container">
<div *ngIf="editing" class="w-full h-14 fixed bottom-0 bg-cyan-600 flex">
<button class="ml-4" mat-raised-button (click)="cancelEditing()">
Cancel
</button>
<button class="mr-4" mat-raised-button (click)="saveMission()">Save</button>
</div>
<div class="px-4">
<h1>{{ mission.name }}</h1>
<div class="pl-6 text-lg text-black">Resources</div>
<table mat-table [dataSource]="mission.resources" class="pb-6 w-full text-gray-700">
<ng-container matColumnDef="resourceName">
<th mat-header-cell *matHeaderCellDef class="text-lg text-black">
Resources
</th>
<td mat-cell *matCellDef="let resource" class="text-gray-700">
{{ resource.name }}
</td>
</ng-container>
<h4 class="!m-0">Objective</h4>
<p>{{ mission.objective }}</p>
<tr mat-header-row *matHeaderRowDef="resourceColumns"></tr>
<tr mat-row *matRowDef="let row; columns: resourceColumns"></tr>
</table>
<h4 class="!m-0">Start</h4>
<p>{{ mission.start }}</p>
<div class="pl-6 text-lg text-black">Assignments</div>
<table
mat-table
[dataSource]="mission.agentAssignments"
class="pb-6 w-full text-gray-700"
>
<ng-container matColumnDef="assignmentId">
<th mat-header-cell *matHeaderCellDef class="text-black">Agent name</th>
<td mat-cell *matCellDef="let assignment">
{{ assignment.agent.name }}
</td>
</ng-container>
<h4 class="!m-0">Duration in days</h4>
<p>{{ mission.durationInDays }}</p>
<ng-container matColumnDef="assignmentStart">
<th mat-header-cell *matHeaderCellDef class="text-black">Start</th>
<td mat-cell *matCellDef="let assignment">{{ assignment.start }}</td>
</ng-container>
<h4 class="!m-0">Country</h4>
<p>{{ mission.country?.name }}</p>
</div>
<tr mat-header-row *matHeaderRowDef="assignmentColumns"></tr>
<tr
mat-row
*matRowDef="let row; columns: assignmentColumns"
(click)="goToAssignmentDetail(row.id)"
class="hover:bg-gray-200 hover:cursor-pointer"
></tr>
</table>
<mat-list>
<div mat-subheader>Resources</div>
<mat-list-item *ngFor="let resource of mission.resources">
<mat-icon mat-list-icon>markunread_mailbox</mat-icon>
<h2 mat-line> {{ resource.name }} </h2>
</mat-list-item>
<mat-divider></mat-divider>
<div mat-subheader>Assignments</div>
<mat-list-item *ngFor="let assignment of mission.agentAssignments">
<mat-icon mat-list-icon>assignment</mat-icon>
<div mat-line>{{ assignment.agent.name }} </div>
<div mat-line>{{ assignment.start }} ({{ assignment.durationInDays }} days)</div>
</mat-list-item>
</mat-list>
<button color="accent" mat-fab routerLink="../edit/{{ mission.id }}" class="!fixed right-8 bottom-20"
aria-label="Edit mission details">
<mat-icon>edit</mat-icon>
</button>
</div>
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import { environment } from 'src/environments/environment';
import { RequestState } from '../request-state.enum';
......@@ -14,18 +14,12 @@ export class MissionDetailComponent implements OnInit {
private id: string | undefined;
requestState = RequestState;
state: RequestState = this.requestState.PENDING;
editing = false;
errorMessage = '';
mission: Mission | null = null;
private oldMission: Mission | null = null;
assignmentColumns = ['assignmentId', 'assignmentStart'];
resourceColumns = ['resourceName'];
constructor(
private httpClient: HttpClient,
private route: ActivatedRoute,
private router: Router
) {}
async ngOnInit() {
......@@ -40,57 +34,13 @@ export class MissionDetailComponent implements OnInit {
this.mission = await firstValueFrom(
this.httpClient.get<Mission>(`${environment.apiUrl}/missions/${this.id}`)
);
this.state = RequestState.SUCCESS;
} catch (err) {
this.state = RequestState.ERROR;
this.errorMessage = 'Could not load mission!';
throw err;
}
}
console.log(this.mission);
async saveMission() {
this.endEditing();
try {
this.state = RequestState.PENDING;
this.mission = await firstValueFrom(
this.httpClient.put<Mission>(
`${environment.apiUrl}/missions/${this.id}`,
this.mission
)
);
this.state = RequestState.SUCCESS;
} catch (err) {
this.state = RequestState.ERROR;
this.errorMessage = 'Could not load agent!';
throw err;
}
}
addResource(newResource: string) {
if (!newResource || newResource.length === 0) {
return;
}
if (!this.mission?.resources) {
this.mission!.resources = [];
}
this.mission!.resources.push({ name: newResource });
this.mission!.resources = [...this.mission!.resources];
}
goToAssignmentDetail(assignmentId: string) {
this.router.navigate(['assignment', assignmentId]);
}
startEditing() {
this.editing = true;
this.oldMission = JSON.parse(JSON.stringify(this.mission));
}