Skip to content
This repository has been archived by the owner on Oct 4, 2024. It is now read-only.

Commit

Permalink
FE-#93: Add expense-split widget and edit/create expense entry dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
Drumber committed May 16, 2024
1 parent 53c51f1 commit cd59a31
Show file tree
Hide file tree
Showing 17 changed files with 448 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.time.LocalDateTime;

public abstract class Widget {
public abstract class Widget implements WidgetInterface {
public abstract String getId();
public abstract LocalDateTime getCreationDate();
public abstract WidgetType getWidgetType();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dhbw.get2gether.backend.widget.model;

import java.time.LocalDateTime;

public interface WidgetInterface {
String getId();
LocalDateTime getCreationDate();
WidgetType getWidgetType();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.dhbw.get2gether.backend.widget.model.expensesplit;

import com.dhbw.get2gether.backend.widget.model.WidgetInterface;
import com.dhbw.get2gether.backend.widget.model.WidgetType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
Expand All @@ -11,10 +13,16 @@
@Builder(toBuilder = true)
@Getter
@AllArgsConstructor
public class ExpenseSplitWidgetDto {
public class ExpenseSplitWidgetDto implements WidgetInterface {
private final String id;
private final LocalDateTime creationDate;
@Builder.Default
private List<ExpenseEntryDto> entries = new ArrayList<>();


@Override
public WidgetType getWidgetType() {
return WidgetType.EXPENSE_SPLIT;
}
}

8 changes: 7 additions & 1 deletion frontend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ import {
MatEndDate,
MatStartDate
} from "@angular/material/datepicker";
import { ExpenseSplitWidgetComponent } from './widgets/expense-split-widget/expense-split-widget.component';
import { CreateEditExpenseEntryDialogComponent } from './widgets/expense-split-widget/create-edit-expense-entry-dialog/create-edit-expense-entry-dialog.component';
import {MatSelectModule} from "@angular/material/select";

registerLocaleData(localeDe);

Expand Down Expand Up @@ -109,6 +112,8 @@ function loadMapApi(httpClient: HttpClient) {
WidgetsBarComponent,
ParticipantsSidenavComponent,
ParticipantCardComponent,
ExpenseSplitWidgetComponent,
CreateEditExpenseEntryDialogComponent,
],
imports: [
BrowserModule,
Expand Down Expand Up @@ -150,7 +155,8 @@ function loadMapApi(httpClient: HttpClient) {
MatDatepickerCancel,
MatDatepickerApply,
ReactiveFormsModule,
MatNativeDateModule
MatNativeDateModule,
MatSelectModule
],
providers: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
(onWidgetUpdated)="onWidgetUpdated.emit($event)"
></app-maps-widget>
}
@case (WidgetType.EXPENSE_SPLIT) {
<app-expense-split-widget
[widget]="widget"
[eventData]="eventData"
(onWidgetUpdated)="onWidgetUpdated.emit($event)"
></app-expense-split-widget>
}

<!-- TODO: add other widget components here -->

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<h2 mat-dialog-title>Ausgabe erstellen</h2>

<mat-dialog-content>
<form [formGroup]="formGroup">
<div class="form-grid">
<p class="label">Beschreibung</p>
<mat-form-field subscriptSizing="dynamic">
<mat-label>Beschreibung</mat-label>
<input matInput formControlName="description">
</mat-form-field>

<p class="label">Betrag</p>
<mat-form-field subscriptSizing="dynamic">
<mat-label>Betrag</mat-label>
<input matInput type="number" placeholder="0,00" formControlName="price" class="price-input">
<span matTextSuffix>&nbsp;€</span>
</mat-form-field>

@if (userService.user | async; as user) {
<p class="label">Zahlungsempfänger</p>
<div>
<img [ngSrc]="user.profilePictureUrl" width="32" height="32" alt="" class="user-profile-picture">
{{ user.firstName }} {{ user.lastName }}
</div>
}

<p class="label">Beteiligte</p>
<mat-form-field subscriptSizing="dynamic">
<mat-select formControlName="involvedUsers" multiple placeholder="Keine Personen ausgewählt">
@for (user of users; track user.id) {
<mat-option [value]="user.id">
<img [ngSrc]="user.profilePictureUrl" width="24" height="24" alt="" class="user-profile-picture">
{{ user.firstName }} {{ user.lastName }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</form>
</mat-dialog-content>

<mat-dialog-actions>
<button mat-button mat-dialog-close>Abbrechen</button>
<button mat-button [disabled]="!formGroup.valid" (click)="submit()">
{{ isCreatingNewEntry ? 'Erstellen' : 'Speichern' }}
</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
.form-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: var(--size-24);

.label {
margin: 0 0 4px;
align-self: center;
font-size: var(--font-size-subtitle);
font-weight: var(--font-weight-subtitle);
}

@media screen and (max-width: 500px) {
grid-template-columns: 1fr;
gap: 0;

.label:not(:first-child) {
margin-top: var(--size-24);
}
}
}

.user-profile-picture {
border-radius: 50%;
vertical-align: middle;
margin-right: 5px;
}

.price-input {
-moz-appearance: textfield;
text-align: right;

&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
display: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { CreateEditExpenseEntryDialogComponent } from './create-edit-expense-entry-dialog.component';

describe('CreateEditExpenseSplitDialogComponent', () => {
let component: CreateEditExpenseEntryDialogComponent;
let fixture: ComponentFixture<CreateEditExpenseEntryDialogComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CreateEditExpenseEntryDialogComponent]
})
.compileComponents();

fixture = TestBed.createComponent(CreateEditExpenseEntryDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {SimpleUser} from "../../../../model/user";
import {ExpenseEntry, ExpenseEntryAddCommand, ExpenseEntryUpdateCommand} from "../../../../model/expense-split-widget";
import {FormBuilder, FormControl, FormGroup, Validators} from "@angular/forms";
import {UserService} from "../../../../services/user.service";

@Component({
selector: 'app-create-edit-expense-split-dialog',
templateUrl: './create-edit-expense-entry-dialog.component.html',
styleUrl: './create-edit-expense-entry-dialog.component.scss'
})
export class CreateEditExpenseEntryDialogComponent {

users: SimpleUser[];
entry: ExpenseEntry | undefined;

formGroup: FormGroup;

constructor(
@Inject(MAT_DIALOG_DATA) data: { users: SimpleUser[], entry: ExpenseEntry | undefined },
private dialogRef: MatDialogRef<CreateEditExpenseEntryDialogComponent>,
public userService: UserService,
fb: FormBuilder
) {
this.users = data.users;
this.entry = data.entry;

this.formGroup = fb.group({
description: new FormControl(
this.entry?.description ?? "",
Validators.required
),
price: new FormControl(
this.entry?.price ?? null,
[Validators.required, Validators.min(0)]
),
involvedUsers: new FormControl(
this.entry?.involvedUsers?.map(u => u.user.id) ?? []
)
});
}

submit() {
let data: ExpenseEntryAddCommand | ExpenseEntryUpdateCommand = {
description: this.formGroup.value.description,
price: this.formGroup.value.price,
involvedUsers: this.formGroup.value.involvedUsers
};
this.dialogRef.close(data);
}

get isCreatingNewEntry(): boolean {
return !this.entry;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="container">
@if (widget.entries.length > 0) {
<button mat-stroked-button (click)="createNewExpenseEntry()">
<mat-icon>add</mat-icon>
Ausgabe erstellen
</button>
} @else {
<div class="no-entries-container">
<img ngSrc="./assets/noExpenseSplits.svg" width="300" height="200" alt="">

<h4>Keine Ausgabenverteilung angelegt</h4>
<button mat-stroked-button (click)="createNewExpenseEntry()">
<mat-icon>add</mat-icon>
Ausgabe erstellen
</button>
</div>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.no-entries-container {
display: grid;
place-items: center;
text-align: center;
padding: var(--size-12);

img {
max-width: 100%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { ExpenseSplitWidgetComponent } from './expense-split-widget.component';

describe('ExpenseSplitWidgetComponent', () => {
let component: ExpenseSplitWidgetComponent;
let fixture: ComponentFixture<ExpenseSplitWidgetComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ExpenseSplitWidgetComponent]
})
.compileComponents();

fixture = TestBed.createComponent(ExpenseSplitWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {BaseWidget} from "../../../model/common-widget";
import {Event} from "../../../model/event";
import {ExpenseEntry, ExpenseSplitWidget} from "../../../model/expense-split-widget";
import {MatDialog} from "@angular/material/dialog";
import {
CreateEditExpenseEntryDialogComponent
} from "./create-edit-expense-entry-dialog/create-edit-expense-entry-dialog.component";
import {ExpenseSplitWidgetService} from "../../../services/widgets/expense-split-widget.service";

@Component({
selector: 'app-expense-split-widget',
templateUrl: './expense-split-widget.component.html',
styleUrl: './expense-split-widget.component.scss'
})
export class ExpenseSplitWidgetComponent {

@Input()
eventData!: Event;

@Input({transform: (value: BaseWidget): ExpenseSplitWidget => value as ExpenseSplitWidget})
widget!: ExpenseSplitWidget;

@Output()
onWidgetUpdated = new EventEmitter<ExpenseSplitWidget>();

constructor(
private service: ExpenseSplitWidgetService,
private dialog: MatDialog
) {
}

createNewExpenseEntry() {
const dialogRef = this.dialog.open(CreateEditExpenseEntryDialogComponent, {
width: "600px",
data: {users: this.eventData.participants}
});

dialogRef.afterClosed().subscribe(addCommand => {
if (!addCommand) return;

this.service.createExpenseEntry(this.eventData.id, this.widget.id, addCommand)
.subscribe(widget => this.onWidgetUpdated.emit(widget));
})
}

editExpenseEntry(entry: ExpenseEntry) {
const dialogRef = this.dialog.open(CreateEditExpenseEntryDialogComponent, {
width: "600px",
data: {
users: this.eventData.participants,
entry: entry
}
});

dialogRef.afterClosed().subscribe(updateCommand => {
if (!updateCommand) return;

this.service.updateExpenseEntry(this.eventData.id, this.widget.id, entry.id, updateCommand)
.subscribe(widget => this.onWidgetUpdated.emit(widget));
})
}

}
Loading

0 comments on commit cd59a31

Please sign in to comment.