Compare commits

...

9 Commits

45 changed files with 460 additions and 430 deletions

View File

@ -1,5 +1,5 @@
import {UserPublic} from "../User/UserPublic"; import {UserPublic} from "../User/UserPublic";
import {validateBoolean, validateDate, validateList, validateString} from "../common/validators"; import {validateDate, validateList, validateString} from "../common/validators";
import {UserPrivate} from "../User/UserPrivate"; import {UserPrivate} from "../User/UserPrivate";
import {GroupUuid} from "./GroupUuid"; import {GroupUuid} from "./GroupUuid";
@ -13,12 +13,11 @@ export class Group extends GroupUuid {
readonly password: string, readonly password: string,
readonly users: UserPublic[], readonly users: UserPublic[],
readonly banned: UserPublic[], readonly banned: UserPublic[],
readonly initial: boolean,
) { ) {
super(uuid); super(uuid);
} }
isOwner(user: UserPublic) { isOwner(user: UserPublic): boolean {
return this.owner.publicUuid === user.publicUuid; return this.owner.publicUuid === user.publicUuid;
} }
@ -31,11 +30,10 @@ export class Group extends GroupUuid {
validateString(json['password']), validateString(json['password']),
validateList(json['users'], UserPublic.fromJson), validateList(json['users'], UserPublic.fromJson),
validateList(json['banned'], UserPublic.fromJson), validateList(json['banned'], UserPublic.fromJson),
validateBoolean(json['initial']),
); );
} }
usersByNameOwnerFirst() { usersByNameOwnerFirst(): UserPublic[] {
return this.users.sort((a, b) => this.compareOwnerFirstThenName(a, b)); return this.users.sort((a, b) => this.compareOwnerFirstThenName(a, b));
} }
@ -52,17 +50,21 @@ export class Group extends GroupUuid {
return a.created.getTime() - b.created.getTime(); return a.created.getTime() - b.created.getTime();
} }
isOwnedBy(user: UserPublic | UserPrivate | null) { isOwnedBy(user: UserPublic | UserPrivate | null): boolean {
return user !== null && user.is(this.owner); return user !== null && user.is(this.owner);
} }
bannedByName() { bannedByName(): UserPublic[] {
return this.banned.sort(UserPublic.compareName); return this.banned.sort(UserPublic.compareName);
} }
hasUser(user: UserPublic) { hasUser(user: UserPublic): boolean {
return this.users.some(u => u.is(user)); return this.users.some(u => u.is(user));
} }
equals(group: Group | null): boolean {
return this.uuid === group?.uuid;
}
} }

View File

@ -1,14 +1,15 @@
import {Group} from "../../group/Group"; import {Group} from "../../group/Group";
import {validateDate, validateDateOrNull, validateNumberOrNull, validateString} from "../../common/validators"; import {validateDate, validateList, validateString} from "../../common/validators";
import {NumbersLot} from "./NumbersLot";
import {UserPrivate} from "../../User/UserPrivate";
export class Numbers { export class Numbers {
constructor( constructor(
readonly uuid: string, readonly uuid: string,
readonly group: Group,
readonly date: Date, readonly date: Date,
readonly read: Date | null, readonly lots: NumbersLot[],
readonly number: number | null, readonly group: Group,
) { ) {
// - // -
} }
@ -16,11 +17,18 @@ export class Numbers {
static fromJson(json: any): Numbers { static fromJson(json: any): Numbers {
return new Numbers( return new Numbers(
validateString(json['uuid']), validateString(json['uuid']),
Group.fromJson(json['group']),
validateDate(json['date']), validateDate(json['date']),
validateDateOrNull(json['read']), validateList(json['lots'], NumbersLot.fromJson),
validateNumberOrNull(json['number']), Group.fromJson(json['group']),
); );
} }
getMine(user: UserPrivate | null): NumbersLot | null {
return this.lots.filter(u => u.user.is(user))[0];
}
sameGroup(numbers: Numbers): boolean {
return this.group.equals(numbers.group);
}
} }

View File

@ -0,0 +1,20 @@
import {UserPublic} from "../../User/UserPublic";
import {validateNumberOrNull} from "../../common/validators";
export class NumbersLot {
constructor(
readonly user: UserPublic,
readonly number: number | null,
) {
// -
}
static fromJson(json: any): NumbersLot {
return new NumbersLot(
UserPublic.fromJson(json['user']),
validateNumberOrNull(json['number']),
);
}
}

View File

@ -28,8 +28,8 @@ export class NumbersService {
this.api.getPage(['Numbers', 'page', groupUuid, page, pageSize], Numbers.fromJson, next); this.api.getPage(['Numbers', 'page', groupUuid, page, pageSize], Numbers.fromJson, next);
} }
fetchAndMarkAsRead(numbersUuid: string, next: Next<Numbers>): void { byUuid(numbersUuid: string, next: Next<Numbers>): void {
this.api.postSingle(['Numbers', 'fetchAndMarkAsRead'], numbersUuid, Numbers.fromJson, next); this.api.postSingle(['Numbers', 'byUuid'], numbersUuid, Numbers.fromJson, next);
} }
canAccess(numbersUuid: string, next: Next<boolean>): void { canAccess(numbersUuid: string, next: Next<boolean>): void {

View File

@ -1,15 +1,6 @@
<div class="tileContainer"> <div class="tileContainer">
<div class="tile" *ngIf="group === null && granted === false"> <app-password-tile [visible]="group === null && granted === false" (join)="join($event)"></app-password-tile>
<div class="tileInner">
<div class="tileTitle">
Passwort
</div>
</div>
<div class="tileContent">
<app-password [visible]="true" (join)="join($event)"></app-password>
</div>
</div>
<ng-container *ngIf="group"> <ng-container *ngIf="group">
@ -42,7 +33,7 @@
</table> </table>
<div class="buttons"> <div class="buttons">
<div class="button buttonRight buttonDelete" (click)="groupDelete(group)"> <div class="button buttonRight buttonDelete" (click)="groupDelete(group)" *ngIf="userService.iOwn(group)">
Gruppe löschen Gruppe löschen
</div> </div>
</div> </div>
@ -57,26 +48,24 @@
Mitglieder Mitglieder
</div> </div>
<div class="tileContent"> <div class="tileContent">
<div class="numbers"> <table>
<table> <tr [class.user_owner]="group.isOwner(user)" *ngFor="let user of group.usersByNameOwnerFirst()">
<tr [class.user_owner]="group.isOwner(user)" *ngFor="let user of group.usersByNameOwnerFirst()"> <td (click)="userService.goto(user)">
<td (click)="userService.goto(user)"> {{ user.name }}
{{ user.name }} </td>
</td> <td>
<td>
<span class="owner" *ngIf="group.isOwnedBy(user)"> <span class="owner" *ngIf="group.isOwnedBy(user)">
Admin Admin
</span> </span>
<div class="buttons"> <div class="buttons">
<ng-container *ngIf="userService.iOwn(group) && !userService.iAm(user)"> <ng-container *ngIf="userService.iOwn(group) && !userService.iAm(user)">
<div class="button buttonRight buttonBan" (click)="groupService.ban(group, user)">Verbannen</div> <div class="button buttonRight buttonBan" (click)="groupService.ban(group, user)">Verbannen</div>
<div class="button buttonRight buttonRemove" (click)="groupService.kick(group, user)">Entfernen</div> <div class="button buttonRight buttonRemove" (click)="groupService.kick(group, user)">Entfernen</div>
</ng-container> </ng-container>
</div> </div>
</td> </td>
</tr> </tr>
</table> </table>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -87,22 +76,20 @@
Verbannt Verbannt
</div> </div>
<div class="tileContent"> <div class="tileContent">
<div class="numbers"> <table>
<table> <tr [class.user_owner]="group.isOwner(user)" *ngFor="let user of group.bannedByName()">
<tr [class.user_owner]="group.isOwner(user)" *ngFor="let user of group.bannedByName()"> <td (click)="userService.goto(user)">
<td (click)="userService.goto(user)"> {{ user.name }}
{{ user.name }} </td>
</td> <td>
<td> <div class="buttons">
<div class="buttons"> <ng-container *ngIf="userService.iOwn(group) && !userService.iAm(user)">
<ng-container *ngIf="userService.iOwn(group) && !userService.iAm(user)"> <div class="button buttonRight buttonUnban" (click)="groupService.unban(group, user)">Aufheben</div>
<div class="button buttonRight buttonUnban" (click)="groupService.unban(group, user)">Aufheben</div> </ng-container>
</ng-container> </div>
</div> </td>
</td> </tr>
</tr> </table>
</table>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -116,14 +103,18 @@
<div class="buttons"> <div class="buttons">
<div class="button buttonRight buttonNext" *ngIf="userService.iOwn(group)" (click)="numbersService.create(group.uuid)">+ Nächste Runde</div> <div class="button buttonRight buttonNext" *ngIf="userService.iOwn(group)" (click)="numbersService.create(group.uuid)">+ Nächste Runde</div>
</div> </div>
<div class="numbers"> <table>
<table> <tr
<tr class="number" *ngFor="let numbers of numbersList.content" (click)="numbersService.goto(numbers)" [class.read]="numbers.read !== null"> *ngFor="let numbers of numbersList.content"
<td>{{ numbers.date | relative:now }}</td> (click)="numbersService.goto(numbers)"
<td>{{ numbers.number || '-' }}</td> [class.date_fresh]="now.getTime() - numbers.date.getTime() < 60 * 1000"
</tr> [class.date_middle]="now.getTime() - numbers.date.getTime() >= 60 * 1000"
</table> [class.date_old]="now.getTime() - numbers.date.getTime() >= 5 * 60 * 1000"
</div> >
<td>{{ numbers.date | relative:now }}</td>
<td>{{ numbers.getMine(userService.user)?.number || '-' }}</td>
</tr>
</table>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,12 +4,24 @@ th {
text-align: left; text-align: left;
} }
.date_fresh {
color: green;
}
.date_middle {
color: orange;
}
.date_old {
color: red;
}
.read { .read {
color: gray; color: lightgray;
} }
.unread { .unread {
background-color: lightskyblue; background-color: palegreen;
} }
.buttonRemove { .buttonRemove {

View File

@ -7,7 +7,7 @@ import {GroupService} from "../../../api/group/group.service";
import {UserService} from "../../../api/User/user.service"; import {UserService} from "../../../api/User/user.service";
import {Group} from "../../../api/group/Group"; import {Group} from "../../../api/group/Group";
import {NumbersService} from "../../../api/tools/Numbers/numbers.service"; import {NumbersService} from "../../../api/tools/Numbers/numbers.service";
import {PasswordComponent} from "../../../shared/password/password.component"; import {PasswordTileComponent} from "../../../shared/password-tile/password-tile.component";
import {Numbers} from "../../../api/tools/Numbers/Numbers"; import {Numbers} from "../../../api/tools/Numbers/Numbers";
import {Page} from "../../../api/common/Page"; import {Page} from "../../../api/common/Page";
import {RelativePipe} from "../../../shared/relative.pipe"; import {RelativePipe} from "../../../shared/relative.pipe";
@ -26,7 +26,7 @@ import {UserPublic} from "../../../api/User/UserPublic";
ReactiveFormsModule, ReactiveFormsModule,
TextComponent, TextComponent,
FormsModule, FormsModule,
PasswordComponent, PasswordTileComponent,
RelativePipe RelativePipe
], ],
templateUrl: './group.component.html', templateUrl: './group.component.html',
@ -95,10 +95,10 @@ export class GroupComponent implements OnInit, OnDestroy {
ngOnDestroy(): void { ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe()); this.subs.forEach(sub => sub.unsubscribe());
this.subs.length=0; this.subs.length = 0;
this.userSubs.forEach(sub => sub.unsubscribe()); this.userSubs.forEach(sub => sub.unsubscribe());
this.userSubs.length=0; this.userSubs.length = 0;
} }
private setGroup(group: Group) { private setGroup(group: Group) {
@ -107,6 +107,9 @@ export class GroupComponent implements OnInit, OnDestroy {
this.userSubs.length = 0; this.userSubs.length = 0;
if (this.group !== null) { if (this.group !== null) {
this.group.users.forEach(_ => this.userSubs.push(this.userService.subscribePush(UserPublic, u => this.updateUser(u)))); this.group.users.forEach(_ => this.userSubs.push(this.userService.subscribePush(UserPublic, u => this.updateUser(u))));
this.updateNumbersList();
} else {
this.numbersList = Page.EMPTY;
} }
} }

View File

@ -1,61 +1,37 @@
<ng-container *ngIf="userService.user !== null"> <ng-container *ngIf="userService.user !== null">
<h1>Profil</h1> <div class="tileContainer">
<table>
<tr>
<th>
Name:
</th>
<td>
<app-text [initial]="userService.user.name" [editable]="true" (onChange)="userService.changeName($event)" [validator]="nameValidator"></app-text>
</td>
</tr>
<tr>
<td class="hint" colspan="2">
Mindestens {{ USER_NAME_MIN_LENGTH }} Zeichen. Keine Leerzeichen. Buchstaben oder Zahlen müssen enthalten sein.
</td>
</tr>
<tr> <div class="tile">
<th colspan="2">&nbsp;</th> <div class="tileInner">
</tr> <div class="tileTitle">
Benutzername
</div>
<div class="tileContent">
<app-text [initial]="userService.user.name" [editable]="true" (onChange)="userService.changeName($event)" [validator]="nameValidator"></app-text>
<div class="hint">
Mindestens {{ USER_NAME_MIN_LENGTH }} Zeichen. Keine Leerzeichen. Buchstaben oder Zahlen müssen enthalten sein.
</div>
</div>
</div>
</div>
<tr> <div class="tile">
<th> <div class="tileInner">
Passwort: <div class="tileTitle">
</th> Passwort
<td> <span *ngIf="!userService.user.password" class="passwordNotSet">(nicht gesetzt))</span>
<div *ngIf="!userService.user.password" class="passwordNotSet">Nicht gesetzt</div> <span *ngIf="userService.user.password" class="passwordSet">(gesetzt)</span>
<div *ngIf="userService.user.password" class="passwordSet">Gesetzt</div> </div>
</td> <div class="tileContent">
</tr> <input #p0 type="text" [(ngModel)]="password0" [class.passwordInvalid]="p0Invalid()" [class.passwordValid]="p0Valid()" (keydown.enter)="p0Enter()">
<tr> <input #p1 type="text" [(ngModel)]="password1" [class.passwordInvalid]="p1Invalid()" [class.passwordValid]="p1Valid()" (keydown.enter)="p1Enter()">
<th> <div class="hint">
Neues Passwort: Mindestens {{ USER_PASSWORD_MIN_LENGTH }} Zeichen. Nicht nur Zahlen. Nicht nur Buchstaben.
</th> </div>
<td> </div>
<input #p0 type="text" [(ngModel)]="password0" [class.passwordInvalid]="p0Invalid()" [class.passwordValid]="p0Valid()" (keydown.enter)="p0Enter()"> </div>
</td> </div>
</tr>
<tr>
<th>
Wiederholung:
</th>
<td>
<input #p1 type="text" [(ngModel)]="password1" [class.passwordInvalid]="p1Invalid()" [class.passwordValid]="p1Valid()" (keydown.enter)="p1Enter()">
</td>
</tr>
<tr>
<td class="hint" colspan="2">
Mindestens {{ USER_PASSWORD_MIN_LENGTH }} Zeichen. Nicht nur Zahlen. Nicht nur Buchstaben.
</td>
</tr>
<tr> </div>
<th colspan="2">&nbsp;</th>
</tr>
</table>
<app-group-list [groups]="groups"></app-group-list>
</ng-container> </ng-container>

View File

@ -51,8 +51,9 @@ export class ProfileComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.updateGroupList();
this.subs.push(this.userService.subscribePush(UserPrivate, _ => { this.subs.push(this.userService.subscribePush(UserPrivate, _ => {
this.groupService.findAllJoined(groups => this.groups = groups); this.updateGroupList();
})); }));
} }
@ -61,6 +62,10 @@ export class ProfileComponent implements OnInit, OnDestroy {
this.subs.length = 0; this.subs.length = 0;
} }
private updateGroupList() {
this.groupService.findAllJoined(groups => this.groups = groups);
}
protected nameValidator(name: string): boolean { protected nameValidator(name: string): boolean {
return name.length >= USER_NAME_MIN_LENGTH && !/\s+|^[^a-zA-Z0-9]+$/.test(name); return name.length >= USER_NAME_MIN_LENGTH && !/\s+|^[^a-zA-Z0-9]+$/.test(name);
} }

View File

@ -1,7 +1,7 @@
<div <div
*ngIf="numbers" *ngIf="numbers"
class="numbers" [class.date_fresh]="now.getTime() - numbers.date.getTime() < 60 * 1000"
[class.date_middle]="now.getTime() - numbers.date.getTime() >= 30 * 1000" [class.date_middle]="now.getTime() - numbers.date.getTime() >= 60 * 1000"
[class.date_old]="now.getTime() - numbers.date.getTime() >= 5 * 60 * 1000" [class.date_old]="now.getTime() - numbers.date.getTime() >= 5 * 60 * 1000"
> >
@ -10,7 +10,7 @@
</div> </div>
<div class="huge" (click)="gotoGroup()"> <div class="huge" (click)="gotoGroup()">
{{ numbers.number || '-' }} {{ numbers.getMine(userService.user)?.number || '-' }}
</div> </div>
<div class="buttons"> <div class="buttons">

View File

@ -8,7 +8,7 @@
text-align: center; text-align: center;
} }
.numbers { .date_fresh {
color: green; color: green;
} }

View File

@ -4,7 +4,7 @@ import {ActivatedRoute} from "@angular/router";
import {NumbersService} from "../../../api/tools/Numbers/numbers.service"; import {NumbersService} from "../../../api/tools/Numbers/numbers.service";
import {NgIf} from "@angular/common"; import {NgIf} from "@angular/common";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {PasswordComponent} from "../../../shared/password/password.component"; import {PasswordTileComponent} from "../../../shared/password-tile/password-tile.component";
import {GroupService} from "../../../api/group/group.service"; import {GroupService} from "../../../api/group/group.service";
import {UserService} from "../../../api/User/user.service"; import {UserService} from "../../../api/User/user.service";
import {RelativePipe} from "../../../shared/relative.pipe"; import {RelativePipe} from "../../../shared/relative.pipe";
@ -16,7 +16,7 @@ import {Subscription, timer} from "rxjs";
imports: [ imports: [
NgIf, NgIf,
FormsModule, FormsModule,
PasswordComponent, PasswordTileComponent,
RelativePipe RelativePipe
], ],
templateUrl: './numbers.component.html', templateUrl: './numbers.component.html',
@ -24,12 +24,12 @@ import {Subscription, timer} from "rxjs";
}) })
export class NumbersComponent implements OnInit, OnDestroy { export class NumbersComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
protected numbers: Numbers | null = null; protected numbers: Numbers | null = null;
protected now: Date = new Date(); protected now: Date = new Date();
private timer?: Subscription;
constructor( constructor(
protected readonly activatedRoute: ActivatedRoute, protected readonly activatedRoute: ActivatedRoute,
protected readonly numbersService: NumbersService, protected readonly numbersService: NumbersService,
@ -40,13 +40,13 @@ export class NumbersComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.timer = timer(1000, 1000).subscribe(() => this.now = new Date()); this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
this.activatedRoute.params.subscribe(params => { this.subs.push(this.activatedRoute.params.subscribe(params => {
const uuid = params['uuid']; const uuid = params['uuid'];
if (uuid) { if (uuid) {
this.numbersService.canAccess(uuid, granted => { this.numbersService.canAccess(uuid, granted => {
if (granted) { if (granted) {
this.numbersService.fetchAndMarkAsRead(uuid, numbers => this.numbers = numbers); this.numbersService.byUuid(uuid, numbers => this.numbers = numbers);
} else { } else {
this.numbersService.getGroupUuid(uuid, groupUuid => this.groupService.goto(groupUuid)); this.numbersService.getGroupUuid(uuid, groupUuid => this.groupService.goto(groupUuid));
} }
@ -54,14 +54,17 @@ export class NumbersComponent implements OnInit, OnDestroy {
} else { } else {
this.numbers = null; this.numbers = null;
} }
}));
this.userService.subscribePush(Numbers, numbers => {
if (this.numbers?.sameGroup(numbers)) {
this.numbers = numbers;
}
}); });
} }
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.timer) { this.subs.forEach(sub => sub.unsubscribe());
this.timer.unsubscribe(); this.subs.length = 0;
this.timer = undefined;
}
} }
@HostListener('window:keydown.escape') @HostListener('window:keydown.escape')

View File

@ -1,9 +1,10 @@
import {Component, Inject, LOCALE_ID, OnInit} from '@angular/core'; import {Component, Inject, LOCALE_ID, OnDestroy, OnInit} from '@angular/core';
import {BODIES, BODIES_PRINT, EARTH, EARTH_MOON, JUPITER, JUPITER_SCALED_DIAMETER, SUN} from "../SOLAR_SYSTEM"; import {BODIES, BODIES_PRINT, EARTH, EARTH_MOON, JUPITER, JUPITER_SCALED_DIAMETER, SUN} from "../SOLAR_SYSTEM";
import {ActivatedRoute, Params} from "@angular/router"; import {ActivatedRoute, Params} from "@angular/router";
import {DecimalPipe, NgForOf, NgIf} from "@angular/common"; import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
import {MIO_KILO, SolarSystemBody} from "../SolarSystemBody"; import {MIO_KILO, SolarSystemBody} from "../SolarSystemBody";
import {Unit} from "../../../../Unit"; import {Unit} from "../../../../Unit";
import {Subscription} from "rxjs";
function getScale(params: Params) { function getScale(params: Params) {
if ('scale' in params) { if ('scale' in params) {
@ -37,10 +38,12 @@ export function makePaler(hexColor: string, factor: number): string {
templateUrl: './solar-system-printout.component.html', templateUrl: './solar-system-printout.component.html',
styleUrl: './solar-system-printout.component.less' styleUrl: './solar-system-printout.component.less'
}) })
export class SolarSystemPrintoutComponent implements OnInit { export class SolarSystemPrintoutComponent implements OnInit, OnDestroy {
public readonly PRINTABLE_ONLY: boolean = true; public readonly PRINTABLE_ONLY: boolean = true;
private readonly subs: Subscription[] = [];
protected readonly makePaler = makePaler; protected readonly makePaler = makePaler;
protected readonly BODIES_PRINT = BODIES_PRINT; protected readonly BODIES_PRINT = BODIES_PRINT;
@ -64,10 +67,15 @@ export class SolarSystemPrintoutComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
JUPITER.scaledDiameter = JUPITER_SCALED_DIAMETER; JUPITER.scaledDiameter = JUPITER_SCALED_DIAMETER;
this.activatedRoute.params.subscribe(params => { this.subs.push(this.activatedRoute.params.subscribe(params => {
const scale = getScale(params); const scale = getScale(params);
BODIES.forEach(b => b.scale(scale)); BODIES.forEach(b => b.scale(scale));
}); }));
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
this.subs.length = 0;
} }
fontSize(body: SolarSystemBody): string { fontSize(body: SolarSystemBody): string {

View File

@ -1,6 +1,29 @@
<ng-container *ngIf="user !== null"> <ng-container *ngIf="user !== null">
<h2>Benutzer: {{ user.name }}</h2>
<h3>Gemeinsame Gruppen</h3> <div class="tileContainer">
<app-group-list [groups]="commonGroups"></app-group-list>
<div class="tile">
<div class="tileInner">
<div class="tileTitle">
Benutzer
</div>
<div class="tileContent">
{{ user.name }}
</div>
</div>
</div>
<div class="tile">
<div class="tileInner">
<div class="tileTitle">
Gemeinsame Gruppen
</div>
<div class="tileContent">
<app-group-list [groups]="commonGroups"></app-group-list>
</div>
</div>
</div>
</div>
</ng-container> </ng-container>

View File

@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnDestroy, OnInit} from '@angular/core';
import {NgForOf, NgIf} from "@angular/common"; import {NgForOf, NgIf} from "@angular/common";
import {ActivatedRoute} from "@angular/router"; import {ActivatedRoute} from "@angular/router";
import {GroupListComponent} from "../group/shared/group-list/group-list.component"; import {GroupListComponent} from "../group/shared/group-list/group-list.component";
@ -6,6 +6,9 @@ import {GroupService} from "../../api/group/group.service";
import {UserService} from "../../api/User/user.service"; import {UserService} from "../../api/User/user.service";
import {Group} from "../../api/group/Group"; import {Group} from "../../api/group/Group";
import {UserPublic} from "../../api/User/UserPublic"; import {UserPublic} from "../../api/User/UserPublic";
import {ReactiveFormsModule} from "@angular/forms";
import {TextComponent} from "../../shared/text/text.component";
import {Subscription} from "rxjs";
@Component({ @Component({
selector: 'app-user', selector: 'app-user',
@ -13,12 +16,16 @@ import {UserPublic} from "../../api/User/UserPublic";
imports: [ imports: [
NgForOf, NgForOf,
NgIf, NgIf,
GroupListComponent GroupListComponent,
ReactiveFormsModule,
TextComponent
], ],
templateUrl: './user.component.html', templateUrl: './user.component.html',
styleUrl: './user.component.less' styleUrl: './user.component.less'
}) })
export class UserComponent implements OnInit { export class UserComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
protected user: UserPublic | null = null; protected user: UserPublic | null = null;
@ -33,13 +40,18 @@ export class UserComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.activatedRoute.params.subscribe(params => { this.subs.push(this.activatedRoute.params.subscribe(params => {
const publicUuid = params['publicUuid']; const publicUuid = params['publicUuid'];
if (publicUuid) { if (publicUuid) {
this.userService.getByPublicUuid(publicUuid, user => this.user = user); this.userService.getByPublicUuid(publicUuid, user => this.user = user);
this.groupService.findAllCommon(publicUuid, commonGroups => this.commonGroups = commonGroups); this.groupService.findAllCommon(publicUuid, commonGroups => this.commonGroups = commonGroups);
} }
}); }));
}
ngOnDestroy(): void {
this.subs.forEach(sub => sub.unsubscribe());
this.subs.length = 0;
} }
} }

View File

@ -0,0 +1,13 @@
<div class="tile" *ngIf="visible">
<div class="tileInner" *ngIf="visible">
<div class="tileTitle">
Gruppenpasswort
</div>
<div class="tileContent">
<input type="text" [(ngModel)]="password" (keydown.enter)="join.emit(password)">
<div class="buttons">
<div class="button buttonRight buttonJoin" (click)="join.emit(password)">Beitreten</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1 @@
@import "../../../common";

View File

@ -3,16 +3,16 @@ import {FormsModule} from "@angular/forms";
import {NgIf} from "@angular/common"; import {NgIf} from "@angular/common";
@Component({ @Component({
selector: 'app-password', selector: 'app-password-tile',
standalone: true, standalone: true,
imports: [ imports: [
FormsModule, FormsModule,
NgIf NgIf
], ],
templateUrl: './password.component.html', templateUrl: './password-tile.component.html',
styleUrl: './password.component.less' styleUrl: './password-tile.component.less'
}) })
export class PasswordComponent implements OnInit { export class PasswordTileComponent implements OnInit {
@Input() @Input()
visible: boolean = false; visible: boolean = false;

View File

@ -1,7 +0,0 @@
<ng-container *ngIf="visible">
<h1>Passwort</h1>
<input type="text" [(ngModel)]="password" (keydown.enter)="join.emit(password)">
<div class="buttons">
<div class="button buttonRight buttonJoin" (click)="join.emit(password)">Beitreten</div>
</div>
</ng-container>

View File

@ -1,4 +1,3 @@
@import "./tile.less";
@import "./user.less"; @import "./user.less";
.buttons { .buttons {

View File

@ -29,10 +29,61 @@ table {
border-collapse: collapse; border-collapse: collapse;
} }
.tileContainer {
padding: @halfSpace;
.tile {
width: 100%;
padding: @halfSpace;
.tileInner {
border: 1px solid #ddd;
border-radius: @space;
background-color: #fbfbfb;
.tileTitle {
font-weight: bold;
padding: @halfSpace @space;
background-color: lightskyblue;
}
.tileContent {
padding: @halfSpace;
table {
width: 100%;
}
td {
white-space: nowrap;
border: 0.2em solid white;
}
}
.tileFooter {
padding: @halfSpace @space;
}
}
}
}
@media (min-width: 1000px) { @media (min-width: 1000px) {
body { body {
font-size: 18px; font-size: 18px;
} }
.tileContainer {
.tile {
float: left;
width: 500px;
}
}
} }

View File

@ -1,56 +0,0 @@
@import "./config.less";
.tileContainer {
padding: @halfSpace;
.tile {
width: 100%;
padding: @halfSpace;
.tileInner {
border: 1px solid #ddd;
border-radius: @space;
background-color: #fbfbfb;
.tileTitle {
font-weight: bold;
padding: @halfSpace @space;
background-color: lightskyblue;
}
.tileContent {
padding: @halfSpace;
table {
width: 100%;
}
td {
white-space: nowrap;
border: 0.2em solid white;
}
}
.tileFooter {
padding: @halfSpace @space;
}
}
}
}
@media (min-width: 1000px) {
.tileContainer {
.tile {
float: left;
width: 500px;
}
}
}

View File

@ -75,15 +75,11 @@ public class Group extends GroupAbstract implements IWebSocketMessage {
@Column(nullable = false) @Column(nullable = false)
private String password = UUID.randomUUID().toString().substring(0, 4); private String password = UUID.randomUUID().toString().substring(0, 4);
@Setter
@Column(nullable = false)
private boolean initial = true;
@NonNull @NonNull
@Column(nullable = false) @Column(nullable = false)
private ZonedDateTime lastAccess = created; private ZonedDateTime lastAccess = created;
protected Group(@NonNull final User owner) { public Group(@NonNull final User owner) {
this.owner = owner; this.owner = owner;
} }

View File

@ -14,25 +14,25 @@ import java.util.Set;
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@RequestMapping("Group") @RequestMapping("Group")
public class GroupReadController { public class GroupController {
private final GroupReadService groupReadService; private final GroupService groupService;
@PostMapping("get") @PostMapping("get")
public GroupDto get(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) { public GroupDto get(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) {
return groupReadService.get(privateUuid, groupUuid); return groupService.get(privateUuid, groupUuid);
} }
@NonNull @NonNull
@GetMapping("findAllJoined") @GetMapping("findAllJoined")
public Set<GroupDto> findAllJoined(@NonNull final UserPrivateUuid userUuid) { public Set<GroupDto> findAllJoined(@NonNull final UserPrivateUuid userUuid) {
return groupReadService.findAllJoined(userUuid); return groupService.findAllJoined(userUuid);
} }
@NonNull @NonNull
@PostMapping("findAllCommon") @PostMapping("findAllCommon")
public Set<GroupDto> findAllCommon(@NonNull final UserPrivateUuid userUuid, @NonNull final UserPublicUuid targetUuid) { public Set<GroupDto> findAllCommon(@NonNull final UserPrivateUuid userUuid, @NonNull final UserPublicUuid targetUuid) {
return groupReadService.findAllCommon(userUuid, targetUuid); return groupService.findAllCommon(userUuid, targetUuid);
} }
} }

View File

@ -1,5 +1,7 @@
package de.ph87.tools.group; package de.ph87.tools.group;
import de.ph87.tools.group.access.GroupAccess;
import de.ph87.tools.group.access.GroupAccessService;
import de.ph87.tools.group.dto.GroupDto; import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.uuid.GroupUuid; import de.ph87.tools.group.uuid.GroupUuid;
import de.ph87.tools.user.User; import de.ph87.tools.user.User;
@ -22,7 +24,7 @@ import java.util.stream.Collectors;
@Service @Service
@Transactional @Transactional
@RequiredArgsConstructor @RequiredArgsConstructor
public class GroupReadService { public class GroupService {
private final GroupAccessService groupAccessService; private final GroupAccessService groupAccessService;

View File

@ -1,12 +1,13 @@
package de.ph87.tools.group; package de.ph87.tools.group.access;
import de.ph87.tools.group.Group;
import de.ph87.tools.user.User; import de.ph87.tools.user.User;
import lombok.Data; import lombok.Data;
@Data @Data
public class GroupAccess { public class GroupAccess {
public final User user; public final User principal;
public final Group group; public final Group group;

View File

@ -1,4 +1,4 @@
package de.ph87.tools.group; package de.ph87.tools.group.access;
import de.ph87.tools.group.uuid.GroupUuid; import de.ph87.tools.group.uuid.GroupUuid;
import de.ph87.tools.user.uuid.UserPrivateUuid; import de.ph87.tools.user.uuid.UserPrivateUuid;

View File

@ -1,5 +1,7 @@
package de.ph87.tools.group; package de.ph87.tools.group.access;
import de.ph87.tools.group.Group;
import de.ph87.tools.group.GroupRepository;
import de.ph87.tools.group.uuid.GroupUuid; import de.ph87.tools.group.uuid.GroupUuid;
import de.ph87.tools.user.User; import de.ph87.tools.user.User;
import de.ph87.tools.user.UserAccessService; import de.ph87.tools.user.UserAccessService;
@ -42,7 +44,7 @@ public class GroupAccessService {
@NonNull @NonNull
public GroupAccess ownerAccess(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) { public GroupAccess ownerAccess(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) {
final GroupAccess groupAccess = access(userPrivateUuid, groupUuid); final GroupAccess groupAccess = access(userPrivateUuid, groupUuid);
if (!groupAccess.group.isOwnedBy(groupAccess.user)) { if (!groupAccess.group.isOwnedBy(groupAccess.principal)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST); throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
} }
return groupAccess; return groupAccess;

View File

@ -62,8 +62,6 @@ public class GroupDto {
return banned.size(); return banned.size();
} }
public final boolean initial;
public GroupDto(@NonNull final Group group, @NonNull final UserPublicDto owner, @NonNull final Set<UserPublicDto> users, @NonNull final Set<UserPublicDto> banned) { public GroupDto(@NonNull final Group group, @NonNull final UserPublicDto owner, @NonNull final Set<UserPublicDto> users, @NonNull final Set<UserPublicDto> banned) {
this.uuid = group.getUuid(); this.uuid = group.getUuid();
this.title = group.getTitle(); this.title = group.getTitle();
@ -72,7 +70,6 @@ public class GroupDto {
this.owner = owner; this.owner = owner;
this.users = users; this.users = users;
this.banned = banned; this.banned = banned;
this.initial = group.isInitial();
} }
} }

View File

@ -1,4 +1,4 @@
package de.ph87.tools.group; package de.ph87.tools.group.member;
import de.ph87.tools.group.dto.GroupDto; import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.requests.GroupJoinRequest; import de.ph87.tools.group.requests.GroupJoinRequest;

View File

@ -1,5 +1,8 @@
package de.ph87.tools.group; package de.ph87.tools.group.member;
import de.ph87.tools.group.Group;
import de.ph87.tools.group.GroupMapper;
import de.ph87.tools.group.GroupService;
import de.ph87.tools.group.dto.GroupDto; import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.events.GroupLeftEvent; import de.ph87.tools.group.events.GroupLeftEvent;
import de.ph87.tools.group.requests.GroupJoinRequest; import de.ph87.tools.group.requests.GroupJoinRequest;
@ -29,7 +32,7 @@ public class GroupMemberService {
private final UserService userService; private final UserService userService;
private final GroupReadService groupReadService; private final GroupService groupService;
private final UserPushService userPushService; private final UserPushService userPushService;
@ -40,7 +43,7 @@ public class GroupMemberService {
@NonNull @NonNull
public GroupDto join(@Nullable final UserPrivateUuid privateUuid, @NonNull final GroupJoinRequest request, @NonNull final HttpServletResponse response) { public GroupDto join(@Nullable final UserPrivateUuid privateUuid, @NonNull final GroupJoinRequest request, @NonNull final HttpServletResponse response) {
final User user = userService.getUserByPrivateUuidOrElseCreate(privateUuid, response); final User user = userService.getUserByPrivateUuidOrElseCreate(privateUuid, response);
final Group group = groupReadService.getGroupByGroupUuid(request.groupUuid); final Group group = groupService.getGroupByGroupUuid(request.groupUuid);
if (group.isBanned(user)) { if (group.isBanned(user)) {
log.error("User is banned from Group and cannot join it: user={}, group={}", user, group); log.error("User is banned from Group and cannot join it: user={}, group={}", user, group);
throw new ResponseStatusException(HttpStatus.FORBIDDEN); throw new ResponseStatusException(HttpStatus.FORBIDDEN);
@ -54,7 +57,7 @@ public class GroupMemberService {
public void leave(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) { public void leave(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid) {
final User user = userAccessService.access(privateUuid); final User user = userAccessService.access(privateUuid);
final Group group = groupReadService.getGroupByGroupUuid(groupUuid); final Group group = groupService.getGroupByGroupUuid(groupUuid);
if (group.isOwnedBy(user)) { if (group.isOwnedBy(user)) {
// owner cannot remove itself from group // owner cannot remove itself from group
throw new ResponseStatusException(HttpStatus.BAD_REQUEST); throw new ResponseStatusException(HttpStatus.BAD_REQUEST);

View File

@ -1,4 +1,4 @@
package de.ph87.tools.group; package de.ph87.tools.group.owner;
import de.ph87.tools.group.dto.GroupDto; import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.requests.GroupChangePasswordRequest; import de.ph87.tools.group.requests.GroupChangePasswordRequest;

View File

@ -1,7 +1,13 @@
package de.ph87.tools.group; package de.ph87.tools.group.owner;
import de.ph87.tools.group.Group;
import de.ph87.tools.group.GroupMapper;
import de.ph87.tools.group.GroupRepository;
import de.ph87.tools.group.access.GroupAccess;
import de.ph87.tools.group.access.GroupAccessService;
import de.ph87.tools.group.dto.GroupDto; import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.events.GroupDeletedEvent; import de.ph87.tools.group.events.GroupDeletedEvent;
import de.ph87.tools.group.member.GroupMemberService;
import de.ph87.tools.group.requests.GroupChangePasswordRequest; import de.ph87.tools.group.requests.GroupChangePasswordRequest;
import de.ph87.tools.group.requests.GroupChangeTitleRequest; import de.ph87.tools.group.requests.GroupChangeTitleRequest;
import de.ph87.tools.group.requests.GroupUserRequest; import de.ph87.tools.group.requests.GroupUserRequest;
@ -84,7 +90,7 @@ public class GroupOwnerService {
@NonNull @NonNull
public GroupDto changeTitle(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupChangeTitleRequest request) { public GroupDto changeTitle(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupChangeTitleRequest request) {
final GroupAccess ug = groupAccessService.access(privateUuid, request.groupUuid); final GroupAccess ug = groupAccessService.access(privateUuid, request.groupUuid);
if (!ug.group.isOwnedBy(ug.user)) { if (!ug.group.isOwnedBy(ug.principal)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN); throw new ResponseStatusException(HttpStatus.FORBIDDEN);
} }
ug.group.setTitle(request.title); ug.group.setTitle(request.title);
@ -111,7 +117,7 @@ public class GroupOwnerService {
private OwnerRemoveResult _removeUser(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request, @NonNull final BiConsumer<Group, User> beforePush) { private OwnerRemoveResult _removeUser(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUserRequest request, @NonNull final BiConsumer<Group, User> beforePush) {
final GroupAccess access = groupAccessService.ownerAccess(privateUuid, request.groupUuid); final GroupAccess access = groupAccessService.ownerAccess(privateUuid, request.groupUuid);
final User user = access.group.getUsers().stream().filter(u -> u.getPublicUuid().equals(request.userPublicUuid)).findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST)); final User user = access.group.getUsers().stream().filter(u -> u.getPublicUuid().equals(request.userPublicUuid)).findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
if (user.equals(access.user)) { if (user.equals(access.principal)) {
// owner cannot kick itself from group // owner cannot kick itself from group
throw new ResponseStatusException(HttpStatus.BAD_REQUEST); throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
} }

View File

@ -1,17 +1,15 @@
package de.ph87.tools.tools.numbers; package de.ph87.tools.tools.numbers;
import de.ph87.tools.group.Group; import de.ph87.tools.group.Group;
import de.ph87.tools.tools.numbers.lot.NumberLot;
import de.ph87.tools.tools.numbers.uuid.NumbersAbstract; import de.ph87.tools.tools.numbers.uuid.NumbersAbstract;
import de.ph87.tools.tools.numbers.uuid.NumbersUuid; import de.ph87.tools.tools.numbers.uuid.NumbersUuid;
import de.ph87.tools.user.User; import de.ph87.tools.user.User;
import de.ph87.tools.user.reference.UserReference;
import jakarta.annotation.Nullable;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.List; import java.util.*;
import java.util.UUID;
@Entity @Entity
@Getter @Getter
@ -45,40 +43,22 @@ public class Numbers extends NumbersAbstract {
@Column(nullable = false) @Column(nullable = false)
private ZonedDateTime date = ZonedDateTime.now(); private ZonedDateTime date = ZonedDateTime.now();
@Column
@Nullable
private ZonedDateTime read = null;
@NonNull @NonNull
@OrderColumn @ElementCollection
@OneToMany(orphanRemoval = true) private List<NumberLot> lots = new ArrayList<>();
private List<UserReference> users;
public Numbers(@NonNull final Group group, @NonNull final List<UserReference> users) { public Numbers(@NonNull final Group group) {
this.group = group; this.group = group;
this.users = users; createRandomLots(group.getUsers());
} }
public void setRead(@NonNull final ZonedDateTime date) { private void createRandomLots(@NonNull final Set<User> users) {
if (this.read != null) { lots.clear();
throw new RuntimeException(); final List<User> shuffledUsers = new ArrayList<>(users);
Collections.shuffle(shuffledUsers);
for (int index = 0; index < shuffledUsers.size(); index++) {
lots.add(new NumberLot(shuffledUsers.get(index), index + 1));
} }
this.read = date;
}
@Nullable
public Integer getNumberForUser(@NonNull final User user) {
for (int userReferenceIndex = 0; userReferenceIndex < users.size(); userReferenceIndex++) {
final UserReference userReference = users.get(userReferenceIndex);
if (userReference.getUser() != null && userReference.getUser().equals(user)) {
return userReferenceIndex + 1;
}
}
return null;
}
public boolean containsUser(@NonNull final User user) {
return users.stream().anyMatch(u -> user.equals(u.getUser()));
} }
} }

View File

@ -2,19 +2,22 @@ package de.ph87.tools.tools.numbers;
import de.ph87.tools.user.User; import de.ph87.tools.user.User;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull;
import lombok.ToString; import lombok.ToString;
@Getter @Getter
@ToString @ToString
class NumbersAccess { public class NumbersAccess {
@NonNull
public final Numbers numbers; public final Numbers numbers;
public final User user; @NonNull
public final User principal;
public NumbersAccess(final Numbers numbers, final User user) { public NumbersAccess(@NonNull final User principal, @NonNull final Numbers numbers) {
this.numbers = numbers; this.numbers = numbers;
this.user = user; this.principal = principal;
} }
} }

View File

@ -41,9 +41,9 @@ public class NumbersController {
} }
@NonNull @NonNull
@PostMapping("fetchAndMarkAsRead") @PostMapping("byUuid")
public NumbersDto fetchAndMarkAsRead(@NonNull final UserPrivateUuid privateUuid, final NumbersUuid numbersUuid) { public NumbersDto byUuid(@NonNull final UserPrivateUuid privateUuid, final NumbersUuid numbersUuid) {
return numbersService.fetchAndMarkAsRead(privateUuid, numbersUuid); return numbersService.byUuid(privateUuid, numbersUuid);
} }
} }

View File

@ -3,14 +3,16 @@ package de.ph87.tools.tools.numbers;
import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import de.ph87.tools.common.uuid.UuidSerializer; import de.ph87.tools.common.uuid.UuidSerializer;
import de.ph87.tools.group.dto.GroupDto; import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.tools.numbers.lot.NumberLotDto;
import de.ph87.tools.tools.numbers.uuid.NumbersAbstract; import de.ph87.tools.tools.numbers.uuid.NumbersAbstract;
import de.ph87.tools.tools.numbers.uuid.NumbersUuid; import de.ph87.tools.tools.numbers.uuid.NumbersUuid;
import jakarta.annotation.Nullable; import jakarta.persistence.ElementCollection;
import lombok.Getter; import lombok.Getter;
import lombok.NonNull; import lombok.NonNull;
import lombok.ToString; import lombok.ToString;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.List;
@Getter @Getter
@ToString(callSuper = true) @ToString(callSuper = true)
@ -20,24 +22,21 @@ public class NumbersDto extends NumbersAbstract {
@JsonSerialize(using = UuidSerializer.class) @JsonSerialize(using = UuidSerializer.class)
private final NumbersUuid uuid; private final NumbersUuid uuid;
@NonNull
private final GroupDto group;
@NonNull @NonNull
private final ZonedDateTime date; private final ZonedDateTime date;
@Nullable @NonNull
private final ZonedDateTime read; @ElementCollection
private final List<NumberLotDto> lots;
@Nullable @NonNull
private final Integer number; private final GroupDto group;
public NumbersDto(@NonNull final Numbers numbers, @NonNull final GroupDto group, @Nullable final Integer number) { public NumbersDto(@NonNull final NumbersAccess lotAccess, @NonNull final GroupDto group) {
this.uuid = numbers.getUuid(); this.uuid = lotAccess.numbers.getUuid();
this.date = numbers.getDate(); this.date = lotAccess.numbers.getDate();
this.read = numbers.getRead(); this.lots = lotAccess.numbers.getLots().stream().map(lot -> new NumberLotDto(lot, lotAccess.principal)).toList();
this.group = group; this.group = group;
this.number = number;
} }
} }

View File

@ -6,8 +6,6 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.ListCrudRepository; import org.springframework.data.repository.ListCrudRepository;
import java.util.List;
public interface NumbersRepository extends ListCrudRepository<Numbers, String> { public interface NumbersRepository extends ListCrudRepository<Numbers, String> {
@NonNull @NonNull
@ -15,6 +13,4 @@ public interface NumbersRepository extends ListCrudRepository<Numbers, String> {
void deleteAllByGroup(@NonNull Group group); void deleteAllByGroup(@NonNull Group group);
List<Numbers> findAllByGroupAndReadNull(@NonNull Group group);
} }

View File

@ -1,7 +1,7 @@
package de.ph87.tools.tools.numbers; package de.ph87.tools.tools.numbers;
import de.ph87.tools.group.GroupAccess; import de.ph87.tools.group.access.GroupAccess;
import de.ph87.tools.group.GroupAccessService; import de.ph87.tools.group.access.GroupAccessService;
import de.ph87.tools.group.GroupMapper; import de.ph87.tools.group.GroupMapper;
import de.ph87.tools.group.dto.GroupDto; import de.ph87.tools.group.dto.GroupDto;
import de.ph87.tools.group.uuid.GroupUuid; import de.ph87.tools.group.uuid.GroupUuid;
@ -9,8 +9,6 @@ import de.ph87.tools.tools.numbers.uuid.NumbersUuid;
import de.ph87.tools.user.User; import de.ph87.tools.user.User;
import de.ph87.tools.user.UserAccessService; import de.ph87.tools.user.UserAccessService;
import de.ph87.tools.user.push.UserPushService; import de.ph87.tools.user.push.UserPushService;
import de.ph87.tools.user.reference.UserReference;
import de.ph87.tools.user.reference.UserReferenceService;
import de.ph87.tools.user.uuid.UserPrivateUuid; import de.ph87.tools.user.uuid.UserPrivateUuid;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -23,11 +21,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
@ -39,8 +33,6 @@ public class NumbersService {
private final GroupAccessService groupAccessService; private final GroupAccessService groupAccessService;
private final UserReferenceService userReferenceService;
private final UserPushService userPushService; private final UserPushService userPushService;
private final UserAccessService userAccessService; private final UserAccessService userAccessService;
@ -49,37 +41,13 @@ public class NumbersService {
public void create(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) { public void create(@NonNull final UserPrivateUuid userPrivateUuid, @NonNull final GroupUuid groupUuid) {
final GroupAccess access = groupAccessService.ownerAccess(userPrivateUuid, groupUuid); final GroupAccess access = groupAccessService.ownerAccess(userPrivateUuid, groupUuid);
final List<UserReference> users = access.getGroup().getUsers().stream().map(userReferenceService::create).collect(Collectors.toList()); final Numbers numbers = numbersRepository.save(new Numbers(access.group));
Collections.shuffle(users); pushAllLots(numbers);
final Numbers numbers = numbersRepository.save(new Numbers(access.getGroup(), users));
publish(numbers);
}
private void publish(@NonNull final Numbers numbers) {
numbers.getUsers()
.stream()
.map(UserReference::getUser)
.filter(Objects::nonNull)
.forEach(user -> publish(numbers, user));
}
private void publish(@NonNull final Numbers numbers, @NonNull final User user) {
final NumbersDto dto = toDto(numbers, user);
log.debug("Sending event: {}", dto);
userPushService.push(user, dto);
}
@NonNull
public NumbersDto toDto(@NonNull final Numbers numbers, @NonNull final User user) {
final GroupDto group = groupMapper.toDto(numbers.getGroup());
final Integer number = numbers.getNumberForUser(user);
return new NumbersDto(numbers, group, number);
} }
public boolean canAccess(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) { public boolean canAccess(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final User user = userAccessService.access(privateUuid); final NumbersAccess access = access(privateUuid, numbersUuid);
final Numbers numbers = numbersRepository.findById(numbersUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST)); return access.numbers.getGroup().getUsers().contains(access.principal);
return numbers.containsUser(user);
} }
@NonNull @NonNull
@ -89,28 +57,44 @@ public class NumbersService {
} }
@NonNull @NonNull
public NumbersDto fetchAndMarkAsRead(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) { public NumbersDto byUuid(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final NumbersAccess access = access(privateUuid, numbersUuid); final NumbersAccess lotAccess = access(privateUuid, numbersUuid);
final ZonedDateTime now = ZonedDateTime.now(); return toDto(lotAccess);
numbersRepository.findAllByGroupAndReadNull(access.numbers.getGroup()).forEach(numbers -> numbers.setRead(now));
return toDto(access.numbers, access.user);
}
@NonNull
private NumbersAccess access(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final User user = userAccessService.access(privateUuid);
final Numbers numbers = numbersRepository.findById(numbersUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
if (!numbers.containsUser(user)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
return new NumbersAccess(numbers, user);
} }
@NonNull @NonNull
public Page<NumbersDto> page(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid, final int page, final int pageSize) { public Page<NumbersDto> page(@NonNull final UserPrivateUuid privateUuid, @NonNull final GroupUuid groupUuid, final int page, final int pageSize) {
final GroupAccess access = groupAccessService.access(privateUuid, groupUuid); final GroupAccess groupAccess = groupAccessService.access(privateUuid, groupUuid);
final PageRequest pageable = PageRequest.of(page, pageSize, Sort.by(new Sort.Order(Sort.Direction.DESC, "date"))); final PageRequest pageable = PageRequest.of(page, pageSize, Sort.by(new Sort.Order(Sort.Direction.DESC, "date")));
return numbersRepository.findAllByGroup(access.group, pageable).map(numbers -> toDto(numbers, access.user)); return numbersRepository.findAllByGroup(groupAccess.group, pageable).map(numbers -> toDto(new NumbersAccess(groupAccess.principal, numbers)));
}
/* ACCESS --------------------------------------------------------------------------------------- */
@NonNull
private NumbersAccess access(@NonNull final UserPrivateUuid privateUuid, @NonNull final NumbersUuid numbersUuid) {
final User principal = userAccessService.access(privateUuid);
final Numbers numbers = numbersRepository.findById(numbersUuid.uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST));
if (!numbers.getGroup().getUsers().contains(principal)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return new NumbersAccess(principal, numbers);
}
/* PUSH, DTO ------------------------------------------------------------------------------------ */
private void pushAllLots(@NonNull final Numbers numbers) {
numbers.getLots()
.stream()
.filter(Objects::nonNull)
.map(lot -> new NumbersAccess(lot.getUser(), numbers))
.forEach(lotAccess -> userPushService.push(lotAccess.principal, toDto(lotAccess)));
}
@NonNull
private NumbersDto toDto(@NonNull final NumbersAccess lotAccess) {
final GroupDto group = groupMapper.toDto(lotAccess.numbers.getGroup());
return new NumbersDto(lotAccess, group);
} }
} }

View File

@ -0,0 +1,37 @@
package de.ph87.tools.tools.numbers.lot;
import de.ph87.tools.user.User;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.ManyToOne;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.ToString;
@Getter
@ToString
@Embeddable
@NoArgsConstructor
public class NumberLot {
@NonNull
@ManyToOne(optional = false)
private User user;
@Column(nullable = false)
private int number;
@Column(nullable = false)
private boolean revealed = false;
public NumberLot(@NonNull final User user, final int number) {
this.user = user;
this.number = number;
}
public boolean is(@NonNull final User user) {
return this.user.equals(user);
}
}

View File

@ -0,0 +1,25 @@
package de.ph87.tools.tools.numbers.lot;
import de.ph87.tools.user.User;
import de.ph87.tools.user.UserPublicDto;
import jakarta.annotation.Nullable;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
@Getter
@ToString
public class NumberLotDto {
@NonNull
private final UserPublicDto user;
@Nullable
private final Integer number;
public NumberLotDto(@NonNull final NumberLot lot, @NonNull final User principal) {
this.user = new UserPublicDto(lot.getUser());
this.number = lot.isRevealed() || lot.getUser().equals(principal) ? lot.getNumber() : null;
}
}

View File

@ -1,35 +0,0 @@
package de.ph87.tools.user.reference;
import de.ph87.tools.user.User;
import jakarta.annotation.Nullable;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.ToString;
@Entity
@Getter
@ToString
@NoArgsConstructor
@Table(name = "`user_reference`")
public class UserReference {
@Id
@GeneratedValue
private long id;
@Nullable
@ManyToOne
private User user;
@NonNull
@Column(nullable = false)
private String name;
public UserReference(@NonNull final User user) {
this.user = user;
this.name = user.getName();
}
}

View File

@ -1,7 +0,0 @@
package de.ph87.tools.user.reference;
import org.springframework.data.repository.ListCrudRepository;
public interface UserReferenceRepository extends ListCrudRepository<UserReference, Long> {
}

View File

@ -1,23 +0,0 @@
package de.ph87.tools.user.reference;
import de.ph87.tools.user.User;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class UserReferenceService {
private final UserReferenceRepository userReferenceRepository;
@NonNull
public UserReference create(@NonNull final User user) {
return userReferenceRepository.save(new UserReference(user));
}
}