Compare commits

...

5 Commits

20 changed files with 278 additions and 41 deletions

View File

@ -1,2 +1,3 @@
{"_id":{"$oid":"5abf65d83fc5fa349ffd5cbb"},"playersBlufor":4,"playersOpfor":4,"budgetBlufor":4535500,"budgetOpfor":4221250,"endBudgetBlufor":997000,"endBudgetOpfor":512000,"title":"Battle #1","campaign":{"$oid":"5abd55ea9e30a76bfef747d6"},"date":{"$date":"2018-03-19T23:00:00.000Z"},"endDate":{"$date":"2018-03-20T01:30:00.000Z"},"ptBlufor":34,"ptOpfor":25,"timestamp":{"$date":"2018-03-31T10:41:28.451Z"},"updatedAt":{"$date":"2018-03-31T10:41:28.451Z"},"__v":0} {"_id":{"$oid":"5abf65d83fc5fa349ffd5cbb"},"playersBlufor":4,"playersOpfor":4,"budgetBlufor":4535500,"budgetOpfor":4221250,"endBudgetBlufor":997000,"endBudgetOpfor":512000,"title":"Battle #1","campaign":{"$oid":"5abd55ea9e30a76bfef747d6"},"date":{"$date":"2018-03-19T23:00:00.000Z"},"endDate":{"$date":"2018-03-20T01:30:00.000Z"},"ptBlufor":34,"ptOpfor":25,"timestamp":{"$date":"2018-03-31T10:41:28.451Z"},"updatedAt":{"$date":"2018-03-31T10:41:28.451Z"},"__v":0}
{"_id":{"$oid":"5abf65ae3fc5fa349ffd5ca3"},"playersBlufor":4,"playersOpfor":4,"budgetBlufor":4535500,"budgetOpfor":4221250,"endBudgetBlufor":997000,"endBudgetOpfor":512000,"title":"Test Battle #0","campaign":{"$oid":"5abd55ea9e30a76bfef747d6"},"date":{"$date":"2018-03-19T23:00:00.000Z"},"endDate":{"$date":"2018-03-20T01:30:00.000Z"},"ptBlufor":34,"ptOpfor":25,"timestamp":{"$date":"2018-03-31T10:40:46.695Z"},"updatedAt":{"$date":"2018-03-31T10:40:46.695Z"},"__v":0} {"_id":{"$oid":"5abf65ae3fc5fa349ffd5ca3"},"playersBlufor":4,"playersOpfor":4,"budgetBlufor":4535500,"budgetOpfor":4221250,"endBudgetBlufor":997000,"endBudgetOpfor":512000,"title":"Test Battle #0","campaign":{"$oid":"5abd55ea9e30a76bfef747d6"},"date":{"$date":"2018-03-19T23:00:00.000Z"},"endDate":{"$date":"2018-03-20T01:30:00.000Z"},"ptBlufor":34,"ptOpfor":25,"timestamp":{"$date":"2018-03-31T10:40:46.695Z"},"updatedAt":{"$date":"2018-03-31T10:40:46.695Z"},"__v":0}
{"_id":{"$oid":"5abf65ae2df5fa349ffd5ca3"},"playersBlufor":20,"playersOpfor":20,"budgetBlufor":4535500,"budgetOpfor":4535500,"endBudgetBlufor":0,"endBudgetOpfor":20000,"title":"Test Battle XY","campaign":{"$oid":"5abd55ea9e30a76bfef747d6"},"date":{"$date":"2018-03-02T23:00:00.000Z"},"endDate":{"$date":"2018-03-02T01:30:00.000Z"},"ptBlufor":34,"ptOpfor":25,"timestamp":{"$date":"2018-03-02T10:40:46.695Z"},"updatedAt":{"$date":"2018-03-02T10:40:46.695Z"},"__v":0}

View File

@ -76,6 +76,35 @@ Create a new war
+ Attributes (War, fixed-type) + Attributes (War, fixed-type)
### Update War [PATCH /wars/{id}]
Update a war, identified by its id
**Permission: 3**
+ Parameters
+ id: `5abf65ae2df5fa349ffd5ca3` (string, required) - unique id of campaign
+ Request (application/json)
+ Attributes
+ _id: `5abf65ae2df5fa349ffd5ca3` (string, required) - unique id of war
+ title: `Final Touchdown` (string, optional) - display name of the war
+ date: `2017-05-11T20:00:00.471Z` (string, optional) - starting date
+ endDate: `2017-05-12T00:30:32.471Z` (string, optional) - end date
+ ptBlufor: 5 (number, optional) - points Blufor
+ ptOpfor: 5 (number, optional) - points Opfor
+ playersBlufor: 20 (number, optional) - number of players Blufor
+ playersOpfor: 20 (number, optional) - number of players Opfor
+ budgetBlufor: 3000000 (number, optional) - start budget Blufor
+ budgetOpfor: 3000000 (number, optional) - start budget Opfor
+ endBudgetBlufor: 10000 (number, optional) - end budget Blufor
+ endBudgetOpfor: 12000 (number, optional) - end budget Opfor
+ Response 200 (application/json; charset=utf-8)
+ Attributes (War, fixed-type)
### Delete War [DELETE /wars/{id}] ### Delete War [DELETE /wars/{id}]
Delete a war Delete a war

View File

@ -168,6 +168,37 @@ wars.route('/:id')
}); });
}) })
.patch(apiAuthenticationMiddleware, checkMT, (req, res, next) => {
if (!req.body || (req.body._id && req.body._id !== req.params.id)) {
// little bit different as in PUT. :id does not need to be in data, but if the _id and url id must match
const err = new Error('id of PATCH resource and send JSON body are not equal ' + req.params.id + ' ' +
req.body._id);
err.status = codes.notfound;
next(err);
return; // prevent node to process this function further after next() has finished.
}
req.body.updatedAt = new Date();
req.body.$inc = {__v: 1};
if (req.body.hasOwnProperty('__v')) {
delete req.body.__v;
}
// PATCH is easier with mongoose than PUT. You simply update by all data that comes from outside. no need to
// reset attributes that are missing.
WarModel.findByIdAndUpdate(req.params.id, req.body, {new: true}, (err, item) => {
if (err) {
err.status = codes.wrongrequest;
} else if (!item) {
err = new Error('item not found');
err.status = codes.notfound;
} else {
res.locals.items = item;
}
next(err);
});
})
.delete(apiAuthenticationMiddleware, checkMT, (req, res, next) => { .delete(apiAuthenticationMiddleware, checkMT, (req, res, next) => {
WarModel.findByIdAndRemove(req.params.id, (err, item) => { WarModel.findByIdAndRemove(req.params.id, (err, item) => {
if (err) { if (err) {

View File

@ -1,7 +1,7 @@
import {RouterModule, Routes} from '@angular/router'; import {RouterModule, Routes} from '@angular/router';
import {LoginComponent} from './login/index'; import {LoginComponent, LoginGuardMT} from './login';
import {NotFoundComponent} from './common/not-found/not-found.component'; import {NotFoundComponent} from './common/not-found';
import {LoginGuardAdmin, LoginGuardHL} from './login/login.guard'; import {LoginGuardAdmin, LoginGuardHL} from './login';
import {armyRoutes, armyRoutingComponents} from './army/army.routing'; import {armyRoutes, armyRoutingComponents} from './army/army.routing';
import {SignupComponent} from './login/signup.component'; import {SignupComponent} from './login/signup.component';
import {RouteConfig} from './app.config'; import {RouteConfig} from './app.config';
@ -72,4 +72,4 @@ export const appRouting = RouterModule.forRoot(appRoutes);
export const routingComponents = [...armyRoutingComponents, LoginComponent, SignupComponent, NotFoundComponent]; export const routingComponents = [...armyRoutingComponents, LoginComponent, SignupComponent, NotFoundComponent];
export const routingProviders = [LoginGuardHL, LoginGuardAdmin]; export const routingProviders = [LoginGuardHL, LoginGuardMT, LoginGuardAdmin];

View File

@ -38,5 +38,9 @@ export class WarService {
.map(res => res.json()); .map(res => res.json());
} }
updateWar(war: War) {
return this.http.patch(this.config.apiWarPath + '/' + war._id, war)
.map(res => res.json());
}
} }

View File

@ -26,7 +26,6 @@ export class CampaignSubmitComponent {
constructor(private route: ActivatedRoute, constructor(private route: ActivatedRoute,
private router: Router, private router: Router,
private campaignService: CampaignService) { private campaignService: CampaignService) {
this.subscription = this.route.params this.subscription = this.route.params
.map(params => params['id']) .map(params => params['id'])
.filter(id => id !== undefined) .filter(id => id !== undefined)
@ -55,5 +54,4 @@ export class CampaignSubmitComponent {
this.router.navigate(['..'], {relativeTo: this.route}); this.router.navigate(['..'], {relativeTo: this.route});
return false; return false;
} }
} }

View File

@ -6,11 +6,13 @@ import {WarItemComponent} from './war/war-list/war-item.component';
import {ModuleWithProviders} from '@angular/core'; import {ModuleWithProviders} from '@angular/core';
import {CampaignSubmitComponent} from './campaign/campaign-submit/campaign-submit.component'; import {CampaignSubmitComponent} from './campaign/campaign-submit/campaign-submit.component';
import {CampaignPlayerDetailComponent} from './campaign/campaign-player-detail/campaign-player-detail.component'; import {CampaignPlayerDetailComponent} from './campaign/campaign-player-detail/campaign-player-detail.component';
import {WarDetailComponent} from './war/war-detail/war-detail.component';
import {ScoreboardComponent} from './war/scoreboard/scoreboard.component'; import {ScoreboardComponent} from './war/scoreboard/scoreboard.component';
import {WarSubmitComponent} from './war-submit/war-submit.component'; import {WarSubmitComponent} from './war/war-submit/war-submit.component';
import {FractionStatsComponent} from './war/fraction-stats/fraction-stats.component'; import {FractionStatsComponent} from './war/fraction-stats/fraction-stats.component';
import {StatisticHighScoreComponent} from './campaign/highscore/highscore.component'; import {StatisticHighScoreComponent} from './campaign/highscore/highscore.component';
import {WarHeaderComponent} from './war/war-header/war-header.component';
import {WarEditComponent} from './war/war-edit/war-edit.component';
import {LoginGuardMT} from '../login';
export const statsRoutes: Routes = [{ export const statsRoutes: Routes = [{
@ -32,35 +34,44 @@ export const statsRoutes: Routes = [{
component: StatisticHighScoreComponent, component: StatisticHighScoreComponent,
outlet: 'right' outlet: 'right'
}, },
{
path: 'campaign',
component: CampaignSubmitComponent,
outlet: 'right'
},
{
path: 'campaign/:id',
component: CampaignSubmitComponent,
outlet: 'right'
},
{
path: 'new',
component: WarSubmitComponent,
outlet: 'right'
},
{ {
path: 'war/:id', path: 'war/:id',
component: WarDetailComponent, component: WarHeaderComponent,
outlet: 'right' outlet: 'right'
}, },
{ {
path: 'campaign-player/:id/:playerName', path: 'campaign-player/:id/:playerName',
component: CampaignPlayerDetailComponent, component: CampaignPlayerDetailComponent,
outlet: 'right' outlet: 'right'
},
{
path: 'campaign',
component: CampaignSubmitComponent,
outlet: 'right',
canActivate: [LoginGuardMT]
},
{
path: 'campaign/:id',
component: CampaignSubmitComponent,
outlet: 'right',
canActivate: [LoginGuardMT]
},
{
path: 'submit-war',
component: WarSubmitComponent,
outlet: 'right',
canActivate: [LoginGuardMT]
},
{
path: 'submit-war/:id',
component: WarEditComponent,
outlet: 'right',
canActivate: [LoginGuardMT]
}]; }];
export const statsRouterModule: ModuleWithProviders = RouterModule.forChild(statsRoutes); export const statsRouterModule: ModuleWithProviders = RouterModule.forChild(statsRoutes);
export const statsRoutingComponents = [StatisticComponent, StatisticOverviewComponent, StatisticHighScoreComponent, export const statsRoutingComponents = [StatisticComponent, StatisticOverviewComponent, StatisticHighScoreComponent,
CampaignSubmitComponent, WarListComponent, WarSubmitComponent, WarDetailComponent, ScoreboardComponent, CampaignSubmitComponent, WarListComponent, WarSubmitComponent, WarEditComponent, WarHeaderComponent,
FractionStatsComponent, CampaignPlayerDetailComponent, WarItemComponent]; ScoreboardComponent, FractionStatsComponent, CampaignPlayerDetailComponent, WarItemComponent];

View File

@ -0,0 +1,88 @@
<form #form="ngForm" class="overview">
<h3>Schlacht bearbeiten</h3>
<div class="form-group">
<label for="title">Titel</label>
<input type="text" class="form-control"
[(ngModel)]="war.title"
name="title"
id="title"
required maxlength="50"/>
<show-error displayName="Name" controlPath="title"></show-error>
</div>
<div class="form-group">
<label for="campaign">Kampagne</label>
<select class="form-control"
name="campaign"
id="campaign"
[(ngModel)]="war.campaign"
required>
<option *ngFor="let campaign of campaignService.campaigns" [ngValue]="campaign._id">
{{campaign.title}}
</option>
</select>
</div>
<div class="form-group">
<label for="ptBlufor">Punkte NATO</label>
<input type="number" class="form-control"
[(ngModel)]="war.ptBlufor"
name="ptBlufor"
id="ptBlufor"
required min="0"/>
<show-error displayName="Name" controlPath="ptBlufor"></show-error>
</div>
<div class="form-group">
<label for="ptOpfor">Punkte CSAT</label>
<input type="number" class="form-control"
[(ngModel)]="war.ptOpfor"
name="ptOpfor"
id="ptOpfor"
required min="0"/>
<show-error displayName="Name" controlPath="ptOpfor"></show-error>
</div>
<div class="form-group">
<label for="endBudgetBlufor">Endbudget NATO</label>
<input type="number" class="form-control"
[(ngModel)]="war.endBudgetBlufor"
name="endBudgetBlufor"
id="endBudgetBlufor"
required />
<show-error displayName="Name" controlPath="endBudgetBlufor"></show-error>
</div>
<div class="form-group">
<label for="endBudgetOpfor">Endbudget CSAT</label>
<input type="number" class="form-control"
[(ngModel)]="war.endBudgetOpfor"
name="endBudgetOpfor"
id="endBudgetOpfor"
required />
<show-error displayName="Name" controlPath="endBudgetOpfor"></show-error>
</div>
<button id="cancel"
*ngIf="!loading"
(click)="cancel()"
class="btn btn-default">
Abbrechen
</button>
<button id="save"
*ngIf="!loading"
(click)="updateWar()"
class="btn btn-default"
[disabled]="!form.valid">
Bestätigen
</button>
<span *ngIf="loading" class="load-indicator load-arrow glyphicon-refresh-animate"></span>
<span *ngIf="showErrorLabel"
class="center-block label label-danger" style="font-size: medium; padding: 2px; margin-top: 2px">
{{error}}
</span>
</form>

View File

@ -0,0 +1,58 @@
import {Component, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {NgForm} from '@angular/forms';
import {WarService} from '../../../services/logs/war.service';
import {War} from '../../../models/model-interfaces';
import {CampaignService} from '../../../services/logs/campaign.service';
import {Subscription} from 'rxjs/Subscription';
@Component({
selector: 'war-edit',
templateUrl: './war-edit.component.html',
styleUrls: ['./war-edit.component.css', '../../../style/load-indicator.css',
'../../../style/entry-form.css', '../../../style/overview.css']
})
export class WarEditComponent {
war: War = {players: []};
subscription: Subscription;
showFileError = false;
showErrorLabel = false;
error;
@ViewChild(NgForm) form: NgForm;
constructor(private route: ActivatedRoute,
private router: Router,
private warService: WarService,
private campaignService: CampaignService) {
this.subscription = this.route.params
.map(params => params['id'])
.filter(id => id !== undefined)
.flatMap(id => this.warService.getWar(id))
.subscribe(war => {
this.war = war;
});
}
updateWar() {
this.warService.updateWar(this.war)
.subscribe(war => {
this.router.navigate(['../../war/' + war._id], {relativeTo: this.route});
},
error => {
this.error = error._body.error.message;
this.showErrorLabel = true;
});
}
cancel() {
this.router.navigate(['..'], {relativeTo: this.route});
return false;
}
}

View File

@ -10,10 +10,10 @@ import {ScoreboardComponent} from '../scoreboard/scoreboard.component';
@Component({ @Component({
selector: 'war-detail', selector: 'war-detail',
templateUrl: './war-detail.component.html', templateUrl: './war-header.component.html',
styleUrls: ['./war-detail.component.css', '../../../style/list-entry.css', '../../../style/hide-scrollbar.css'] styleUrls: ['./war-header.component.css', '../../../style/list-entry.css', '../../../style/hide-scrollbar.css']
}) })
export class WarDetailComponent implements OnInit { export class WarHeaderComponent implements OnInit {
readonly fraction = Fraction; readonly fraction = Fraction;

View File

@ -13,6 +13,10 @@
<span (click)="delete(); $event.stopPropagation()" <span (click)="delete(); $event.stopPropagation()"
title="Löschen" title="Löschen"
class="glyphicon glyphicon-trash trash"></span> class="glyphicon glyphicon-trash trash"></span>
<span (click)="edit(); $event.stopPropagation()"
style="padding-right: 10px;"
title="Bearbeiten"
class="glyphicon glyphicon-edit trash"></span>
</div> </div>
</div> </div>

View File

@ -16,6 +16,8 @@ export class WarItemComponent implements OnInit {
@Output() warSelected = new EventEmitter(); @Output() warSelected = new EventEmitter();
@Output() warEdit = new EventEmitter();
@Output() warDelete = new EventEmitter(); @Output() warDelete = new EventEmitter();
constructor(public loginService: LoginService) { constructor(public loginService: LoginService) {
@ -28,6 +30,10 @@ export class WarItemComponent implements OnInit {
this.warSelected.emit(this.war._id); this.warSelected.emit(this.war._id);
} }
edit() {
this.warEdit.emit(this.war._id);
}
delete() { delete() {
this.warDelete.emit(this.war); this.warDelete.emit(this.war);
} }

View File

@ -47,6 +47,7 @@
<div *ngFor="let war of campaign.wars"> <div *ngFor="let war of campaign.wars">
<pjm-war-item <pjm-war-item
[war]="war" [war]="war"
(warEdit)="editWar($event)"
(warDelete)="deleteWar(war)" (warDelete)="deleteWar(war)"
(warSelected)="selectWar($event)" (warSelected)="selectWar($event)"
[selected]="war._id == selectedWarId"> [selected]="war._id == selectedWarId">

View File

@ -49,11 +49,6 @@ export class WarListComponent implements OnInit {
this.router.navigate([{outlets: {'right': ['campaign']}}], {relativeTo: this.route}); this.router.navigate([{outlets: {'right': ['campaign']}}], {relativeTo: this.route});
} }
selectNewWar() {
this.selectedWarId = null;
this.router.navigate([{outlets: {'right': ['new']}}], {relativeTo: this.route});
}
selectWar(warId) { selectWar(warId) {
if (this.selectedWarId !== warId) { if (this.selectedWarId !== warId) {
this.selectedWarId = warId; this.selectedWarId = warId;
@ -75,6 +70,16 @@ export class WarListComponent implements OnInit {
} }
} }
selectNewWar() {
this.selectedWarId = null;
this.router.navigate([{outlets: {'right': ['submit-war']}}], {relativeTo: this.route});
}
editWar(warId) {
this.selectedWarId = warId;
this.router.navigate([{outlets: {'right': ['submit-war', warId]}}], {relativeTo: this.route});
}
deleteWar(war: War) { deleteWar(war: War) {
if (confirm('Soll die Schlacht ' + war.title + ' wirklich gelöscht werden?')) { if (confirm('Soll die Schlacht ' + war.title + ' wirklich gelöscht werden?')) {
this.warService.deleteWar(war._id) this.warService.deleteWar(war._id)
@ -82,7 +87,9 @@ export class WarListComponent implements OnInit {
if (this.selectedWarId === war._id) { if (this.selectedWarId === war._id) {
this.selectOverview('all'); this.selectOverview('all');
} }
this.campaigns.splice(this.campaigns.indexOf(war), 1); this.campaigns.forEach(campaign => {
campaign.wars.splice(campaign.wars.indexOf(war), 1);
});
}); });
} }
} }

View File

@ -1,16 +1,16 @@
import {Component, ViewChild} from '@angular/core'; import {Component, ViewChild} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {NgForm} from '@angular/forms'; import {NgForm} from '@angular/forms';
import {WarService} from '../../services/logs/war.service'; import {WarService} from '../../../services/logs/war.service';
import {War} from '../../models/model-interfaces'; import {War} from '../../../models/model-interfaces';
import {CampaignService} from '../../services/logs/campaign.service'; import {CampaignService} from '../../../services/logs/campaign.service';
@Component({ @Component({
selector: 'war-submit', selector: 'war-submit',
templateUrl: './war-submit.component.html', templateUrl: './war-submit.component.html',
styleUrls: ['./war-submit.component.css', '../../style/load-indicator.css', styleUrls: ['./war-submit.component.css', '../../../style/load-indicator.css',
'../../style/entry-form.css', '../../style/overview.css'] '../../../style/entry-form.css', '../../../style/overview.css']
}) })
export class WarSubmitComponent { export class WarSubmitComponent {
@ -70,5 +70,4 @@ export class WarSubmitComponent {
this.router.navigate(['..'], {relativeTo: this.route}); this.router.navigate(['..'], {relativeTo: this.route});
return false; return false;
} }
} }