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
5 changes: 5 additions & 0 deletions src/app/components/navbar/navbar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ export class NavbarComponent implements OnInit, OnDestroy {
url: "/salaries/historical-data",
show: hasCurrentUser,
},
{
title: "Курсы валют",
url: "/salaries/currencies-chart",
show: hasCurrentUser,
},
],
},
];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,146 @@
<app-page-header>Курсы валюты</app-page-header>

<div class="container my-5">
<div class="container my-5" *ngIf="currencies; else dataLoading">
<div class="card">
<div class="card-body">
<div>
<table class="table table-sm table-hover">
<div class="card-body p-4">
<div class="mb-3">
<button class="btn btn-sm btn-primary" (click)="create()">
Создать запись
</button>
</div>

<div
class="table-responsive mb-3"
*ngIf="currencies.length > 0; else nothingToShow"
>
<table class="table table-hover table-sm">
<thead>
<tr>
<th>Валюта</th>
<th>Курс</th>
<th>Дата</th>
<tr class="small">
<th>Дата публикации</th>
<th>Валюты (JSON)</th>
<th>Создано</th>
<th>Обновлено</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of currencies" class="small">
<td>{{ item.currencyString }}</td>
<td>{{ item.value }} kzt</td>
<td>{{ item.pubDate | date: "yyyy-MM-dd" }}</td>
<td>{{ item.currencyDate | date: "yyyy-MM-dd" }}</td>
<td>
<button
type="button"
class="btn-link-sm text-start"
(click)="showCurrenciesDetails(item)"
title="Нажмите для просмотра деталей"
>
<code>{{ item.currenciesShort }}</code>
</button>
</td>
<td>{{ item.createdAt | date: "yyyy-MM-dd HH:mm" }}</td>
<td>{{ item.updatedAt | date: "yyyy-MM-dd HH:mm" }}</td>
<td>
<button
type="button"
class="btn-link-sm btn-link-danger"
(click)="openDeleteDialog(item)"
>
Удалить
</button>
</td>
</tr>
</tbody>
</table>
</div>

<div class="mt-4" *ngIf="source && source.results.length > 0">
<app-pagination-buttons
[source]="source"
(pageChange)="loadData($event)"
></app-pagination-buttons>
</div>
</div>
</div>
</div>

<app-confirm-dialog
*ngIf="confirmDeletionMessage"
[message]="confirmDeletionMessage"
></app-confirm-dialog>

<app-dialog
[show]="!!editForm"
(closed)="onEditModalDlgClose()"
[header]="'Создать запись курса валюты'"
>
<form
class="mb-3"
*ngIf="!!editForm"
[formGroup]="editForm"
(ngSubmit)="onEditFormSubmit()"
>
<div class="mb-3">
<label class="form-label">Валюта</label>
<select class="form-select" formControlName="currency">
<option *ngFor="let option of currencyOptions" [ngValue]="option.value">
{{ option.label }}
</option>
</select>
<app-field-error [field]="editForm.get('currency')"></app-field-error>
</div>

<div class="mb-3">
<label class="form-label">Курс (в KZT)</label>
<input
type="number"
step="0.0001"
class="form-control"
formControlName="value"
placeholder="Например: 450.5"
/>
<app-field-error [field]="editForm.get('value')"></app-field-error>
</div>

<div class="mb-3">
<label class="form-label">Дата публикации</label>
<input type="date" class="form-control" formControlName="pubDate" />
<app-field-error [field]="editForm.get('pubDate')"></app-field-error>
</div>

<div class="mb-3">
<button class="btn btn-primary" type="submit">Сохранить</button>
</div>
</form>
</app-dialog>

<!-- Modal for viewing currencies JSON -->
<app-dialog
[show]="!!selectedCurrencyItem"
(closed)="closeCurrenciesDetails()"
[header]="'Данные о курсах валют'"
additionalCss="modal-lg"
>
<div *ngIf="selectedCurrencyItem">
<div class="mb-3">
<strong>Дата:</strong>
{{ selectedCurrencyItem.currencyDate | date: "yyyy-MM-dd" }}
</div>
<div class="mb-3">
<strong>Данные валют (JSON):</strong>
<pre class="bg-light p-3 rounded mt-2 currencies-json">{{
selectedCurrencyItem.currencies | json
}}</pre>
</div>
</div>
</app-dialog>

<ng-template #dataLoading>
<div class="container py-5">
<app-data-loader></app-data-loader>
</div>
</ng-template>

<ng-template #nothingToShow>
<div class="text-center p-3 text-muted">
Нет записей о курсах валют. Создайте первую запись!
</div>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.currencies-json {
max-height: 400px;
overflow-y: auto;
font-size: 0.85rem;
white-space: pre-wrap;
word-break: break-all;
}

code {
font-size: 0.8rem;
}

Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import {
mostUsedImports,
mostUsedServices,
testUtilStubs,
} from "@shared/test-utils";

import { AdminToolsService } from "@services/admin-tools.service";
import { CurrenciesPageComponent } from "./currencies-page.component";
import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { TitleService } from "@services/title.service";
import { CurrenciesCollectionService } from "@services/currencies-collection.service";
import { AlertService } from "@shared/components/alert/services/alert.service";
import { of } from "rxjs";

describe("CurrenciesPageComponent", () => {
let component: CurrenciesPageComponent;
Expand All @@ -16,13 +13,35 @@ describe("CurrenciesPageComponent", () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CurrenciesPageComponent],
imports: [...mostUsedImports],
providers: [...testUtilStubs, ...mostUsedServices, AdminToolsService],
providers: [
{
provide: TitleService,
useValue: {
setTitle: jasmine.createSpy("setTitle"),
resetTitle: jasmine.createSpy("resetTitle"),
},
},
{
provide: CurrenciesCollectionService,
useValue: {
getAll: jasmine
.createSpy("getAll")
.and.returnValue(of({ results: [], totalItems: 0 })),
create: jasmine.createSpy("create"),
delete: jasmine.createSpy("delete"),
},
},
{
provide: AlertService,
useValue: {
success: jasmine.createSpy("success"),
error: jasmine.createSpy("error"),
},
},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(CurrenciesPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,133 @@
import { Component, OnDestroy, OnInit } from "@angular/core";
import { TitleService } from "@services/title.service";
import { HealthCheckItem } from "../health-check-table/health-check-item";
import { AdminToolsService } from "@services/admin-tools.service";
import { untilDestroyed } from "@shared/subscriptions/until-destroyed";
import { CurrencyItem } from "./currency-item";
import { defaultPageParams } from "@models/page-params";
import { PaginatedList } from "@models/paginated-list";
import {
CurrenciesCollectionService,
CurrencyRecord,
} from "@services/currencies-collection.service";
import { DialogMessage } from "@shared/components/dialogs/models/dialog-message";
import { ConfirmMsg } from "@shared/components/dialogs/models/confirm-msg";
import { AlertService } from "@shared/components/alert/services/alert.service";
import { CurrencyRecordEditForm } from "./currency-record-edit-form";
import { CurrencyType } from "@services/admin-tools.service";

@Component({
templateUrl: "./currencies-page.component.html",
styleUrls: ["./currencies-page.component.scss"],
standalone: false,
})
export class CurrenciesPageComponent implements OnInit, OnDestroy {
authorizationToken: string | null = null;
configs: string | null = null;
healthCheckItems: Array<HealthCheckItem> = [];
currencies: Array<CurrencyItem> = [];
currencies: Array<CurrencyItem> | null = null;
source: PaginatedList<CurrencyRecord> | null = null;
currentPage: number = 1;

confirmDeletionMessage: DialogMessage<ConfirmMsg> | null = null;
editForm: CurrencyRecordEditForm | null = null;

// For viewing currencies JSON
selectedCurrencyItem: CurrencyItem | null = null;

// Currency options for select box
readonly currencyOptions = [
{ value: CurrencyType.USD, label: "USD" },
{ value: CurrencyType.EUR, label: "EUR" },
{ value: CurrencyType.RUB, label: "RUB" },
{ value: CurrencyType.KGS, label: "KGS" },
{ value: CurrencyType.SAR, label: "SAR" },
{ value: CurrencyType.AED, label: "AED" },
{ value: CurrencyType.CAD, label: "CAD" },
];

constructor(
private readonly titleService: TitleService,
private readonly adminToolsService: AdminToolsService,
private readonly currenciesService: CurrenciesCollectionService,
private readonly alert: AlertService,
) {
this.titleService.setTitle("Курсы валют");
}

ngOnInit(): void {
this.adminToolsService
.getCurrencies()
this.currencies = null;
this.source = null;
this.loadData(this.currentPage);
}

loadData(pageToRequest: number): void {
this.currencies = null;
this.source = null;
this.currentPage = pageToRequest;

this.currenciesService
.getAll({
page: this.currentPage,
pageSize: defaultPageParams.pageSize,
})
.pipe(untilDestroyed(this))
.subscribe((currencies) => {
this.currencies = currencies.map((x) => new CurrencyItem(x));
.subscribe((response) => {
this.currencies = response.results.map((x) => new CurrencyItem(x));
this.source = response;
});
Comment on lines +63 to 72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling to the subscription.

The loadData subscription lacks an error handler. If the API call fails, the user receives no feedback and the loading state may persist indefinitely.

🔎 Suggested fix
     this.currenciesService
       .getAll({
         page: this.currentPage,
         pageSize: defaultPageParams.pageSize,
       })
       .pipe(untilDestroyed(this))
-      .subscribe((response) => {
-        this.currencies = response.results.map((x) => new CurrencyItem(x));
-        this.source = response;
+      .subscribe({
+        next: (response) => {
+          this.currencies = response.results.map((x) => new CurrencyItem(x));
+          this.source = response;
+        },
+        error: (err) => {
+          this.alert.error("Не удалось загрузить данные. Попробуйте позже.");
+          this.currencies = [];
+          console.error("Failed to load currencies:", err);
+        }
       });

As per coding guidelines for Angular components and proper error handling.

🤖 Prompt for AI Agents
In src/app/modules/admin/components/currencies-page/currencies-page.component.ts
around lines 63 to 72, the subscription to currenciesService.getAll(...) has no
error handler so failures leave the user without feedback and may keep loading
state stuck; add an error callback (or use pipe with catchError/finalize) to
handle API errors: set any loading flags to false in both success and error
paths (or use finalize), set an error state/message or invoke the app/toast
error notification with a clear message, and keep untilDestroyed(this) on the
stream; ensure source/currencies are only set on success and that errors are
logged for debugging.

}

ngOnDestroy(): void {
this.titleService.resetTitle();
}

create(): void {
this.editForm = new CurrencyRecordEditForm();
}

onEditFormSubmit(): void {
if (this.editForm == null) {
return;
}

const createRequest = this.editForm.createRequestOrNull();
if (createRequest == null) {
return;
}

this.currenciesService
.create(createRequest)
.pipe(untilDestroyed(this))
.subscribe(() => {
this.alert.success("Запись о курсе валюты была создана");
this.editForm = null;
this.loadData(this.currentPage);
});
Comment on lines +93 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling to the create subscription.

The create subscription lacks an error handler. If the API call fails, the user receives no feedback and the form remains open with no indication of the failure.

🔎 Suggested fix
     this.currenciesService
       .create(createRequest)
       .pipe(untilDestroyed(this))
-      .subscribe(() => {
-        this.alert.success("Запись о курсе валюты была создана");
-        this.editForm = null;
-        this.loadData(this.currentPage);
+      .subscribe({
+        next: () => {
+          this.alert.success("Запись о курсе валюты была создана");
+          this.editForm = null;
+          this.loadData(this.currentPage);
+        },
+        error: (err) => {
+          this.alert.error("Не удалось создать запись. Попробуйте позже.");
+          console.error("Failed to create currency record:", err);
+        }
       });

As per coding guidelines for Angular components and proper error handling.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.currenciesService
.create(createRequest)
.pipe(untilDestroyed(this))
.subscribe(() => {
this.alert.success("Запись о курсе валюты была создана");
this.editForm = null;
this.loadData(this.currentPage);
});
this.currenciesService
.create(createRequest)
.pipe(untilDestroyed(this))
.subscribe({
next: () => {
this.alert.success("Запись о курсе валюты была создана");
this.editForm = null;
this.loadData(this.currentPage);
},
error: (err) => {
this.alert.error("Не удалось создать запись. Попробуйте позже.");
console.error("Failed to create currency record:", err);
}
});

}

onEditModalDlgClose(): void {
this.editForm = null;
}

openDeleteDialog(item: CurrencyItem): void {
this.confirmDeletionMessage = new DialogMessage(
new ConfirmMsg(
"Удалить запись",
`Вы уверены, что хотите удалить запись от ${new Date(item.currencyDate).toLocaleDateString()}?`,
() => {
this.currenciesService
.delete(item.id)
.pipe(untilDestroyed(this))
.subscribe(() => {
this.alert.success("Запись была удалена");
this.confirmDeletionMessage = null;
this.loadData(this.currentPage);
});
Comment on lines +113 to +120

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add error handling to the delete subscription.

The delete subscription lacks an error handler. If the API call fails, the user receives no feedback and the confirmation dialog may remain open.

🔎 Suggested fix
           this.currenciesService
             .delete(item.id)
             .pipe(untilDestroyed(this))
-            .subscribe(() => {
-              this.alert.success("Запись была удалена");
-              this.confirmDeletionMessage = null;
-              this.loadData(this.currentPage);
+            .subscribe({
+              next: () => {
+                this.alert.success("Запись была удалена");
+                this.confirmDeletionMessage = null;
+                this.loadData(this.currentPage);
+              },
+              error: (err) => {
+                this.alert.error("Не удалось удалить запись. Попробуйте позже.");
+                this.confirmDeletionMessage = null;
+                console.error("Failed to delete currency record:", err);
+              }
             });

As per coding guidelines for Angular components and proper error handling.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.currenciesService
.delete(item.id)
.pipe(untilDestroyed(this))
.subscribe(() => {
this.alert.success("Запись была удалена");
this.confirmDeletionMessage = null;
this.loadData(this.currentPage);
});
this.currenciesService
.delete(item.id)
.pipe(untilDestroyed(this))
.subscribe({
next: () => {
this.alert.success("Запись была удалена");
this.confirmDeletionMessage = null;
this.loadData(this.currentPage);
},
error: (err) => {
this.alert.error("Не удалось удалить запись. Попробуйте позже.");
this.confirmDeletionMessage = null;
console.error("Failed to delete currency record:", err);
}
});
🤖 Prompt for AI Agents
In src/app/modules/admin/components/currencies-page/currencies-page.component.ts
around lines 113-120, the subscription to this.currenciesService.delete(...) has
no error handler; add an error callback to the subscribe so API failures are
handled: on success keep the existing behavior (show success alert, clear
confirmDeletionMessage, reload data), and on error call this.alert.error(...)
with a user-friendly message (and optionally log the error), ensure
confirmDeletionMessage is cleared or left intact per UX decision (prefer
clearing so dialog closes), and keep the untilDestroyed(this) operator; also
consider disabling any loading state before returning from the error handler.

},
),
);
}

showCurrenciesDetails(item: CurrencyItem): void {
this.selectedCurrencyItem = item;
}

closeCurrenciesDetails(): void {
this.selectedCurrencyItem = null;
}
}
Loading