Skip to content
Open
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<mat-card [formGroup]="form" class="gap-4 p-4">
<mat-card-header class="justify-content-center">
<h3 class="fs-1 fw-bold">New category</h3>
</mat-card-header>
<ex-dialog-card>
<div ex-dialog-card-title>New category</div>

<mat-card-content class="d-flex flex-column gap-3">
<form ex-dialog-card-content [formGroup]="form" [confirmExitDialog]="dialogRef" class="d-flex flex-column gap-3">
@let nameControl = form.controls.name;
<mat-form-field>
<mat-label>Name</mat-label>
Expand All @@ -19,7 +17,7 @@ <h3 class="fs-1 fw-bold">New category</h3>
</mat-form-field>

<div class="d-flex flex-column gap-2">
<div class="d-flex align-items-center gap-2">
<div class="d-flex align-items-center gap-2">
<ex-button
filled
color="accent"
Expand Down Expand Up @@ -47,8 +45,7 @@ <h3 class="fs-1 fw-bold">New category</h3>
Emoji is required!
</div>
}
</div>

</div>
@let noteControl = form.controls.note;
<mat-form-field>
<mat-label>Note</mat-label>
Expand All @@ -57,9 +54,9 @@ <h3 class="fs-1 fw-bold">New category</h3>
<ex-validator [control]="noteControl" />
</mat-error>
</mat-form-field>
</mat-card-content>
</form>

<mat-card-actions class="justify-content-end align-items-center gap-2 w-100 px-4 pt-0">
<div ex-dialog-card-actions class="d-flex flex-column-reverse flex-sm-row justify-content-end align-items-center gap-2 w-100">
<ex-button
outline
color="accent"
Expand All @@ -77,8 +74,8 @@ <h3 class="fs-1 fw-bold">New category</h3>
>
Create
</ex-button>
</mat-card-actions>
</mat-card>
</div>
</ex-dialog-card>

<mat-menu
#emojiMenu="matMenu"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,11 @@ textarea {
.emoji-error {
color: var(--error-color);
padding: 0 20px;
}

ex-button {
width: 100%;
@include displaySm {
width: auto;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component, inject } from '@angular/core';
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
Expand All @@ -9,30 +8,33 @@ import { PickerComponent } from '@ctrl/ngx-emoji-mart';
import { EmojiEvent } from '@ctrl/ngx-emoji-mart/ngx-emoji';
import { Category } from '../../../data-model/modules/category/Category';
import { ButtonComponent } from '../../../shared/button/button.component';
import { DialogCardComponent } from '../../../shared/dialog-card/dialog-card.component';
import { DialogComponent } from '../../../shared/dialog/dialog.service';
import { InputClearButtonComponent } from '../../../shared/input-clear-button/input-clear-button.component';
import { SnackbarService } from '../../../shared/snackbar/snackbar.service';
import { ValidatorComponent } from '../../../shared/validator/validator.component';
import { CategoryService } from '../../category.service';
import { AutoTrimDirective } from "src/app/shared/auto-trim.directive";
import { AutoTrimDirective } from '../../../shared/auto-trim.directive';
import { ConfirmExitDialogDirective } from '../../../shared/confirm-exit-dialog.directive';

@Component({
selector: 'ex-create-category-dialog',
templateUrl: './create-category-dialog.component.html',
styleUrl: './create-category-dialog.component.scss',
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatCardModule,
MatMenuModule,
MatIconModule,
PickerComponent,
InputClearButtonComponent,
ButtonComponent,
ValidatorComponent,
AutoTrimDirective
],
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatMenuModule,
MatIconModule,
PickerComponent,
InputClearButtonComponent,
ButtonComponent,
ValidatorComponent,
DialogCardComponent,
AutoTrimDirective,
ConfirmExitDialogDirective,
],
})
export class CreateCategoryDialogComponent extends DialogComponent<undefined, boolean> {
private readonly categoryService = inject(CategoryService);
Expand Down Expand Up @@ -75,9 +77,9 @@ export class CreateCategoryDialogComponent extends DialogComponent<undefined, bo
try {
const newCategory = await this.categoryService.create(request);
this.snackbarService.showSuccess(`Category '${newCategory.emoji}' created successfully!`);
this.dialogRef.close(true);
this.dialogRef.submit(true);
} catch (_err) {
this.dialogRef.close(false);
this.dialogRef.submit(false);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<mat-card [formGroup]="form" class="gap-4 p-4">
<mat-card-header class="justify-content-center">
<h3 class="fs-1 fw-bold">New transaction</h3>
</mat-card-header>
<ex-dialog-card [formGroup]="form">
<div ex-dialog-card-title>New transaction</div>

<mat-card-content class="d-flex flex-column gap-3">
<form ex-dialog-card-content [confirmExitDialog]="dialogRef" [formGroup]="form" class="d-flex flex-column gap-3">
@let titleControl = form.controls.title;
<mat-form-field>
<mat-label>Title</mat-label>
Expand All @@ -18,7 +16,7 @@ <h3 class="fs-1 fw-bold">New transaction</h3>
</mat-error>
</mat-form-field>

<div class="d-flex flex-column flex-md-row gap-3">
<div class="d-flex flex-column flex-sm-row gap-3">
@let amountControl = form.controls.amount;
<mat-form-field>
<mat-label>Amount</mat-label>
Expand Down Expand Up @@ -49,7 +47,7 @@ <h3 class="fs-1 fw-bold">New transaction</h3>
</mat-form-field>
</div>

<div class="d-flex flex-column flex-md-row gap-3">
<div class="d-flex flex-column flex-sm-row gap-3">
@let categoryControl = form.controls.category;
<mat-form-field>
<mat-label>Category</mat-label>
Expand Down Expand Up @@ -86,12 +84,11 @@ <h3 class="fs-1 fw-bold">New transaction</h3>
</mat-error>
</mat-form-field>

<!-- TODO change to textarea -->
@let recurringControl = form.controls.recurring;
<mat-checkbox [formControl]="recurringControl">Automatically recurring?</mat-checkbox>
</mat-card-content>
</form>

<mat-card-actions class="justify-content-end align-items-center gap-2 w-100 px-4 pt-0">
<div ex-dialog-card-actions class="d-flex flex-column-reverse flex-sm-row justify-content-end align-items-center gap-2 w-100">
<ex-button
outline
color="accent"
Expand All @@ -109,5 +106,5 @@ <h3 class="fs-1 fw-bold">New transaction</h3>
>
Create
</ex-button>
</mat-card-actions>
</mat-card>
</div>
</ex-dialog-card>
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,11 @@

textarea {
resize: none;
}

ex-button {
width: 100%;
@include displaySm {
width: auto;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component, inject, OnInit } from '@angular/core';
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field';
Expand All @@ -10,13 +9,15 @@ import { Category } from '../../../data-model/modules/category/Category';
import { Transaction } from '../../../data-model/modules/transaction/Transaction';
import { TransactionType } from '../../../data-model/modules/transaction/TransactionType';
import { ButtonComponent } from '../../../shared/button/button.component';
import { DialogCardComponent } from '../../../shared/dialog-card/dialog-card.component';
import { DialogWithBaseComponent } from '../../../shared/dialog/dialog.service';
import { InputClearButtonComponent } from '../../../shared/input-clear-button/input-clear-button.component';
import { SnackbarService } from '../../../shared/snackbar/snackbar.service';
import { ValidatorComponent } from '../../../shared/validator/validator.component';
import { CategoryService } from '../../category.service';
import { TransactionService } from '../transaction.service';
import { AutoTrimDirective } from "src/app/shared/auto-trim.directive";
import { AutoTrimDirective } from '../../../shared/auto-trim.directive';
import { ConfirmExitDialogDirective } from '../../../shared/confirm-exit-dialog.directive';

export interface CreateTransactionDialogData {
type?: TransactionType;
Expand All @@ -28,17 +29,18 @@ export interface CreateTransactionDialogData {
templateUrl: './create-transaction-dialog.component.html',
styleUrl: './create-transaction-dialog.component.scss',
imports: [
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatCardModule,
MatSelectModule, MatDatepickerModule,
MatCheckboxModule,
InputClearButtonComponent,
ButtonComponent,
ValidatorComponent,
AutoTrimDirective
],
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule, MatDatepickerModule,
MatCheckboxModule,
InputClearButtonComponent,
ButtonComponent,
ValidatorComponent,
DialogCardComponent,
AutoTrimDirective,
ConfirmExitDialogDirective,
],
})
export class CreateTransactionDialogComponent extends DialogWithBaseComponent<CreateTransactionDialogData | undefined, boolean> implements OnInit {
private readonly transactionService = inject(TransactionService);
Expand Down Expand Up @@ -95,9 +97,9 @@ export class CreateTransactionDialogComponent extends DialogWithBaseComponent<Cr
try {
const newTransaction = await this.transactionService.create(request);
this.snackbarService.showSuccess(`Transaction '${newTransaction.title.slice(0, 10)}${newTransaction.title.length > 10 ? '...' : ''}' created successfully!`);
this.dialogRef.close(true);
this.dialogRef.submit(true);
} catch (_err) {
this.dialogRef.close(false);
this.dialogRef.submit(false);
}
}
}
22 changes: 22 additions & 0 deletions frontend/Exence/src/app/shared/auth/guard/has-changes.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { inject } from "@angular/core";
import { CanDeactivateFn } from "@angular/router";
import { ConfirmExitService } from "../../confirm-exit.service";

export interface HasChangesComponent {
hasChanges(): boolean;
}

export const hasChangesGuard: CanDeactivateFn<HasChangesComponent> = async (
component: HasChangesComponent
) => {
const confirmExitService = inject(ConfirmExitService);

const componentHasChanges = component.hasChanges && component.hasChanges();
const serviceHasChanges = confirmExitService.hasChanges();

if (componentHasChanges || serviceHasChanges) {
return await confirmExitService.showConfirmDialog();
}

return true;
}
26 changes: 26 additions & 0 deletions frontend/Exence/src/app/shared/confirm-exit-dialog.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Directive, inject, input, OnInit } from "@angular/core";
import { FormGroupDirective } from "@angular/forms";
import { BaseComponent } from "./base-component/base.component";
import { ConfirmExitService } from "./confirm-exit.service";
import { DialogRef } from "./dialog/dialog.service";

@Directive({
selector: '[confirmExitDialog][formGroup]',
})
export class ConfirmExitDialogDirective extends BaseComponent implements OnInit {
private readonly formGroupDirective = inject(FormGroupDirective);
private readonly confirmExitService = inject(ConfirmExitService);

confirmExitDialog = input.required<DialogRef<any, any>>();

ngOnInit(): void {
const form = this.formGroupDirective.form;

this.addSubscription(form.valueChanges.subscribe(() => {
this.confirmExitDialog().setLocked(form.dirty);
}));

this.confirmExitDialog().setLocked(form.dirty);
this.confirmExitDialog().setOnCloseAttemptWhileLocked(async () => await this.confirmExitService.showConfirmDialog());
}
}
22 changes: 22 additions & 0 deletions frontend/Exence/src/app/shared/confirm-exit.directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Directive, inject, OnDestroy, OnInit } from "@angular/core";
import { FormGroupDirective } from "@angular/forms";
import { ConfirmExitService } from "./confirm-exit.service";

@Directive({
selector: '[confirmExit][formGroup]',
standalone: true,
})
export class ConfirmExitDirective implements OnInit, OnDestroy {
private readonly formGroupDirective = inject(FormGroupDirective);
private readonly confirmExitService = inject(ConfirmExitService);

ngOnInit(): void {
const form = this.formGroupDirective.form;
this.confirmExitService.registerForm(form);
}

ngOnDestroy(): void {
const form = this.formGroupDirective.form;
this.confirmExitService.unregisterForm(form);
}
}
42 changes: 42 additions & 0 deletions frontend/Exence/src/app/shared/confirm-exit.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { inject, Injectable } from "@angular/core";
import { FormGroup } from "@angular/forms";
import { DialogService } from "./dialog/dialog.service";
import { MessageDialogButtonConfig, MessageDialogComponent, PredefiedButtons } from "./message-dialog/message-dialog.component";

@Injectable({
providedIn: 'root'
})
export class ConfirmExitService {
private readonly dialog = inject(DialogService);
private trackedForms = new Set<FormGroup>();

registerForm(form: FormGroup): void {
this.trackedForms.add(form);
}

unregisterForm(form: FormGroup): void {
this.trackedForms.delete(form);
}

hasChanges(): boolean {
return Array.from(this.trackedForms).some(form => form.dirty);
}

async showConfirmDialog(): Promise<boolean> {
return this.dialog.openModal(MessageDialogComponent, {
title: 'Unsaved changes',
message: 'You have unsaved changes. Are you sure you want to leave? Your changes will be lost.',
hideCloseIcon: true,
buttons: MessageDialogButtonConfig.custom(
{
text: 'Continue',
value: true,
color: 'primary',
iconPositionEnd: true,
matIcon: 'chevron_forward',
},
PredefiedButtons.CANCEL,
)
});
}
}
Loading
Loading