-
+
+
+
@if (eyes()) {
@@ -23,7 +27,9 @@
Overview
}
-
+
+
+
@if (eyes()) {
{{ totalIn() | currency: "BRL" }}
@@ -33,7 +39,9 @@
Overview
}
-
+
+
+
@if (eyes()) {
{{ totalExpenses() | currency: "BRL" }}
@@ -72,14 +80,24 @@
Overview
[severity]="record.transaction == 'IN' ? 'success' : 'danger'"
>
- {{ record.createdAt }} |
+ {{ record.date }} |
-
+
|
@@ -91,3 +109,87 @@ Overview
[totalRecords]="totalRecords()"
[rowsPerPageOptions]="[10, 20, 30]"
/>
+
+
+
+
diff --git a/timeless-api/src/main/webui/src/app/components/records/records.component.ts b/timeless-api/src/main/webui/src/app/components/records/records.component.ts
index 1b650e0..ba47a11 100644
--- a/timeless-api/src/main/webui/src/app/components/records/records.component.ts
+++ b/timeless-api/src/main/webui/src/app/components/records/records.component.ts
@@ -1,18 +1,48 @@
import { Component, inject, signal, HostListener } from '@angular/core';
import { TableModule } from 'primeng/table';
import { Tag } from 'primeng/tag';
-import { CurrencyPipe } from '@angular/common';
+import { CurrencyPipe, CommonModule } from '@angular/common';
import {
RecordResponseItem,
TimelessApiService,
+ UpdateRecord,
} from '../../timeless-api.service';
import { Paginator, PaginatorState } from 'primeng/paginator';
import { Card } from 'primeng/card';
import { Button } from 'primeng/button';
+import { Dialog } from 'primeng/dialog';
+import { InputText } from 'primeng/inputtext';
+import { InputNumber } from 'primeng/inputnumber';
+import { Select } from 'primeng/select';
+import { DatePicker } from 'primeng/datepicker';
+import {
+ FormBuilder,
+ FormGroup,
+ ReactiveFormsModule,
+ Validators,
+} from '@angular/forms';
+import { Toast } from 'primeng/toast';
+import { MessageService } from 'primeng/api';
@Component({
selector: 'app-records',
- imports: [TableModule, Tag, CurrencyPipe, Paginator, Card, Button],
+ imports: [
+ TableModule,
+ Tag,
+ CurrencyPipe,
+ Paginator,
+ Card,
+ Button,
+ Dialog,
+ InputText,
+ InputNumber,
+ Select,
+ DatePicker,
+ ReactiveFormsModule,
+ CommonModule,
+ Toast,
+ ],
+ providers: [MessageService],
templateUrl: './records.component.html',
styleUrl: './records.component.scss',
})
@@ -28,6 +58,34 @@ export class RecordsComponent {
totalExpenses = signal(0);
hideTag = signal(false);
isMobile = signal(false);
+ editDialogVisible = signal(false);
+ selectedRecord = signal(null);
+
+ private fb = inject(FormBuilder);
+ private messageService = inject(MessageService);
+ editForm: FormGroup = this.fb.group({
+ amount: [0, [Validators.required, Validators.min(0)]],
+ description: ['', [Validators.required]],
+ transaction: ['OUT', [Validators.required]],
+ category: ['GENERAL', [Validators.required]],
+ date: [new Date(), [Validators.required]],
+ });
+
+ categories = [
+ { label: 'Custos Fixos', value: 'FIXED_COSTS' },
+ { label: 'Lazer', value: 'PLEASURES' },
+ { label: 'Conhecimento', value: 'KNOWLEDGE' },
+ { label: 'Metas', value: 'GOALS' },
+ { label: 'Conforto', value: 'COMFORT' },
+ { label: 'Liberdade Financeira', value: 'FINANCIAL_FREEDOM' },
+ { label: 'Geral', value: 'GENERAL' },
+ { label: 'Nenhum', value: 'NONE' },
+ ];
+
+ transactions = [
+ { label: 'Entrada', value: 'IN' },
+ { label: 'Saída', value: 'OUT' },
+ ];
constructor() {
this.checkScreenSize();
@@ -66,6 +124,57 @@ export class RecordsComponent {
});
}
+ showEditDialog(record: RecordResponseItem) {
+ this.selectedRecord.set(record);
+ const dateParts = record.date.split('/');
+ const dateObj = new Date(+dateParts[2], +dateParts[1] - 1, +dateParts[0]);
+
+ this.editForm.patchValue({
+ amount: record.amount,
+ description: record.description,
+ transaction: record.transaction,
+ category: record.category,
+ date: dateObj,
+ });
+ this.editDialogVisible.set(true);
+ }
+
+ saveEdit() {
+ if (this.editForm.valid && this.selectedRecord()) {
+ const formValue = this.editForm.value;
+ const formattedDate = formValue.date.toISOString().split('T')[0];
+
+ const updateRequest: UpdateRecord = {
+ ...formValue,
+ transactionDate: formattedDate,
+ };
+
+ this.timelessApiService
+ .updateRecord(this.selectedRecord()!.id, updateRequest)
+ .subscribe({
+ next: () => {
+ this.editDialogVisible.set(false);
+ this.populatePaginator();
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Sucesso',
+ detail: 'Registro atualizado com sucesso!',
+ life: 3000,
+ });
+ },
+ error: (error) => {
+ console.error('Error updating record:', error);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Erro',
+ detail: 'Falha ao atualizar registro.',
+ life: 3000,
+ });
+ },
+ });
+ }
+ }
+
changeEyes() {
this.eyes.update((value) => !value);
}
@@ -77,8 +186,25 @@ export class RecordsComponent {
}
deleteRecord(id: number) {
- this.timelessApiService.deleteRecord(id).subscribe(() => {
- this.populatePaginator();
+ this.timelessApiService.deleteRecord(id).subscribe({
+ next: () => {
+ this.populatePaginator();
+ this.messageService.add({
+ severity: 'success',
+ summary: 'Sucesso',
+ detail: 'Registro excluído com sucesso!',
+ life: 3000,
+ });
+ },
+ error: (error) => {
+ console.error('Error deleting record:', error);
+ this.messageService.add({
+ severity: 'error',
+ summary: 'Erro',
+ detail: 'Falha ao excluir registro.',
+ life: 3000,
+ });
+ },
});
}
}
diff --git a/timeless-api/src/main/webui/src/app/timeless-api.service.ts b/timeless-api/src/main/webui/src/app/timeless-api.service.ts
index 35d4945..752c691 100644
--- a/timeless-api/src/main/webui/src/app/timeless-api.service.ts
+++ b/timeless-api/src/main/webui/src/app/timeless-api.service.ts
@@ -58,6 +58,10 @@ export class TimelessApiService {
return this.httpClient.delete(`/api/records/${id}`);
}
+ updateRecord(id: number, record: UpdateRecord) {
+ return this.httpClient.put(`/api/records/${id}`, record);
+ }
+
logout() {
localStorage.removeItem(timelessLocalStorageKey);
}
@@ -75,10 +79,15 @@ export interface RecordPageResponse {
}
export interface RecordResponseItem {
+ id: number;
amount: number;
description: string;
- transaction: string;
+ transaction: 'IN' | 'OUT';
+ date: string;
createdAt: string;
+ category: string;
+ tag?: string;
+ icon?: string;
}
export interface UpdateUser {
@@ -88,3 +97,13 @@ export interface UpdateUser {
email: string;
phoneNumber: string;
}
+
+export interface UpdateRecord {
+ id: number;
+ amount: number;
+ description: string;
+ transaction: 'IN' | 'OUT';
+ date: string;
+ category: string;
+ tag?: string;
+}
diff --git a/timeless-api/src/test/java/dev/matheuscruz/presentation/RecordResourceTest.java b/timeless-api/src/test/java/dev/matheuscruz/presentation/RecordResourceTest.java
new file mode 100644
index 0000000..1719591
--- /dev/null
+++ b/timeless-api/src/test/java/dev/matheuscruz/presentation/RecordResourceTest.java
@@ -0,0 +1,108 @@
+package dev.matheuscruz.presentation;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.is;
+
+import dev.matheuscruz.domain.Categories;
+import dev.matheuscruz.domain.Record;
+import dev.matheuscruz.domain.RecordRepository;
+import dev.matheuscruz.domain.Transactions;
+import dev.matheuscruz.domain.User;
+import dev.matheuscruz.domain.UserRepository;
+import dev.matheuscruz.presentation.data.UpdateRecordRequest;
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+import io.quarkus.test.security.oidc.Claim;
+import io.quarkus.test.security.oidc.OidcSecurity;
+import jakarta.inject.Inject;
+import jakarta.persistence.EntityManager;
+import jakarta.transaction.Transactional;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+@QuarkusTest
+class RecordResourceTest {
+
+ @Inject
+ RecordRepository recordRepository;
+
+ @Inject
+ UserRepository userRepository;
+
+ @Inject
+ EntityManager em;
+
+ public static LocalDate fixedDate = LocalDate.of(2026, 1, 1);
+
+ @BeforeEach
+ @Transactional
+ void setUp() {
+ recordRepository.deleteAll();
+ userRepository.deleteAll();
+ }
+
+ @Test
+ @TestSecurity(user = "testUser", roles = "user")
+ @OidcSecurity(claims = { @Claim(key = "upn", value = "testUser") })
+ void shouldUpdateRecord() {
+ // Given
+ Record record = new Record.Builder().userId("testUser").amount(new BigDecimal("100.00"))
+ .description("Original Description").transaction(Transactions.OUT).category(Categories.FIXED_COSTS)
+ .transactionDate(fixedDate).build();
+
+ saveRecord(record);
+
+ UpdateRecordRequest request = new UpdateRecordRequest(new BigDecimal("150.00"), "Updated Description",
+ Transactions.IN, Categories.FINANCIAL_FREEDOM, fixedDate);
+
+ // When
+ given().contentType("application/json").body(request).when().put("/api/records/" + record.getId()).then()
+ .statusCode(204);
+
+ // Then
+ em.clear();
+ Record updatedRecord = recordRepository.findById(record.getId());
+ assert updatedRecord != null;
+ assert updatedRecord.getAmount().compareTo(new BigDecimal("150.00")) == 0;
+ assert updatedRecord.getDescription().equals("Updated Description");
+ assert updatedRecord.getTransaction() == Transactions.IN;
+ assert updatedRecord.getCategory() == Categories.FINANCIAL_FREEDOM;
+ assert updatedRecord.getTransactionDate().equals(fixedDate);
+ }
+
+ @Test
+ @TestSecurity(user = "otherUser", roles = "user")
+ @OidcSecurity(claims = { @Claim(key = "upn", value = "otherUser") })
+ void shouldNotUpdateRecordOfAnotherUser() {
+ // Given
+ Record record = new Record.Builder().userId("testUser").amount(new BigDecimal("100.00"))
+ .description("Original Description").transaction(Transactions.OUT).category(Categories.FIXED_COSTS)
+ .transactionDate(fixedDate).build();
+
+ saveRecord(record);
+
+ UpdateRecordRequest request = new UpdateRecordRequest(new BigDecimal("150.00"), "Updated Description",
+ Transactions.IN, Categories.FINANCIAL_FREEDOM, fixedDate);
+
+ // When
+ given().contentType("application/json").body(request).when().put("/api/records/" + record.getId()).then()
+ .statusCode(403);
+ }
+
+ @Test
+ @TestSecurity(user = "testUser", roles = "user")
+ @OidcSecurity(claims = { @Claim(key = "upn", value = "testUser") })
+ void shouldReturnNotFoundWhenRecordDoesNotExist() {
+ UpdateRecordRequest request = new UpdateRecordRequest(new BigDecimal("150.00"), "Updated Description",
+ Transactions.IN, Categories.FINANCIAL_FREEDOM, LocalDate.now());
+
+ given().contentType("application/json").body(request).when().put("/api/records/999").then().statusCode(404);
+ }
+
+ @Transactional
+ void saveRecord(Record record) {
+ recordRepository.persist(record);
+ }
+}