Compare commits

..

2 Commits

Author SHA1 Message Date
Florian Hartwich ec756028aa Add editing/creating of ranks 2017-05-15 15:32:36 +02:00
Florian Hartwich 4c6f3dab78 Add possibility to remove ranks, catch error use-cases in signature creation and army-overview 2017-05-15 13:44:37 +02:00
19 changed files with 323 additions and 203 deletions

View File

@ -48,12 +48,16 @@ overview.route('/')
}, {sort: {rankLvl: 'desc', name: 'asc'}}, (err, users) => { }, {sort: {rankLvl: 'desc', name: 'asc'}}, (err, users) => {
const squadMembers = []; const squadMembers = [];
async.eachSeries(users, (user, callback) => { async.eachSeries(users, (user, callback) => {
const usr = user.toObject();
RankModel.findOne({level: user.rankLvl, fraction: squad.fraction}, (err, rank) => { RankModel.findOne({level: user.rankLvl, fraction: squad.fraction}, (err, rank) => {
if (err) { if (err) {
return next(err); return next(err);
} }
const usr = user.toObject();
// not defined if rank was deleted / rankLvl not available for fraction
if (rank) {
usr.rank = rank.name; usr.rank = rank.name;
}
delete usr.rankLvl; delete usr.rankLvl;
squadMembers.push(usr) squadMembers.push(usr)

View File

@ -71,6 +71,7 @@ let createSignature = (userId, res, next) => {
return next(err) return next(err)
} }
if (result) {
if (user.squadId.fraction === 'BLUFOR') { if (user.squadId.fraction === 'BLUFOR') {
rankW = 25; rankW = 25;
rankH = 60; rankH = 60;
@ -93,6 +94,11 @@ let createSignature = (userId, res, next) => {
.then(() => { .then(() => {
addDecorationsAndSave(userId, loadedImage, res, next); addDecorationsAndSave(userId, loadedImage, res, next);
}) })
} else {
// user has not any assignable rank in his fraction at this point,
// e.g. assigned rank has been deleted or switched fraction so rankLvl is not defined
addDecorationsAndSave(userId, loadedImage, res, next);
}
}) })
}) })
.catch((err) => { .catch((err) => {

View File

@ -0,0 +1,3 @@
.form-control {
height: auto;
}

View File

@ -7,6 +7,12 @@ div.rank-list-entry, a.rank-list-entry {
margin-bottom: -1px; margin-bottom: -1px;
} }
.rank-list-preview {
height: 54px;
float: left;
margin-right: 12px;
}
.marked { .marked {
background: lightgrey; background: lightgrey;
} }
@ -25,9 +31,9 @@ small {
} }
.trash { .trash {
float:right;
padding-top: 18px; padding-top: 18px;
font-size: 17px; font-size: 17px;
margin-left: -10px;
} }
.selected { .selected {

View File

@ -1,7 +1,7 @@
<div class="fade-in rank-list-entry" [ngClass]="{selected : selected}" (click)="select()"> <div class="fade-in rank-list-entry" [ngClass]="{selected : selected}" (click)="select()">
<div class="row"> <div class="row">
<div class="col-xs-11"> <div class="col-xs-9">
<span> <span>
<a>{{rank.name}}</a> <a>{{rank.name}}</a>
</span> </span>
@ -10,5 +10,12 @@
<small *ngIf="rank.fraction == 'BLUFOR'">NATO</small> <small *ngIf="rank.fraction == 'BLUFOR'">NATO</small>
<small> - Stufe {{rank.level}}</small> <small> - Stufe {{rank.level}}</small>
</div> </div>
<div class="col-xs-3">
<img src="{{imageSrc}}" class="rank-list-preview">
<span (click)="delete(); $event.stopPropagation()" title="Löschen" class="glyphicon glyphicon-trash trash"></span>
</div>
</div> </div>
</div> </div>

View File

@ -14,6 +14,7 @@ export class RankItemComponent {
selected: boolean; selected: boolean;
rank: Rank; rank: Rank;
imageSrc;
rankSelected = new EventEmitter(); rankSelected = new EventEmitter();
rankDelete = new EventEmitter(); rankDelete = new EventEmitter();
@ -22,12 +23,16 @@ export class RankItemComponent {
} }
ngOnInit() {
this.imageSrc = 'resource/rank/' + this.rank._id + '.png?' + Date.now();
}
select() { select() {
this.rankSelected.emit(this.rank._id) this.rankSelected.emit(this.rank._id)
} }
delete() { delete() {
this.rankSelected.emit(this.rank); this.rankDelete.emit(this.rank);
} }
ngAfterViewChecked() { ngAfterViewChecked() {

View File

@ -21,6 +21,9 @@
(change)="filterRanksByFraction(query.value, fractRadioOpfor.value)">CSAT (change)="filterRanksByFraction(query.value, fractRadioOpfor.value)">CSAT
</label> </label>
</form> </form>
<a class="pull-right btn btn-success" (click)="openNewRankForm()">
Neuen Rang hinzufügen
</a>
</div> </div>
<div class="input-group search-bar" style="padding-top: 0;"> <div class="input-group search-bar" style="padding-top: 0;">

View File

@ -48,18 +48,28 @@ export class RankListComponent implements OnInit {
} }
openNewRankForm() { openNewRankForm() {
this.selectedRankId = null;
this.router.navigate([{outlets: {'right': ['new']}}], {relativeTo: this.route}); this.router.navigate([{outlets: {'right': ['new']}}], {relativeTo: this.route});
} }
selectRank(rankId: string | number) { selectRank(rankId: string | number) {
this.selectedRankId = rankId; this.selectedRankId = rankId;
this.router.navigate([{outlets: {'right': ['overview', rankId]}}], {relativeTo: this.route}); this.router.navigate([{outlets: {'right': ['new', rankId]}}], {relativeTo: this.route});
} }
filterRanksByFraction(query = '', fractionFilter) { filterRanksByFraction(query = '', fractionFilter) {
this.ranks$ = this.rankService.findRanks(query, fractionFilter); this.ranks$ = this.rankService.findRanks(query, fractionFilter);
} }
deleteRank(rank) {
const fraction = rank.fraction === 'OPFOR' ? 'CSAT' : 'NATO';
if (confirm('Soll der Rang ' + rank.name + ' (' + fraction + ') wirklich gelöscht werden?')) {
this.rankService.deleteRank(rank)
.subscribe((res) => {
})
}
}
adjustBrowserUrl(queryString = '') { adjustBrowserUrl(queryString = '') {
const absoluteUrl = this.location.path().split('?')[0]; const absoluteUrl = this.location.path().split('?')[0];
const queryPart = queryString !== '' ? `query=${queryString}` : ''; const queryPart = queryString !== '' ? `query=${queryString}` : '';

View File

@ -0,0 +1,7 @@
.preview-image {
margin: 10px;
}
.form-control {
height: auto;
}

View File

@ -0,0 +1,66 @@
<form #form="ngForm" class="overview">
<h3 *ngIf="rank._id">Rang editieren</h3>
<h3 *ngIf="!rank._id">Neuen Rang hinzufügen</h3>
<div class="form-group">
<label for="title">Name</label>
<input type="text" class="form-control"
[(ngModel)]="rank.name"
name="title"
id="title"
required maxlength="50"/>
<show-error text="Name" path="title"></show-error>
</div>
<div class="form-group">
<label for="fraction">Fraktion</label>
<select id="fraction" name="fraction" class="form-control btn dropdown-toggle"
required
[(ngModel)]="rank.fraction">
<option value="OPFOR">CSAT</option>
<option value="BLUFOR">NATO</option>
</select>
<show-error text="Fraktion" path="fraction"></show-error>
</div>
<div class="form-group">
<label for="level">Stufe</label>
<input id="level" name="level" type="number" class="form-control btn dropdown-toggle"
[(ngModel)]="rank.level">
<show-error text="Stufe" path="level"></show-error>
</div>
<div class="form-group">
<label for="logo">Bild</label>
<input id="logo" name="logo" class="ui-button form-control" type="file"
accept="image/png"
#fileInput
(change)="fileChange($event)">
<span class="label label-bg label-danger center-block" style="font-size:small" *ngIf="showImageError">
Bild muss im PNG Format vorliegen
</span>
<img class="preview-image" src="{{imagePreviewSrc}}">
</div>
<button id="cancel"
(click)="cancel()"
class="btn btn-default">
Abbrechen
</button>
<button id="save"
(click)="saveRank(fileInput)"
class="btn btn-default"
[disabled]="!form.valid">
Squad speichern
</button>
<span *ngIf="showSuccessLabel"
class="label label-success label-small"
style="margin-left: inherit">
Erfolgreich gespeichert
</span>
</form>

View File

@ -0,0 +1,105 @@
import {Component, ViewChild} from "@angular/core";
import {ActivatedRoute, Router} from "@angular/router";
import {NgForm} from "@angular/forms";
import {Rank} from "../../models/model-interfaces";
import {RankService} from "../../services/rank-service/rank.service";
import {Subscription} from "rxjs/Subscription";
@Component({
templateUrl: './new-rank.component.html',
styleUrls: ['./new-rank.component.css', '../../style/new-entry-form.css']
})
export class CreateRankComponent {
subscription: Subscription;
rank: Rank = {name: '', fraction: '', level: 0};
fileList: FileList;
saved = false;
showImageError = false;
imagePreviewSrc;
showSuccessLabel = false;
@ViewChild(NgForm) form: NgForm;
constructor(private route: ActivatedRoute,
private router: Router,
private rankService : RankService) {
}
ngOnInit() {
this.subscription = this.route.params
.map(params => params['id'])
.filter(id => id != undefined)
.flatMap(id => this.rankService.getRank(id))
.subscribe(rank => {
this.rank = rank;
this.imagePreviewSrc = 'resource/rank/' + this.rank._id + '.png?' + Date.now();
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
fileChange(event) {
if (!event.target.files[0].name.endsWith('.png')) {
this.showImageError = true;
this.fileList = undefined;
} else {
this.showImageError = false;
this.fileList = event.target.files;
}
}
saveRank(fileInput) {
let file: File;
if (!this.rank._id) {
if (this.fileList) {
file = this.fileList[0];
this.rankService.submitRank(this.rank, file)
.subscribe(rank => {
this.saved = true;
this.router.navigate(['..'], {relativeTo: this.route});
})
} else {
return window.alert(`Bild ist ein Pflichtfeld`);
}
} else {
if (this.fileList) {
file = this.fileList[0];
}
delete this.rank['__v'];
this.rankService.submitRank(this.rank, file)
.subscribe(rank => {
setTimeout(() => {
this.imagePreviewSrc = 'resource/rank/' + this.rank._id + '.png?' + Date.now();
}, 300);
fileInput.value = '';
this.showSuccessLabel = true;
setTimeout(() => {
this.showSuccessLabel = false;
}, 2000)
})
}
}
cancel() {
this.router.navigate([this.rank._id ? '../..' : '..'], {relativeTo: this.route});
return false;
}
canDeactivate(): boolean {
if (this.saved || !this.form.dirty) {
return true;
}
return window.confirm(`Ihr Formular besitzt ungespeicherte Änderungen, möchten Sie die Seite wirklich verlassen?`);
}
}

View File

@ -1,79 +0,0 @@
<div class="overview">
<h3 style="margin-bottom: 25px">Rang-Details
<span *ngIf="showSuccessLabel"
class="label label-success label-small"
style="margin-left: inherit">
Erfolgreich gespeichert
</span>
</h3>
<div *ngIf="rank">
<div class="col-xs-12">
<div class="div-table">
<div class="div-table-row">
<div class="div-table-col content-s">
<label>Name:</label>
</div>
<div class="div-table-col content content-m">
{{rank.name}}
</div>
<div class="div-table-col content-l">
<input class="form-control" width="250px" placeholder="Neuer Name" #newNameInput>
</div>
<div class="div-table-col content-s">
<a class="pull-right btn btn-sm btn-block btn-default" (click)="update('name', newNameInput)">Bestätigen</a>
</div>
</div>
<div class="div-table-row">
<div class="div-table-col content-s">
<label>Fraktion:</label>
</div>
<div class="div-table-col fraction-opfor content-m" *ngIf="rank.fraction == 'OPFOR'">
CSAT
</div>
<div class="div-table-col fraction-blufor content-m" *ngIf="rank.fraction == 'BLUFOR'">
NATO
</div>
<div class="div-table-col">
</div>
</div>
<div class="div-table-row">
<div class="div-table-col content-s">
<label>Stufe:</label>
</div>
<div class="div-table-col content content-m">
{{rank.level}}
</div>
<div class="div-table-col">
</div>
</div>
</div>
<hr>
<div class="div-table">
<div class="div-table-row">
<div class="div-table-col content-s">
<label>Bild:</label>
</div>
<div class="div-table-col content-m-flex">
<img src="{{imagePreview}}">
</div>
<div class="div-table-col content-l">
<label class="control-label">Neues Logo</label>
<input class="form-control" type="file" accept="image/png" #newGraphicInput (change)="fileChange($event)">
<span class="label label-bg label-danger center-block" style="font-size:small" *ngIf="showImageError">
Bild muss im PNG Format vorliegen
</span>
</div>
<div class="div-table-col content-s">
<label>&nbsp;</label>
<a class="pull-right btn btn-sm btn-block btn-default" (click)="updateGraphic(newGraphicInput)">Bestätigen</a>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,86 +0,0 @@
import {Component} from "@angular/core";
import {ActivatedRoute} from "@angular/router";
import {Rank} from "../../models/model-interfaces";
import {RankService} from "../../services/rank-service/rank.service";
@Component({
templateUrl: './rank-overview.component.html',
styleUrls: ['./rank-overview.component.css', '../../style/overview.css'],
})
export class RankOverviewComponent {
showSuccessLabel = false;
showImageError = false;
rank: Rank;
fileList: FileList;
imagePreview;
constructor(private route: ActivatedRoute,
private rankService: RankService) {
}
ngOnInit() {
this.route.params.subscribe((params) => {
this.rankService.getRank(params['id']).subscribe(rank => {
this.rank = rank;
this.imagePreview = 'resource/rank/' + rank._id + '.png?' + Date.now();
})
})
}
/**
* register change on file input and save to local fileList
* @param event
*/
fileChange(event) {
if (!event.target.files[0].name.endsWith('.png')) {
this.showImageError = true;
this.fileList = undefined;
} else {
this.showImageError = false;
this.fileList = event.target.files;
}
}
update(attrName, inputField) {
const inputValue = inputField.value;
if (inputValue.length > 0 && this.rank[attrName] !== inputValue) {
const updateObject = {_id: this.rank._id};
updateObject[attrName] = inputValue;
this.rankService.updateRank(updateObject)
.subscribe(rank => {
this.rank = rank;
inputField.value = '';
this.showSuccessLabel = true;
setTimeout(() => {
this.showSuccessLabel = false;
}, 2000)
})
}
}
updateGraphic(fileInput) {
if (this.fileList && this.fileList.length > 0) {
let file: File = this.fileList[0];
this.rankService.updateRankGraphic(this.rank._id, file)
.subscribe((res) => {
setTimeout(() => {
this.imagePreview = 'resource/rank/' + this.rank._id + '.png?' + Date.now();
}, 300);
fileInput.value = '';
this.showSuccessLabel = true;
setTimeout(() => {
this.showSuccessLabel = false;
}, 2000)
})
}
}
}

View File

@ -1,7 +1,7 @@
import {Routes} from "@angular/router"; import {Routes} from "@angular/router";
import {RankComponent} from "./ranks.component"; import {RankComponent} from "./ranks.component";
import {RankListComponent} from "./rank-list/rank-list.component"; import {RankListComponent} from "./rank-list/rank-list.component";
import {RankOverviewComponent} from "./rank-overview/rank-overview.component"; import {CreateRankComponent} from "./rank-new/new-rank.component";
export const ranksRoutes: Routes = [{ export const ranksRoutes: Routes = [{
@ -14,10 +14,15 @@ export const ranksRoutes: Routes = [{
] ]
}, },
{ {
path: 'overview/:id', path: 'new',
component: RankOverviewComponent, component: CreateRankComponent,
outlet: 'right'
},
{
path: 'new/:id',
component: CreateRankComponent,
outlet: 'right' outlet: 'right'
}]; }];
export const ranksRoutingComponents = [RankComponent, RankListComponent, RankOverviewComponent]; export const ranksRoutingComponents = [RankComponent, RankListComponent, CreateRankComponent];

View File

@ -3,7 +3,7 @@ import {Decoration, Rank} from "../../models/model-interfaces";
import {RequestMethod, RequestOptions, URLSearchParams} from "@angular/http"; import {RequestMethod, RequestOptions, URLSearchParams} from "@angular/http";
import {Observable} from "rxjs/Observable"; import {Observable} from "rxjs/Observable";
import {LOAD} from "../stores/decoration.store"; import {LOAD} from "../stores/decoration.store";
import {EDIT, RankStore} from "../stores/rank.store"; import {ADD, EDIT, RankStore, REMOVE} from "../stores/rank.store";
import {AppConfig} from "../../app.config"; import {AppConfig} from "../../app.config";
import {HttpClient} from "../http-client"; import {HttpClient} from "../http-client";
@ -29,8 +29,8 @@ export class RankService {
this.http.get(this.config.apiUrl + this.config.apiRankPath, searchParams) this.http.get(this.config.apiUrl + this.config.apiRankPath, searchParams)
.map(res => res.json()) .map(res => res.json())
.do((squads) => { .do((ranks) => {
this.rankStore.dispatch({type: LOAD, data: squads}); this.rankStore.dispatch({type: LOAD, data: ranks});
}).subscribe(_ => { }).subscribe(_ => {
}); });
@ -42,6 +42,54 @@ export class RankService {
.map(res => res.json()); .map(res => res.json());
} }
/**
* For creating new data with POST or
* update existing with patch PATCH
*/
submitRank(rank: Rank, imageFile?) {
let requestUrl = this.config.apiUrl + this.config.apiRankPath;
let requestMethod: RequestMethod;
let accessType;
let body;
if (rank._id) {
requestUrl += rank._id;
requestMethod = RequestMethod.Patch;
accessType = EDIT;
} else {
requestMethod = RequestMethod.Post;
accessType = ADD;
}
if (imageFile) {
body = new FormData();
Object.keys(rank).map((objectKey) => {
if (rank[objectKey] !== undefined) {
body.append(objectKey, rank[objectKey]);
}
});
body.append('image', imageFile, imageFile.name);
} else {
body = rank;
}
const options = new RequestOptions({
body: body,
method: requestMethod
});
return this.http.request(requestUrl, options)
.map(res => res.json())
.do(savedRank => {
const action = {type: accessType, data: savedRank};
// leave some time to save image file before accessing it through listview
setTimeout(() => {
this.rankStore.dispatch(action);
}, 300);
});
}
/** /**
* send PATCH request to update db entry * send PATCH request to update db entry
* *
@ -84,6 +132,13 @@ export class RankService {
}); });
} }
deleteRank(rank: Rank) {
return this.http.delete(this.config.apiUrl + this.config.apiRankPath + rank._id)
.do(res => {
this.rankStore.dispatch({type: REMOVE, data: rank});
});
}
} }

View File

@ -0,0 +1,3 @@
.form-control {
height: auto;
}

View File

@ -66,11 +66,11 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th class="col-sm-1" style="width: 60px;">Bild</th> <th class="col-sm-1">Bild</th>
<th class="col-sm-2">Bezeichnung</th> <th class="col-sm-2">Bezeichnung</th>
<th class="col-sm-2">Begründung</th> <th class="col-sm-2">Begründung</th>
<th class="col-sm-1 text-right" style="width: 65px;">Datum</th> <th class="col-sm-1 text-right">Datum</th>
<th class="col-sm-1 text-center" style="width: 45px;"></th> <th class="col-sm-1 text-center"></th>
</tr> </tr>
</thead> </thead>
<tbody *ngFor="let award of awards"> <tbody *ngFor="let award of awards">

View File

@ -12,7 +12,7 @@
</div> </div>
<div class="col-xs-3"> <div class="col-xs-3">
<span (click)="select()" title="Bearbeiten" class="glyphicon glyphicon-pencil pull-left edit"></span> <span (click)="select()" title="Name, Squad, Rang" class="glyphicon glyphicon-pencil pull-left edit"></span>
<span (click)="award()" title="Auszeichnungen" class="icon-award pull-left"></span> <span (click)="award()" title="Auszeichnungen" class="icon-award pull-left"></span>
<span (click)="delete(); $event.stopPropagation()" title="Löschen" class="glyphicon glyphicon-trash trash"></span> <span (click)="delete(); $event.stopPropagation()" title="Löschen" class="glyphicon glyphicon-trash trash"></span>
</div> </div>