Skip to content

Commit d60d5fb

Browse files
PhantomDaveCopilot
andauthored
feat: Implement GraphQL mutations and service for dashboard and widget management (#167)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent f773d7a commit d60d5fb

File tree

18 files changed

+168
-70
lines changed

18 files changed

+168
-70
lines changed

PhantomDave.BankTracking.Api/Program.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,27 @@ public static void Main(string[] args)
8181
builder.Services.AddCors(options =>
8282
{
8383
options.AddDefaultPolicy(policy =>
84-
policy
85-
.WithOrigins("http://localhost:4200", "http://localhost:5095")
86-
.AllowAnyHeader()
87-
.AllowAnyMethod()
88-
.AllowCredentials());
84+
{
85+
if (builder.Environment.IsDevelopment())
86+
{
87+
policy
88+
.WithOrigins("http://localhost:4200", "http://localhost:5095", "http://127.0.0.1:4200")
89+
.AllowAnyHeader()
90+
.AllowAnyMethod()
91+
.AllowCredentials();
92+
}
93+
else
94+
{
95+
policy
96+
.WithOrigins("http://localhost:4200", "http://localhost:5095")
97+
.AllowAnyHeader()
98+
.AllowAnyMethod()
99+
.AllowCredentials();
100+
}
101+
});
89102
});
90103

104+
91105
var graphqlBuilder = builder.Services
92106
.AddGraphQLServer()
93107
.AddAuthorization()

frontend/eslint.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ export default [
2929
],
3030
parser: '@typescript-eslint/parser',
3131
parserOptions: {
32-
project: ['./tsconfig.app.json'],
32+
project: ['./tsconfig.app.json', './tsconfig.spec.json'],
3333
createDefaultProgram: true,
3434
},
3535
rules: {
3636
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
3737
'no-console': 'warn',
38+
3839
},
3940
},
4041
{

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "frontend",
33
"version": "0.0.0",
4+
"type": "module",
45
"scripts": {
56
"ng": "ng",
67
"start": "ng serve",

frontend/src/app/components/dashboards/dashboard-component/dashboard-component.component.html

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,20 @@
77
<gridster [options]="options" [class.edit-mode]="isEditMode()" class="gridster-container">
88
@for (widget of widgets(); track widget) {
99
<gridster-item [item]="widget" class="dashboard-widget">
10-
@switch (widget.type) {
11-
@case (WidgetType.CURRENT_BALANCE) {
12-
<app-widget-remaining [isEditMode]="isEditMode()"></app-widget-remaining>
13-
}
14-
@case (WidgetType.NET_GRAPH) {
15-
<app-widget-net-graph [isEditMode]="isEditMode()"></app-widget-net-graph>
16-
}
17-
@default {
18-
<div class="placeholder-widget">
19-
<p>Widget: {{ widget['component'] || 'Empty' }}</p>
20-
<p>{{ widget.cols }}x{{ widget.rows }}</p>
21-
</div>
10+
@if (widget.id !== undefined) {
11+
@switch (widget.type) {
12+
@case (WidgetType.CURRENT_BALANCE) {
13+
<app-widget-remaining [isEditMode]="isEditMode()" [widgetId]="widget.id" (delete)="removeItem($event)"></app-widget-remaining>
14+
}
15+
@case (WidgetType.NET_GRAPH) {
16+
<app-widget-net-graph [isEditMode]="isEditMode()" [widgetId]="widget.id" (delete)="removeItem($event)"></app-widget-net-graph>
17+
}
18+
@default {
19+
<div class="placeholder-widget">
20+
<p>Widget: {{ widget['component'] || 'Empty' }}</p>
21+
<p>{{ widget.cols }}x{{ widget.rows }}</p>
22+
</div>
23+
}
2224
}
2325
}
2426
</gridster-item>

frontend/src/app/components/dashboards/dashboard-component/dashboard-component.component.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ describe('DashboardComponent', () => {
5656
it('should show success snackbar with correct widget name for CurrentBalance', () => {
5757
component.onWidgetSelected(WidgetType.CURRENT_BALANCE);
5858

59-
expect(snackbarService.success).toHaveBeenCalledWith('Added Remaining Budget widget to dashboard.');
59+
expect(snackbarService.success).toHaveBeenCalledWith(
60+
'Added Remaining Budget widget to dashboard.',
61+
);
6062
});
6163

6264
it('should show error snackbar if widget creation fails', () => {

frontend/src/app/components/dashboards/dashboard-component/dashboard-component.component.ts

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,25 +47,46 @@ export class DashboardComponent implements OnInit {
4747
readonly selectedDashboard = this.dashboardService.selectedDashboard;
4848

4949
constructor() {
50-
effect(() => {
51-
const dashboard = this.selectedDashboard();
52-
if (dashboard) {
53-
const widgets = dashboard.widgets.map((w) =>
54-
WidgetFactory.createWidgetFromData({
55-
...w,
56-
type: w.widgetType,
57-
}),
58-
);
59-
this.widgets.set(widgets);
60-
} else {
61-
this.widgets.set([]);
62-
}
63-
});
50+
effect(
51+
() => {
52+
const dashboard = this.selectedDashboard();
53+
const currentWidgets = this.widgets();
54+
if (dashboard) {
55+
const newWidgetsData = dashboard.widgets;
56+
const areWidgetsSame =
57+
currentWidgets.length === newWidgetsData.length &&
58+
currentWidgets.every((cw) =>
59+
newWidgetsData.some(
60+
(nw) =>
61+
nw.id === cw.id &&
62+
nw.cols === cw.cols &&
63+
nw.rows === cw.rows &&
64+
nw.x === cw.x &&
65+
nw.y === cw.y &&
66+
nw.widgetType === cw.type,
67+
),
68+
);
69+
70+
if (!areWidgetsSame) {
71+
const widgets = newWidgetsData.map((w) =>
72+
WidgetFactory.createWidgetFromData({
73+
...w,
74+
type: w.widgetType,
75+
}),
76+
);
77+
this.widgets.set(widgets);
78+
}
79+
} else if (currentWidgets.length > 0) {
80+
this.widgets.set([]);
81+
}
82+
},
83+
{ allowSignalWrites: true },
84+
);
6485
}
6586

6687
ngOnInit() {
6788
this.options = {
68-
gridType: GridType.Fit,
89+
gridType: GridType.VerticalFixed,
6990
compactType: CompactType.None,
7091
margin: 10,
7192
outerMargin: true,
@@ -78,10 +99,10 @@ export class DashboardComponent implements OnInit {
7899
minCols: 12,
79100
maxCols: 12,
80101
minRows: 12,
81-
maxRows: 100,
102+
maxRows: 12,
82103
maxItemCols: 12,
83104
minItemCols: 1,
84-
maxItemRows: 8,
105+
maxItemRows: 12,
85106
minItemRows: 1,
86107
maxItemArea: 2500,
87108
minItemArea: 1,
@@ -115,10 +136,13 @@ export class DashboardComponent implements OnInit {
115136
disablePushOnResize: true,
116137
pushDirections: { north: false, east: false, south: false, west: false },
117138
pushResizeItems: false,
139+
allowMultiLayer: false,
140+
defaultLayerIndex: 0,
118141
displayGrid: DisplayGrid.OnDragAndResize,
119142
disableWindowResize: false,
120143
disableWarnings: false,
121144
scrollToNewItems: false,
145+
setGridSize: true,
122146
itemChangeCallback: this.itemChange.bind(this),
123147
itemResizeCallback: this.itemResize.bind(this),
124148
};
@@ -144,14 +168,23 @@ export class DashboardComponent implements OnInit {
144168
cols: widget.cols,
145169
rows: widget.rows,
146170
});
147-
} catch (error) {
171+
} catch {
148172
this.snackbarService.error('Failed to save widget changes.');
149173
}
150174
}
151175
}
152176

153-
removeItem(item: GridsterItemConfig) {
154-
this.widgets.update(widgets => widgets.filter(w => w !== item));
177+
async removeItem(itemId: number) {
178+
try {
179+
const success = await this.dashboardService.removeWidget(itemId);
180+
if (success) {
181+
this.snackbarService.success('Widget removed from dashboard.');
182+
} else {
183+
this.snackbarService.error('Failed to remove widget from dashboard.');
184+
}
185+
} catch {
186+
this.snackbarService.error('Failed to remove widget from dashboard.');
187+
}
155188
}
156189

157190
editDashboard() {
@@ -208,12 +241,6 @@ export class DashboardComponent implements OnInit {
208241
});
209242

210243
if (addedWidget) {
211-
// Create widget from backend data to ensure consistency
212-
const backendWidget = WidgetFactory.createWidgetFromData({
213-
...addedWidget,
214-
type: addedWidget.widgetType,
215-
});
216-
this.widgets.set([...this.widgets(), backendWidget]);
217244
const widgetName = WIDGET_DISPLAY_NAMES[widgetType] ?? String(widgetType);
218245
this.snackbarService.success(`Added ${widgetName} widget to dashboard.`);
219246
}

frontend/src/app/components/dashboards/dashboard-drawer-component/dashboard-drawer-component.component.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@ describe('DashboardDrawerComponent', () => {
3737
});
3838

3939
it('should have correct display name for Net Graph', () => {
40-
const netGraphWidget = component.availableWidgets.find((w) => w.type === WidgetType.NET_GRAPH);
40+
const netGraphWidget = component.availableWidgets.find(
41+
(w) => w.type === WidgetType.NET_GRAPH,
42+
);
4143

4244
expect(netGraphWidget?.name).toBe('Net Graph');
4345
});
4446

4547
it('should have correct display name for Current Balance', () => {
4648
const currentBalanceWidget = component.availableWidgets.find(
47-
(w) => w.type === WidgetType.CURRENT_BALANCE
49+
(w) => w.type === WidgetType.CURRENT_BALANCE,
4850
);
4951

5052
expect(currentBalanceWidget?.name).toBe('Remaining Budget');

frontend/src/app/components/import/import-wizard-component/steps/step5-confirm.component.html

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,24 @@
2929
<mat-icon color="primary">numbers</mat-icon>
3030
<span>Separators:</span>
3131
<strong
32-
>decimal {{ decimalSeparator() }} • thousands
32+
>decimal {{ decimalSeparator() }} • thousands
3333
{{ thousandsSeparator() || 'none' }}</strong
3434
>
3535
</div>
36-
<div class="summary-item" *ngIf="rowsToSkip() > 0">
37-
<mat-icon color="primary">skip_next</mat-icon>
38-
<span>Rows to skip:</span>
39-
<strong>{{ rowsToSkip() }}</strong>
40-
</div>
41-
<div class="summary-item" *ngIf="saveAsTemplate()">
42-
<mat-icon color="primary">save</mat-icon>
43-
<span>Save as template:</span>
44-
<strong>{{ templateName() || 'Untitled template' }}</strong>
45-
</div>
36+
@if (rowsToSkip() > 0) {
37+
<div class="summary-item">
38+
<mat-icon color="primary">skip_next</mat-icon>
39+
<span>Rows to skip:</span>
40+
<strong>{{ rowsToSkip() }}</strong>
41+
</div>
42+
}
43+
@if (saveAsTemplate()) {
44+
<div class="summary-item">
45+
<mat-icon color="primary">save</mat-icon>
46+
<span>Save as template:</span>
47+
<strong>{{ templateName() || 'Untitled template' }}</strong>
48+
</div>
49+
}
4650
</div>
4751

4852
<div class="actions">
@@ -69,15 +73,18 @@
6973
<h3>Result</h3>
7074
<div class="result-metrics">
7175
<div>
72-
<mat-icon color="primary">check_circle</mat-icon> Created:
76+
<mat-icon color="primary">check_circle</mat-icon>
77+
Created:
7378
<strong>{{ res.successCount }}</strong>
7479
</div>
7580
<div>
76-
<mat-icon color="warn">warning</mat-icon> Duplicates:
81+
<mat-icon color="warn">warning</mat-icon>
82+
Duplicates:
7783
<strong>{{ res.duplicateCount }}</strong>
7884
</div>
7985
<div>
80-
<mat-icon color="warn">error</mat-icon> Failed:
86+
<mat-icon color="warn">error</mat-icon>
87+
Failed:
8188
<strong>{{ res.failureCount }}</strong>
8289
</div>
8390
</div>

frontend/src/app/models/dashboards/dashboard.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export class DashboardService {
189189
const wasSelected = this._selectedDashboard()?.id === id;
190190
const updatedDashboards = this._dashboards().filter((d) => d.id !== id);
191191
this._dashboards.set(updatedDashboards);
192-
192+
193193
if (wasSelected) {
194194
if (updatedDashboards.length > 0) {
195195
this._selectedDashboard.set(updatedDashboards[updatedDashboards.length - 1]);

frontend/src/app/widgets/widget-net-graph/widget-net-graph.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<app-widget-wrapper
2+
[widgetId]="widgetId()"
23
[title]="'Yearly Net Graph'"
34
[subtitle]="'Net income and expenses over the past year'"
45
[icon]="'show_chart'"
56
[loading]="loading()"
67
[isEditMode]="isEditMode()"
8+
(delete)="onDeleteWidget($event)"
79
>
810
<div class="widget-content">
911
<app-chart [chartOptions]="chartOptions()" />

0 commit comments

Comments
 (0)