From 6d03bdb108ac844ecffc6c82bfaf74783b984bdc Mon Sep 17 00:00:00 2001 From: Arjuna Date: Fri, 2 Jan 2026 19:40:11 +0700 Subject: [PATCH 1/2] feat: financial database & routes added --- Backend/internal/academic/handler.go | 10 +- Backend/internal/academic/router.go | 23 +++ Backend/internal/finance/handler.go | 190 ++++++++++++++++++ Backend/internal/finance/modelDB.go | 31 +++ Backend/internal/finance/router.go | 23 +++ Backend/main.go | 20 +- .../academic/services/academicService.tsx | 24 +-- .../src/features/financial/financial.tsx | 0 .../financial/services/financialService.tsx | 97 +++++++++ 9 files changed, 386 insertions(+), 32 deletions(-) create mode 100644 Backend/internal/academic/router.go create mode 100644 Backend/internal/finance/handler.go create mode 100644 Backend/internal/finance/modelDB.go create mode 100644 Backend/internal/finance/router.go create mode 100644 Frontend/project-CL/src/features/financial/financial.tsx create mode 100644 Frontend/project-CL/src/features/financial/services/financialService.tsx diff --git a/Backend/internal/academic/handler.go b/Backend/internal/academic/handler.go index e988590..cc470a2 100644 --- a/Backend/internal/academic/handler.go +++ b/Backend/internal/academic/handler.go @@ -40,12 +40,11 @@ func UpdateSubject(c *gin.Context) { c.JSON(404, gin.H{"failed update 1": "No data found"}) return } - var input Subject - if err := c.ShouldBindJSON(&input); err != nil { + if err := c.ShouldBindJSON(&subject); err != nil { c.JSON(400, gin.H{"failed update 2": err.Error()}) return } - database.DB.Model(&subject).Updates(input) + database.DB.Model(&subject).Updates(subject) c.JSON(200, gin.H{"message": "OK", "subject": subject}) } @@ -151,12 +150,11 @@ func UpdateNote(c *gin.Context) { c.JSON(404, gin.H{"message": err.Error()}) return } - var input Note - if err := c.ShouldBindJSON(&input); err != nil { + if err := c.ShouldBindJSON(¬e); err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } - database.DB.Model(¬e).Updates(input) + database.DB.Model(¬e).Updates(note) c.JSON(200, gin.H{"message": "OK", "note": note}) } diff --git a/Backend/internal/academic/router.go b/Backend/internal/academic/router.go new file mode 100644 index 0000000..dfa8b0f --- /dev/null +++ b/Backend/internal/academic/router.go @@ -0,0 +1,23 @@ +package academic + +import "github.com/gin-gonic/gin" + +func RegisterRoutes(r *gin.RouterGroup) { + academic := r.Group("/academic") + { + academic.POST("/subjects", CreateSubject) + academic.GET("/subjects", GetSubject) + academic.PUT("/subjects/:id", UpdateSubject) + academic.DELETE("/subjects/:id", DeleteSubject) + + academic.POST("/tasks", CreateTask) + academic.GET("/tasks", GetTask) + academic.PUT("/tasks/:id", UpdateTask) + academic.DELETE("/tasks/:id", DeleteTask) + + academic.POST("/notes", CreateNote) + academic.GET("/notes", GetNotes) + academic.PUT("/notes/:id", UpdateNote) + academic.DELETE("/notes/:id", DeleteNote) + } +} diff --git a/Backend/internal/finance/handler.go b/Backend/internal/finance/handler.go new file mode 100644 index 0000000..648c74e --- /dev/null +++ b/Backend/internal/finance/handler.go @@ -0,0 +1,190 @@ +package finance + +import ( + "personal-erp-backend/database" + + "github.com/gin-gonic/gin" +) + +func GetCategory(c *gin.Context) { + var category []Category + userID, _ := c.Get("userID") + if err := database.DB.Where("user_id = ?", userID).Find(&category).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"fetch success": category}) +} + +func CreateCategory(c *gin.Context) { + var category Category + userID, exist := c.Get("userID") + if err := c.BindJSON(&category); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + if exist { + category.UserID = uint(userID.(int)) + } + if err := database.DB.Create(&category).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"create": category}) +} + +func UpdateCategory(c *gin.Context) { + id := c.Param("id") + userID, _ := c.Get("userID") + var category Category + if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&category).Error; err != nil { + c.JSON(404, gin.H{"error": err.Error()}) + return + } + if err := c.BindJSON(&category); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + if err := database.DB.Model(&category).Updates(category).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"update": category}) +} + +func DeleteCategory(c *gin.Context) { + id := c.Param("id") + userID, _ := c.Get("userID") + var category Category + if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&category).Error; err != nil { + c.JSON(404, gin.H{"error": err.Error()}) + return + } + if err := database.DB.Delete(&category).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"delete": category}) +} + +func GetTransaction(c *gin.Context) { + var transaction []Transaction + userID, _ := c.Get("userID") + if err := database.DB.Where("user_id = ?", userID).Find(&transaction).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"fetch success": transaction}) +} + +func CreateTransaction(c *gin.Context) { + var transaction Transaction + userID, exist := c.Get("userID") + if err := c.BindJSON(&transaction); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + if exist { + transaction.UserID = uint(userID.(int)) + } + if err := database.DB.Create(&transaction).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"create": transaction}) +} + +func UpdateTransaction(c *gin.Context) { + id := c.Param("id") + userID, _ := c.Get("userID") + var transaction Transaction + if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&transaction).Error; err != nil { + c.JSON(404, gin.H{"error": err.Error()}) + return + } + if err := c.BindJSON(&transaction); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + if err := database.DB.Model(&transaction).Updates(transaction).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"update": transaction}) +} + +func DeleteTransaction(c *gin.Context) { + id := c.Param("id") + userID, _ := c.Get("userID") + var transaction Transaction + if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&transaction).Error; err != nil { + c.JSON(404, gin.H{"error": err.Error()}) + return + } + if err := database.DB.Delete(&transaction).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"delete": transaction}) +} + +func GetGoal(c *gin.Context) { + var goal []Goal + userID, _ := c.Get("userID") + if err := database.DB.Where("user_id = ?", userID).Find(&goal).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"goals": goal}) +} + +func CreateGoal(c *gin.Context) { + var goal Goal + userID, exist := c.Get("userID") + if err := c.BindJSON(&goal); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + if exist { + goal.UserID = uint(userID.(int)) + } + if err := database.DB.Create(&goal).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"create": goal}) +} + +func UpdateGoal(c *gin.Context) { + id := c.Param("id") + userID, _ := c.Get("userID") + var goal Goal + if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&goal).Error; err != nil { + c.JSON(404, gin.H{"error": err.Error()}) + return + } + if err := c.BindJSON(&goal); err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + if err := database.DB.Model(&goal).Updates(goal).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"update": goal}) +} + +func DeleteGoal(c *gin.Context) { + id := c.Param("id") + userID, _ := c.Get("userID") + var goal Goal + if err := database.DB.Where("id = ? AND user_id = ?", id, userID).First(&goal).Error; err != nil { + c.JSON(404, gin.H{"error": err.Error()}) + return + } + if err := database.DB.Delete(&goal).Error; err != nil { + c.JSON(400, gin.H{"error": err.Error()}) + return + } + c.JSON(200, gin.H{"delete": goal}) +} diff --git a/Backend/internal/finance/modelDB.go b/Backend/internal/finance/modelDB.go new file mode 100644 index 0000000..3da93bc --- /dev/null +++ b/Backend/internal/finance/modelDB.go @@ -0,0 +1,31 @@ +package finance + +import "time" + +type Category struct { + ID uint `gorm:"primary_key" json:"id"` + UserID uint `gorm:"index" json:"user_id"` + Name string `gorm:"size:255;not null" json:"name"` + Type string `gorm:"size:255;not null" json:"type"` + Bucket string `gorm:"size:255;not null" json:"bucket"` +} + +type Transaction struct { + ID uint `gorm:"primary_key" json:"id"` + UserID uint `gorm:"index" json:"user_id"` + CategoryID uint `gorm:"index" json:"category_id"` + Category Category `gorm:"foreignkey:CategoryID" json:"category"` + Date time.Time `gorm:"not null" json:"date"` + Amount float64 `gorm:"not null" json:"amount"` + Description string `gorm:"size:255;" json:"description"` +} + +type Goal struct { + ID uint `gorm:"primary_key" json:"id"` + UserID uint `gorm:"index" json:"user_id"` + Name string `gorm:"size:255;not null" json:"name"` + TargetAmount float64 `gorm:"not null" json:"target_amount"` + SavedAmount float64 `gorm:"not null" json:"saved_amount"` + Status string `gorm:"size:255;default:'active'" json:"status"` + Description string `gorm:"size:255;" json:"description"` +} diff --git a/Backend/internal/finance/router.go b/Backend/internal/finance/router.go new file mode 100644 index 0000000..9386cc0 --- /dev/null +++ b/Backend/internal/finance/router.go @@ -0,0 +1,23 @@ +package finance + +import "github.com/gin-gonic/gin" + +func RegisterRouter(r *gin.RouterGroup) { + finance := r.Group("/finance") + { + finance.GET("/category", GetCategory) + finance.POST("/category", CreateCategory) + finance.PUT("/category/:id", UpdateCategory) + finance.DELETE("/category/:id", DeleteCategory) + + finance.GET("/transaction", GetTransaction) + finance.POST("/transaction", CreateTransaction) + finance.PUT("/transaction/:id", UpdateTransaction) + finance.DELETE("/transaction/:id", DeleteTransaction) + + finance.GET("/goal", GetGoal) + finance.POST("/goal", CreateGoal) + finance.PUT("/goal/:id", UpdateGoal) + finance.DELETE("/goal/:id", DeleteGoal) + } +} diff --git a/Backend/main.go b/Backend/main.go index a2cf68b..21bffcb 100644 --- a/Backend/main.go +++ b/Backend/main.go @@ -5,6 +5,7 @@ import ( "personal-erp-backend/database" "personal-erp-backend/internal/academic" "personal-erp-backend/internal/auth" + "personal-erp-backend/internal/finance" "time" "github.com/gin-contrib/cors" @@ -19,6 +20,9 @@ func main() { &academic.Subject{}, &academic.Task{}, &academic.Note{}, + &finance.Category{}, + &finance.Transaction{}, + &finance.Goal{}, ) if err != nil { fmt.Println("gagal update database: ", err) @@ -38,20 +42,8 @@ func main() { api := r.Group("/api") api.Use(auth.Middleware()) { - api.POST("/subjects", academic.CreateSubject) - api.GET("/subjects", academic.GetSubject) - api.PUT("/subjects/:id", academic.UpdateSubject) - api.DELETE("/subjects/:id", academic.DeleteSubject) - - api.POST("/tasks", academic.CreateTask) - api.GET("/tasks", academic.GetTask) - api.PUT("/tasks/:id", academic.UpdateTask) - api.DELETE("/tasks/:id", academic.DeleteTask) - - api.POST("/notes", academic.CreateNote) - api.GET("/notes", academic.GetNotes) - api.PUT("/notes/:id", academic.UpdateNote) - api.DELETE("/notes/:id", academic.DeleteNote) + academic.RegisterRoutes(api) + finance.RegisterRouter(api) } r.Run(":8080") diff --git a/Frontend/project-CL/src/features/academic/services/academicService.tsx b/Frontend/project-CL/src/features/academic/services/academicService.tsx index db96339..8a04076 100644 --- a/Frontend/project-CL/src/features/academic/services/academicService.tsx +++ b/Frontend/project-CL/src/features/academic/services/academicService.tsx @@ -37,51 +37,51 @@ export interface Note { export const academicService = { getSubjects: async () => { - const response = await api.get('/subjects') + const response = await api.get('/academic/subjects') return response.data }, createSubject: async (data: Partial) => { - const response = await api.post('/subjects', data) + const response = await api.post('/academic/subjects', data) return response.data }, updateSubject: async (id: number, data: Partial) => { - const response = await api.put(`/subjects/${id}`, data) + const response = await api.put(`/academic/subjects/${id}`, data) return response.data }, deleteSubject: async (id: number) => { - const response = await api.delete(`/subjects/${id}`) + const response = await api.delete(`/academic/subjects/${id}`) return response.data }, getTasks: async () => { - const response = await api.get('/tasks') + const response = await api.get('/academic/tasks') return response.data }, createTask: async (data: Partial) => { - const response = await api.post('/tasks', data) + const response = await api.post('/academic/tasks', data) return response.data }, updateTask: async (id: number, data: Partial) => { - const response = await api.put(`/tasks/${id}`, data) + const response = await api.put(`/academic/tasks/${id}`, data) return response.data }, deleteTask: async (id: number) => { - const response = await api.delete(`/tasks/${id}`) + const response = await api.delete(`/academic/tasks/${id}`) return response.data }, getNotes: async () => { - const response = await api.get('/notes') + const response = await api.get('/academic/notes') return response.data }, createNote: async (data: Partial) => { - const response = await api.post('/notes', data) + const response = await api.post('/academic/notes', data) return response.data }, updateNote: async (id: number, data: Partial) => { - const response = await api.put(`/notes/${id}`, data) + const response = await api.put(`/academic/notes/${id}`, data) return response.data }, deleteNote: async (id: number) => { - const response = await api.delete(`/notes/${id}`) + const response = await api.delete(`/academic/notes/${id}`) return response.data } } diff --git a/Frontend/project-CL/src/features/financial/financial.tsx b/Frontend/project-CL/src/features/financial/financial.tsx new file mode 100644 index 0000000..e69de29 diff --git a/Frontend/project-CL/src/features/financial/services/financialService.tsx b/Frontend/project-CL/src/features/financial/services/financialService.tsx new file mode 100644 index 0000000..91384c2 --- /dev/null +++ b/Frontend/project-CL/src/features/financial/services/financialService.tsx @@ -0,0 +1,97 @@ +import api from "../../../services/api"; + +export interface Category { + id: number; + user_id: number; + name: string; + type: string; + bucket: string; +} + +export interface Transaction { + id: number; + user_id: number; + category_id: number; + category: Category; + date: string; // Go time.Time usually serializes to string ISO format + amount: number; + description: string; +} + +export interface Goal { + id: number; + user_id: number; + name: string; + target_amount: number; + saved_amount: number; + status: string; + description: string; +} + +export const financialService = { + // --- Categories --- + getCategories: async () => { + // Backend returns: { "fetch success": [...] } based on handler.go + const response = await api.get<{ "fetch success": Category[] }>('/finance/category'); + return response.data["fetch success"]; + }, + createCategory: async (data: Partial) => { + // Backend returns: { "create": ... } + const response = await api.post<{ create: Category }>('/finance/category', data); + return response.data.create; + }, + updateCategory: async (id: number, data: Partial) => { + // Backend returns: { "update": ... } + const response = await api.put<{ update: Category }>(`/finance/category/${id}`, data); + return response.data.update; + }, + deleteCategory: async (id: number) => { + // Backend returns: { "delete": ... } + const response = await api.delete<{ delete: Category }>(`/finance/category/${id}`); + return response.data.delete; + }, + + // --- Transactions --- + getTransactions: async () => { + // Backend returns: { "fetch success": [...] } + const response = await api.get<{ "fetch success": Transaction[] }>('/finance/transaction'); + return response.data["fetch success"]; + }, + createTransaction: async (data: Partial) => { + // Backend returns: { "create": ... } + const response = await api.post<{ create: Transaction }>('/finance/transaction', data); + return response.data.create; + }, + updateTransaction: async (id: number, data: Partial) => { + // Backend returns: { "update": ... } + const response = await api.put<{ update: Transaction }>(`/finance/transaction/${id}`, data); + return response.data.update; + }, + deleteTransaction: async (id: number) => { + // Backend returns: { "delete": ... } + const response = await api.delete<{ delete: Transaction }>(`/finance/transaction/${id}`); + return response.data.delete; + }, + + // --- Goals --- + getGoals: async () => { + // Backend returns: { "goals": [...] } NOTE: Different key than others + const response = await api.get<{ goals: Goal[] }>('/finance/goal'); + return response.data.goals; + }, + createGoal: async (data: Partial) => { + // Backend returns: { "create": ... } + const response = await api.post<{ create: Goal }>('/finance/goal', data); + return response.data.create; + }, + updateGoal: async (id: number, data: Partial) => { + // Backend returns: { "update": ... } + const response = await api.put<{ update: Goal }>(`/finance/goal/${id}`, data); + return response.data.update; + }, + deleteGoal: async (id: number) => { + // Backend returns: { "delete": ... } + const response = await api.delete<{ delete: Goal }>(`/finance/goal/${id}`); + return response.data.delete; + } +}; From 5a414d2f18e0e9021f82dc927b250ce4bbb90a71 Mon Sep 17 00:00:00 2001 From: Arjuna Date: Fri, 2 Jan 2026 20:11:47 +0700 Subject: [PATCH 2/2] feat: finance home with dashboard added --- .../financial/components/DashboardCards.tsx | 160 ++++++++++++++++++ .../src/features/financial/financial.tsx | 39 +++++ .../financial/hooks/useFinancialDashboard.ts | 119 +++++++++++++ Frontend/project-CL/src/main.tsx | 5 + 4 files changed, 323 insertions(+) create mode 100644 Frontend/project-CL/src/features/financial/components/DashboardCards.tsx create mode 100644 Frontend/project-CL/src/features/financial/hooks/useFinancialDashboard.ts diff --git a/Frontend/project-CL/src/features/financial/components/DashboardCards.tsx b/Frontend/project-CL/src/features/financial/components/DashboardCards.tsx new file mode 100644 index 0000000..64657ca --- /dev/null +++ b/Frontend/project-CL/src/features/financial/components/DashboardCards.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import type { DashboardMetrics } from '../hooks/useFinancialDashboard'; +import { Wallet, PiggyBank, ShieldAlert, AlertTriangle, CheckCircle, TrendingUp, Layers, Target } from 'lucide-react'; + +interface DashboardCardsProps { + metrics: DashboardMetrics; +} + +export const DashboardCards: React.FC = ({ metrics }) => { + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(amount); + }; + + // Budget Status Logic + const getBudgetStatusColor = (percentage: number) => { + if (percentage < 50) return { bg: 'bg-emerald-50', text: 'text-emerald-600', icon: CheckCircle }; + if (percentage > 80) return { bg: 'bg-rose-50', text: 'text-rose-600', icon: AlertTriangle }; + return { bg: 'bg-amber-50', text: 'text-amber-600', icon: TrendingUp }; + }; + + const statusStyle = getBudgetStatusColor(metrics.budgetStatus); + const StatusIcon = statusStyle.icon; + + return ( +
+
+ + {/* LEFT COLUMN: PRIMARY STATS (Balance, Safe to Spend, Budget Status) */} +
+ + {/* Main Balance Card (Like IPK Card) */} +
+
+ +
+

Remaining Wallet

+
+ {formatCurrency(metrics.balance)} +
+

+ Calculated from Income - Expenses +

+
+ + {/* Safe to Spend & Budget Status Row */} +
+ {/* Daily Safe to Spend */} +
+
+
+ +
+

Daily Safe Limit

+
+
+
{formatCurrency(metrics.dailySafeToSpend)}
+

Per day remaining

+
+
+ + {/* Budget Status */} +
+
+
+ +
+

Budget Health

+
+
+
+ {metrics.budgetStatus.toFixed(1)}% Used +
+
+
+
+
+
+
+
+ + {/* RIGHT COLUMN: BREAKDOWNS & SAVINGS (Like Nav Buttons) */} +
+ {/* Total Savings - Featured Top Right */} + } + color="bg-emerald-50 text-emerald-600" + /> + +
+

+ + Expense Breakdown +

+
+ +
+ } // Using Target for Needs/Essentials + color="bg-rose-50 text-rose-600" + /> + } + color="bg-orange-50 text-orange-600" + /> + } + color="bg-blue-50 text-blue-600" + /> + } + color="bg-slate-100 text-slate-600" + /> +
+
+
+
+ ); +}; + +// Helper Component similar to Academic's NavButton but for Stats +interface StatRowProps { + title: string; + value: string; + desc: string; + icon: React.ReactNode; + color: string; +} + +const StatRow: React.FC = ({ title, value, desc, icon, color }) => { + return ( +
+
+ {icon} +
+
{/* min-w-0 for truncate to work */} +

{title}

+

{value}

+

{desc}

+
+
+ ); +} diff --git a/Frontend/project-CL/src/features/financial/financial.tsx b/Frontend/project-CL/src/features/financial/financial.tsx index e69de29..b0d7cf3 100644 --- a/Frontend/project-CL/src/features/financial/financial.tsx +++ b/Frontend/project-CL/src/features/financial/financial.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useFinancialDashboard } from './hooks/useFinancialDashboard'; +import { DashboardCards } from './components/DashboardCards'; +import Header from "../../components/Header"; + +const Financial: React.FC = () => { + const { metrics, loading, error } = useFinancialDashboard(); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+
+
+ + {/* Future: Toolbar / Filters similar to Academic could go here */} +
+ + {/* View Area */} +
+ {!error ? ( +
+ {error} +
+ ) : ( + + )} +
+
+ ); +}; + +export default Financial; diff --git a/Frontend/project-CL/src/features/financial/hooks/useFinancialDashboard.ts b/Frontend/project-CL/src/features/financial/hooks/useFinancialDashboard.ts new file mode 100644 index 0000000..4e7e9a8 --- /dev/null +++ b/Frontend/project-CL/src/features/financial/hooks/useFinancialDashboard.ts @@ -0,0 +1,119 @@ +import { useState, useEffect } from 'react'; +import { financialService, type Transaction, type Category, type Goal } from '../services/financialService'; + +export interface DashboardMetrics { + totalIncome: number; + totalExpense: number; + balance: number; + budgetStatus: number; // Percentage (0-100+) + dailySafeToSpend: number; + breakdown: { + needs: number; + wants: number; + savings: number; + invest: number; + others: number; + }; + goalsTotalSaved: number; +} + +export const useFinancialDashboard = () => { + const [transactions, setTransactions] = useState([]); + const [categories, setCategories] = useState([]); + const [goals, setGoals] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [metrics, setMetrics] = useState({ + totalIncome: 0, + totalExpense: 0, + balance: 0, + budgetStatus: 0, + dailySafeToSpend: 0, + breakdown: { needs: 0, wants: 0, savings: 0, invest: 0, others: 0 }, + goalsTotalSaved: 0 + }); + + useEffect(() => { + const fetchData = async () => { + try { + const [txData, catData, goalData] = await Promise.all([ + financialService.getTransactions(), + financialService.getCategories(), + financialService.getGoals() + ]); + + setTransactions(txData); + setCategories(catData); + setGoals(goalData); + + calculateMetrics(txData, catData, goalData); + } catch (err) { + console.error("Failed to fetch financial data", err); + setError("Failed to load dashboard data"); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + const calculateMetrics = (txs: Transaction[], cats: Category[], goals: Goal[]) => { + let income = 0; + let expense = 0; + const breakdown = { needs: 0, wants: 0, savings: 0, invest: 0, others: 0 }; + + // Helper to map category ID to bucket + const categoryMap = new Map(); + cats.forEach(c => categoryMap.set(c.id, c.bucket?.toLowerCase() || 'others')); + + txs.forEach(tx => { + // Determine if it's income or expense based on Category Type or Amount sign + // Assuming Category has 'type' which could be 'income' or 'expense' + // We need to fetch the category for this transaction to know the type/bucket + const bucket = categoryMap.get(tx.category_id) || 'others'; + const category = cats.find(c => c.id === tx.category_id); + + if (category?.type === 'income') { + income += tx.amount; + } else { + expense += tx.amount; + // Add to breakdown + switch (bucket) { + case 'needs': breakdown.needs += tx.amount; break; + case 'wants': breakdown.wants += tx.amount; break; + case 'savings': breakdown.savings += tx.amount; break; + case 'invest': breakdown.invest += tx.amount; break; + default: breakdown.others += tx.amount; break; + } + } + }); + + const balance = income - expense; + const budgetStatus = income > 0 ? (expense / income) * 100 : 0; + + // Goals total saved + const goalsSaved = goals.reduce((acc, g) => acc + g.saved_amount, 0); + + // Safe to spend + // Logic: (Balance - Planned Savings?) / Days remaining? + // User asked for "Daily safe to spend". Simple version: Balance / Days remaining in month. + const today = new Date(); + const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); + const daysRemaining = Math.max(1, lastDayOfMonth.getDate() - today.getDate()); // Prevent div by 0 + const dailySafe = balance / daysRemaining; + + setMetrics({ + totalIncome: income, + totalExpense: expense, + balance, + budgetStatus, + dailySafeToSpend: dailySafe, + breakdown, + goalsTotalSaved: goalsSaved + }); + }; + + return { metrics, loading, error, transactions, categories, goals }; +}; diff --git a/Frontend/project-CL/src/main.tsx b/Frontend/project-CL/src/main.tsx index 3d77e93..a0a0622 100644 --- a/Frontend/project-CL/src/main.tsx +++ b/Frontend/project-CL/src/main.tsx @@ -7,6 +7,7 @@ import { createBrowserRouter } from 'react-router' import Academic from './features/academic/academic' import SidebarLayout from './layouts/SidebarLayout' import { SubjectProvider } from './features/academic/hooks/useSubject' +import Financial from './features/financial/financial' const router = createBrowserRouter([ { @@ -20,6 +21,10 @@ const router = createBrowserRouter([ path: '/academic', element: , }, + { + path: '/finance', + element: , + }, ] } ])