Skip to content

Commit 0a81323

Browse files
committed
add savings view
1 parent 0a031d9 commit 0a81323

11 files changed

Lines changed: 1161 additions & 158 deletions

File tree

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/* Animations */
2+
@keyframes fadeIn {
3+
from {
4+
opacity: 0;
5+
}
6+
7+
to {
8+
opacity: 1;
9+
}
10+
}
11+
12+
@keyframes slideUp {
13+
from {
14+
transform: translateY(20px);
15+
opacity: 0;
16+
}
17+
18+
to {
19+
transform: translateY(0);
20+
opacity: 1;
21+
}
22+
}
23+
24+
.animate-fade-in {
25+
animation: fadeIn 0.5s ease-out;
26+
}
27+
28+
.animate-slide-up {
29+
animation: slideUp 0.4s ease-out;
30+
}
31+
32+
.animate-bounce-subtle {
33+
animation: bounce 2s infinite;
34+
}
35+
36+
@keyframes bounce {
37+
38+
0%,
39+
100% {
40+
transform: translateY(-5%);
41+
}
42+
43+
50% {
44+
transform: translateY(0);
45+
}
46+
}
47+
48+
49+
input[type="date"]::-webkit-calendar-picker-indicator,
50+
input[type="month"]::-webkit-calendar-picker-indicator {
51+
filter: var(--icon-color2);
52+
cursor: pointer;
53+
}
54+
55+
input[type="date"],
56+
input[type="month"] {
57+
border: none;
58+
outline: none;
59+
cursor: pointer;
60+
}
61+
62+
.hiddeninput {
63+
z-index: 50;
64+
background: none;
65+
width: 100px;
66+
color: transparent;
67+
}
68+
69+
.hiddeninputdiv {
70+
right: -23px;
71+
}

src/app/component/income-components/saving/saving.component.html

Lines changed: 406 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { SavingComponent } from './saving.component';
4+
5+
describe('TestComponent', () => {
6+
let component: SavingComponent;
7+
let fixture: ComponentFixture<SavingComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [SavingComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(SavingComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { CommonModule, DecimalPipe, DatePipe } from '@angular/common';
3+
import { FormsModule } from '@angular/forms';
4+
import { GoalService, Goal } from '../../../service/localStorage/goal.service';
5+
import { SavingsService, Saving } from '../../../service/localStorage/savings.service';
6+
import { ConfigService } from '../../../service/config/config.service';
7+
8+
@Component({
9+
selector: 'app-saving',
10+
standalone: true,
11+
imports: [CommonModule, DecimalPipe, DatePipe, FormsModule],
12+
templateUrl: './saving.component.html',
13+
styleUrl: './saving.component.css'
14+
})
15+
export class SavingComponent implements OnInit {
16+
currentGoal: Goal | null = null;
17+
savings: Saving[] = [];
18+
19+
showSavingModal = false;
20+
showGoalModal = false;
21+
showGoalDetailsModal = false;
22+
editingSavingId: string | null = null;
23+
24+
todayDateStr = '';
25+
savingForm: Partial<Saving> = {};
26+
goalForm: Partial<Goal> = {};
27+
28+
// Dashboard Stats
29+
totalSavedAmount = 0;
30+
goalTarget = 0;
31+
progressPercentage = 0;
32+
remainingPercentage = 0;
33+
currentMonthAdded = 0;
34+
averageSavedPerDay = 0;
35+
36+
constructor(
37+
private goalService: GoalService,
38+
private savingsService: SavingsService,
39+
private configService: ConfigService
40+
) {
41+
this.todayDateStr = this.configService.getLocalTime().split('T')[0];
42+
}
43+
44+
ngOnInit() {
45+
this.refreshData();
46+
}
47+
48+
refreshData(): void {
49+
this.currentGoal = this.goalService.getAll()[0] || null;
50+
this.savings = this.savingsService.getAll() || [];
51+
this.calculateStats();
52+
}
53+
54+
calculateStats() {
55+
this.totalSavedAmount = this.savings.reduce((sum, s) => sum + Number(s.amount || 0), 0);
56+
this.goalTarget = Number(this.currentGoal?.target_amount || 0);
57+
58+
const progress = this.goalTarget > 0 ? (this.totalSavedAmount / this.goalTarget) * 100 : 0;
59+
this.progressPercentage = Math.min(Math.max(progress, 0), 100);
60+
this.remainingPercentage = Math.max(100 - this.progressPercentage, 0);
61+
62+
const now = new Date();
63+
this.currentMonthAdded = (this.goalTarget - this.totalSavedAmount) > 0 ? this.goalTarget - this.totalSavedAmount : 0;
64+
65+
if (this.currentGoal?.start_date && this.totalSavedAmount > 0) {
66+
const start = new Date(this.currentGoal.start_date).getTime();
67+
const today = new Date().getTime();
68+
let days = Math.ceil((today - start) / (1000 * 3600 * 24));
69+
this.averageSavedPerDay = this.totalSavedAmount / (days <= 0 ? 1 : days);
70+
} else {
71+
this.averageSavedPerDay = 0;
72+
}
73+
}
74+
75+
/* ---------- Validation Getters ---------- */
76+
77+
get savingDateError(): string | null {
78+
if (!this.savingForm.date) return "Date is required.";
79+
if (new Date(this.savingForm.date) > new Date(this.todayDateStr)) return "Date cannot be in the future.";
80+
return null;
81+
}
82+
83+
get isSavingFormValid(): boolean {
84+
return !!this.savingForm.amount && this.savingForm.amount > 0 && !this.savingDateError;
85+
}
86+
87+
get goalDateError(): string | null {
88+
const { start_date, target_date } = this.goalForm;
89+
90+
if (!start_date || !target_date) {
91+
return "Both Start Date and Target Date are required.";
92+
}
93+
94+
if (new Date(start_date) > new Date(target_date)) {
95+
return "Start date cannot be after target date.";
96+
}
97+
98+
return null;
99+
}
100+
101+
get isGoalFormValid(): boolean {
102+
return (
103+
!!this.goalForm.goal_name &&
104+
!!this.goalForm.target_amount &&
105+
this.goalForm.target_amount > 0 &&
106+
!!this.goalForm.start_date && // Ensure start date exists
107+
!!this.goalForm.target_date && // Ensure target date exists
108+
!this.goalDateError
109+
);
110+
}
111+
/* ---------- Actions ---------- */
112+
113+
saveSavingsData() {
114+
if (!this.isSavingFormValid) return;
115+
116+
const savingData: Saving = {
117+
saving_id: this.editingSavingId || 'sav_' + Date.now(),
118+
amount: Number(this.savingForm.amount),
119+
date: this.savingForm.date!,
120+
note: this.savingForm.note || ''
121+
};
122+
123+
if (this.editingSavingId) {
124+
this.savingsService.update(this.editingSavingId, savingData);
125+
} else {
126+
this.savingsService.add(savingData);
127+
}
128+
129+
this.closeModals();
130+
this.refreshData();
131+
}
132+
133+
saveGoalData() {
134+
if (!this.isGoalFormValid) return;
135+
136+
const goalData: Goal = {
137+
goal_id: this.currentGoal?.goal_id || 'goal_' + Date.now(),
138+
goal_name: this.goalForm.goal_name!,
139+
target_amount: Number(this.goalForm.target_amount),
140+
start_date: this.goalForm.start_date!,
141+
target_date: this.goalForm.target_date || '',
142+
note: this.goalForm.note || ''
143+
};
144+
145+
if (this.currentGoal) {
146+
this.goalService.update(goalData.goal_id, goalData);
147+
} else {
148+
this.goalService.add(goalData);
149+
}
150+
151+
this.closeModals();
152+
this.refreshData();
153+
}
154+
155+
deleteSaving(id: string) {
156+
if (confirm('Delete this saving record?')) {
157+
this.savingsService.delete(id);
158+
this.refreshData();
159+
}
160+
}
161+
162+
deleteGoal() {
163+
if (confirm('Delete your entire goal? Progress will be kept but the target will be removed.')) {
164+
if (this.currentGoal) {
165+
this.goalService.delete(this.currentGoal.goal_id);
166+
this.currentGoal = null;
167+
this.refreshData();
168+
}
169+
this.closeModals();
170+
}
171+
}
172+
173+
// --- Modal Control ---
174+
openAddModal() {
175+
this.editingSavingId = null;
176+
this.savingForm = { amount: undefined, date: this.todayDateStr, note: '' };
177+
this.showSavingModal = true;
178+
}
179+
180+
editSaving(saving: Saving) {
181+
this.editingSavingId = saving.saving_id;
182+
this.savingForm = { ...saving };
183+
this.showSavingModal = true;
184+
}
185+
186+
openGoalModal() {
187+
this.showGoalDetailsModal = false;
188+
this.goalForm = this.currentGoal ? { ...this.currentGoal } : {
189+
goal_name: '', target_amount: undefined, start_date: this.todayDateStr, target_date: '', note: ''
190+
};
191+
this.showGoalModal = true;
192+
}
193+
194+
handleGoalCardClick() {
195+
this.currentGoal ? (this.showGoalDetailsModal = true) : this.openGoalModal();
196+
}
197+
198+
closeModals() {
199+
this.showSavingModal = false;
200+
this.showGoalModal = false;
201+
this.showGoalDetailsModal = false;
202+
}
203+
}

0 commit comments

Comments
 (0)