Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,16 @@ public enum WidgetType {
CATEGORY_BOXPLOT,
MONTHLY_BOXPLOT,
YEARLY_SLOPE,
CATEGORY_SANKEY));
CATEGORY_SANKEY,
EXPENSE_FREQUENCY_STATCARD,
INCOME_FREQUENCY_STATCARD,
NO_SPEND_DAYS_STATCARD,
TOP_EXPENSE_CATEGORY_STATCARD,
TOP_INCOME_CATEGORY_STATCARD,
TOP_EXPENSE_TRANSACTION_STATCARD,
TOP_INCOME_TRANSACTION_STATCARD,
BURN_RATE_STATCARD,
SAVINGS_RATE_STATCARD));

public boolean isStatCard() {
return STAT_CARD_TYPES.contains(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.exence.finance.modules.statistics.dto.response;

import com.exence.finance.modules.statistics.dto.Timeframe;
import com.exence.finance.modules.statistics.dto.WidgetSetting;
import com.exence.finance.modules.statistics.dto.WidgetType;
import java.util.Map;

public record ChartWidgetDTO(
Long id,
Expand All @@ -12,4 +14,5 @@ public record ChartWidgetDTO(
Integer x,
Integer y,
Integer cols,
Integer rows) {}
Integer rows,
Map<WidgetSetting, Object> settings) {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.exence.finance.modules.statistics.dto.response;

import com.exence.finance.modules.statistics.dto.WidgetSetting;
import com.exence.finance.modules.statistics.dto.WidgetType;
import com.exence.finance.modules.statistics.dto.payload.WidgetDataPayload;
import java.util.Map;

public record WidgetDataResponse(Long widgetId, WidgetType type, WidgetDataPayload payload) {}
public record WidgetDataResponse(
Long widgetId, WidgetType type, WidgetDataPayload payload, Map<WidgetSetting, Object> settings) {}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public WidgetDataResponse getWidgetData(Long widgetId, Timeframe timeframe) {
}

WidgetDataPayload payload = provider.getData(request);
return new WidgetDataResponse(widget.getId(), widget.getType(), payload);
return new WidgetDataResponse(widget.getId(), widget.getType(), payload, widget.getSettings());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { WidgetType } from './widget-config.model';
import { WidgetDataPayload } from './WidgetDataPayload';
import { WidgetSetting } from './WidgetSetting';

export interface WidgetDataResponse<T extends WidgetDataPayload = WidgetDataPayload> {
widgetId: number;
type: WidgetType;
payload: T;
settings?: Record<WidgetSetting, unknown>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,15 @@ export const WIDGET_CATEGORY_TYPES: Partial<Record<WidgetType, CategoryType[]>>
[WidgetType.MONTHLY_BOXPLOT]: [CategoryType.INCOME, CategoryType.EXPENSE],
[WidgetType.YEARLY_SLOPE]: [CategoryType.INCOME, CategoryType.EXPENSE],
[WidgetType.CATEGORY_SANKEY]: [CategoryType.INCOME, CategoryType.EXPENSE],
[WidgetType.EXPENSE_FREQUENCY_STATCARD]: [CategoryType.EXPENSE],
[WidgetType.INCOME_FREQUENCY_STATCARD]: [CategoryType.INCOME],
[WidgetType.NO_SPEND_DAYS_STATCARD]: [CategoryType.EXPENSE],
[WidgetType.TOP_EXPENSE_CATEGORY_STATCARD]: [CategoryType.EXPENSE],
[WidgetType.TOP_INCOME_CATEGORY_STATCARD]: [CategoryType.INCOME],
[WidgetType.TOP_EXPENSE_TRANSACTION_STATCARD]: [CategoryType.EXPENSE],
[WidgetType.TOP_INCOME_TRANSACTION_STATCARD]: [CategoryType.INCOME],
[WidgetType.BURN_RATE_STATCARD]: [CategoryType.EXPENSE],
[WidgetType.SAVINGS_RATE_STATCARD]: [CategoryType.INCOME],
};

export const CATEGORY_FILTERABLE_WIDGET_TYPES: WidgetType[] = Object.keys(WIDGET_CATEGORY_TYPES) as WidgetType[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</mat-error>
</mat-form-field>

@if (!isStatCard() && isCategoryFilterable()) {
@if (isCategoryFilterable()) {
@let categoryControls = form.controls.categories.controls;
<mat-form-field>
<mat-label>Categories</mat-label>
Expand All @@ -36,8 +36,17 @@
<ex-validator [control]="categoryControls.searchText" />
</mat-error>
</mat-form-field>
@if (noneSelected()) {
<div class="d-flex flex-row justify-content-start px-3 py-1">
<ex-button text (click)="selectAll()">Select all</ex-button>
</div>
} @else {
<div class="d-flex flex-row justify-content-start px-3 py-1">
<ex-button text (click)="deselectAll()">Deselect all</ex-button>
</div>
}
@for (category of filteredCategories(); track category.id) {
<mat-option [value]="category">
<mat-option [value]="category.id">
<mat-icon [style.color]="category.color">{{ category.icon }}</mat-icon>
{{ category.name }}
</mat-option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Category } from '../../../data-model/modules/category/Category';
import { CategoryType } from '../../../data-model/modules/category/CategoryType';
import {
CATEGORY_FILTERABLE_WIDGET_TYPES,
GROUP_WIDGET_TYPES,
WIDGET_CATEGORY_TYPES,
WidgetType,
} from '../../../data-model/modules/statistics/widget-config.model';
Expand All @@ -23,6 +22,7 @@ import { SelectAutoFocusDirective } from '../../../shared/select-auto-focus.dire
import { toRawValueSignal } from '../../../shared/util/utils';
import { ValidatorComponent } from '../../../shared/validator/validator.component';
import { CategoryService } from '../../transactions-and-categories/category.service';
import { ExtraValidators } from '../../../shared/validators';

export interface EditChartDialogData {
title: string;
Expand Down Expand Up @@ -59,16 +59,19 @@ export class EditChartDialogComponent extends DialogComponent<EditChartDialogDat

data = this.dialogRef.value;

private readonly cardTypes = GROUP_WIDGET_TYPES.card;
private readonly categoryFilterableWidgets = CATEGORY_FILTERABLE_WIDGET_TYPES;

form = this.fb.group({
title: this.fb.control<string>(this.data.title, [Validators.required, Validators.maxLength(255)]),
categories: this.fb.group({
selectedCategories: this.fb.control<Category[]>([]),
selectedCategories: this.fb.control<number[]>(
(this.data.settings?.[WidgetSetting.CATEGORY_IDS] as number[] | undefined) ?? [],
[Validators.required, ExtraValidators.filledArray],
),
searchText: this.fb.control<string>('', [Validators.maxLength(25)]),
}),
});
selectedCategoriesValue = toRawValueSignal(this.form.controls.categories.controls.selectedCategories);

private categories = signal<Category[]>([]);
private searchText = toRawValueSignal(this.form.controls.categories.controls.searchText);
Expand All @@ -85,10 +88,10 @@ export class EditChartDialogComponent extends DialogComponent<EditChartDialogDat
return categories.filter(c => c.name.toLowerCase().includes(search.toLowerCase()));
});

isStatCard = computed(() => this.cardTypes.includes(this.data.type));

isCategoryFilterable = computed(() => this.categoryFilterableWidgets.includes(this.data.type));

noneSelected = computed<boolean>(() => this.selectedCategoriesValue().length === 0);

constructor() {
super(inject(DialogRef));
this.categoryService.list().then(categories => this.categories.set(categories));
Expand All @@ -101,9 +104,7 @@ export class EditChartDialogComponent extends DialogComponent<EditChartDialogDat
const settings = { ...this.data.settings } as Record<string, unknown>;

if (this.isCategoryFilterable()) {
const categoryIds = formValue.categories.selectedCategories
.map(c => c.id)
.filter((id): id is number => id !== undefined);
const categoryIds = formValue.categories.selectedCategories;
if (categoryIds.length > 0) {
settings[WidgetSetting.CATEGORY_IDS] = categoryIds;
} else {
Expand All @@ -113,4 +114,12 @@ export class EditChartDialogComponent extends DialogComponent<EditChartDialogDat

this.dialogRef.submit({ title: formValue.title, settings });
}

selectAll(): void {
this.form.controls.categories.controls.selectedCategories.setValue(this.filteredCategories().map(c => c.id!));
}

deselectAll(): void {
this.form.controls.categories.controls.selectedCategories.setValue([]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ <h3 class="fs-5">{{ widget.title }}</h3>
</mat-error>
</mat-form-field>

@if (!isStatCard() && isCategoryFilterable()) {
@if (isCategoryFilterable()) {
@let categoryControls = form.controls.categories.controls;
<mat-form-field>
<mat-label>Categories</mat-label>
Expand All @@ -78,8 +78,17 @@ <h3 class="fs-5">{{ widget.title }}</h3>
<ex-validator [control]="categoryControls.searchText" />
</mat-error>
</mat-form-field>
@if (noneSelected()) {
<div class="d-flex flex-row justify-content-start px-3 py-1">
<ex-button text (click)="selectAll()">Select all</ex-button>
</div>
} @else {
<div class="d-flex flex-row justify-content-start px-3 py-1">
<ex-button text (click)="deselectAll()">Deselect all</ex-button>
</div>
}
@for (category of filteredCategories(); track category.id) {
<mat-option [value]="category">
<mat-option [value]="category.id">
<mat-icon [style.color]="category.color">{{ category.icon }}</mat-icon>
{{ category.name }}
</mat-option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { SelectAutoFocusDirective } from '../../../shared/select-auto-focus.dire
import { toRawValueSignal } from '../../../shared/util/utils';
import { ValidatorComponent } from '../../../shared/validator/validator.component';
import { CategoryService } from '../../transactions-and-categories/category.service';
import { ExtraValidators } from '../../../shared/validators';

export interface WidgetCatalogDialogData {
statCards: StatCardWidget[];
Expand Down Expand Up @@ -89,10 +90,11 @@ export class WidgetCatalogDialogComponent extends DialogComponent<
form = this.fb.group({
title: this.fb.control<string>('', [Validators.required, Validators.maxLength(255)]),
categories: this.fb.group({
selectedCategories: this.fb.control<Category[]>([]),
selectedCategories: this.fb.control<number[]>([], [Validators.required, ExtraValidators.filledArray]),
searchText: this.fb.control<string>('', [Validators.maxLength(25)]),
}),
});
selectedCategoriesValue = toRawValueSignal(this.form.controls.categories.controls.selectedCategories);

private categories = signal<Category[]>([]);
private searchText = toRawValueSignal(this.form.controls.categories.controls.searchText);
Expand All @@ -110,11 +112,6 @@ export class WidgetCatalogDialogComponent extends DialogComponent<
return categories.filter(c => c.name.toLowerCase().includes(search.toLowerCase()));
});

isStatCard = computed(() => {
const selected = this.selectedWidgetValue();
return selected ? this.cardTypes.includes(selected.type) : false;
});

isCategoryFilterable = computed(() => {
const selected = this.selectedWidgetValue();
return selected ? this.categoryFilterableWidgets.includes(selected.type) : false;
Expand All @@ -125,6 +122,8 @@ export class WidgetCatalogDialogComponent extends DialogComponent<
return s.selectedIndex === s.steps.length - 1;
});

noneSelected = computed<boolean>(() => this.selectedCategoriesValue().length === 0);

constructor() {
super(inject(DialogRef));
this.categoryService.list().then(categories => this.categories.set(categories));
Expand All @@ -133,6 +132,10 @@ export class WidgetCatalogDialogComponent extends DialogComponent<
const selected = this.selectedWidgetValue();
this.form.controls.title.setValue(selected?.title ?? '');
});

effect(() =>
this.form.controls.categories.controls.selectedCategories.setValue(this.categories().map(c => c.id!)),
);
}

statCardSelectionDisabled(type: WidgetType): boolean {
Expand All @@ -147,9 +150,7 @@ export class WidgetCatalogDialogComponent extends DialogComponent<
const settings = {} as Record<WidgetSetting, unknown>;

if (this.isCategoryFilterable()) {
const categoryIds = formValue.categories.selectedCategories
.map(c => c.id)
.filter((id): id is number => id !== undefined);
const categoryIds = formValue.categories.selectedCategories;
if (categoryIds.length > 0) {
settings[WidgetSetting.CATEGORY_IDS] = categoryIds;
}
Expand All @@ -171,4 +172,12 @@ export class WidgetCatalogDialogComponent extends DialogComponent<
previousStep(): void {
this.stepper().previous();
}

selectAll(): void {
this.form.controls.categories.controls.selectedCategories.setValue(this.filteredCategories().map(c => c.id!));
}

deselectAll(): void {
this.form.controls.categories.controls.selectedCategories.setValue([]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@
@case ('max') {
Maximum value is {{ errorValue()?.max }}!
}
@case ('filledArray') {
You have to choose minimum 1 value!
}
}
}
5 changes: 5 additions & 0 deletions frontend/Exence/src/app/shared/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,9 @@ export class ExtraValidators {
passwordRegex.lengthInterval.test(value);
return pass ? null : { password: true };
}

static filledArray(control: AbstractControl): ValidationErrors | null {
const value = control.value;
return Array.isArray(value) && value.length > 0 ? null : { filledArray: true };
}
}
Loading