From fc2e6f7363db4fc8229271220088cbd8e2cafe4a Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 3 Jan 2026 10:39:17 +0700 Subject: [PATCH 1/2] feat: fin category added & home redesign --- .../src/features/academic/academic.tsx | 3 +- .../financial/components/AddCategoryModal.tsx | 130 +++++++++++++++ .../financial/components/DashboardCards.tsx | 90 ++++++++--- .../components/DeleteCategoryModal.tsx | 38 +++++ .../components/views/BudgetingView.tsx | 14 ++ .../components/views/CategoryView.tsx | 111 +++++++++++++ .../financial/components/views/GoalsView.tsx | 14 ++ .../components/views/TransactionView.tsx | 14 ++ .../src/features/financial/financial.tsx | 152 +++++++++++++++++- .../financial/hooks/useFinancialDashboard.ts | 47 +++--- 10 files changed, 564 insertions(+), 49 deletions(-) create mode 100644 Frontend/project-CL/src/features/financial/components/AddCategoryModal.tsx create mode 100644 Frontend/project-CL/src/features/financial/components/DeleteCategoryModal.tsx create mode 100644 Frontend/project-CL/src/features/financial/components/views/BudgetingView.tsx create mode 100644 Frontend/project-CL/src/features/financial/components/views/CategoryView.tsx create mode 100644 Frontend/project-CL/src/features/financial/components/views/GoalsView.tsx create mode 100644 Frontend/project-CL/src/features/financial/components/views/TransactionView.tsx diff --git a/Frontend/project-CL/src/features/academic/academic.tsx b/Frontend/project-CL/src/features/academic/academic.tsx index 8af1cbf..3e6ab06 100644 --- a/Frontend/project-CL/src/features/academic/academic.tsx +++ b/Frontend/project-CL/src/features/academic/academic.tsx @@ -123,13 +123,12 @@ export default function Academic() { - diff --git a/Frontend/project-CL/src/features/financial/components/AddCategoryModal.tsx b/Frontend/project-CL/src/features/financial/components/AddCategoryModal.tsx new file mode 100644 index 0000000..dabf039 --- /dev/null +++ b/Frontend/project-CL/src/features/financial/components/AddCategoryModal.tsx @@ -0,0 +1,130 @@ +import React, { useState, useEffect } from 'react'; +import { X } from 'lucide-react'; +import type { Category } from '../services/financialService'; + +interface AddCategoryModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (name: string, type: 'income' | 'expense', bucket: string) => Promise; + category?: Category | null; // Optional category for editing +} + +export default function AddCategoryModal({ isOpen, onClose, onSave, category }: AddCategoryModalProps) { + const [name, setName] = useState(''); + const [type, setType] = useState<'income' | 'expense'>('expense'); + const [bucket, setBucket] = useState('needs'); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (isOpen && category) { + setName(category.name); + setType(category.type as 'income' | 'expense'); + setBucket(category.bucket); + } else if (isOpen && !category) { + // Reset for add mode + setName(''); + setType('expense'); + setBucket('needs'); + } + }, [isOpen, category]); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + // If income, bucket might be irrelevant, but keeping it simple or setting to 'income' + const finalBucket = type === 'income' ? 'income' : bucket; + await onSave(name, type, finalBucket); + setLoading(false); + }; + + return ( +
+
+
+

{category ? 'Edit Category' : 'Add New Category'}

+ +
+ +
+ {/* Name Input */} +
+ + setName(e.target.value)} + placeholder="e.g., Groceries, Salary, Netflix" + className="w-full px-4 py-2 rounded-xl border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition-all" + /> +
+ + {/* Type Selection */} +
+ +
+ + +
+
+ + {/* Bucket Selection (Only for Expense) */} + {type === 'expense' && ( +
+ + +
+ )} + +
+ + +
+
+
+
+ ); +} diff --git a/Frontend/project-CL/src/features/financial/components/DashboardCards.tsx b/Frontend/project-CL/src/features/financial/components/DashboardCards.tsx index 64657ca..c27f00b 100644 --- a/Frontend/project-CL/src/features/financial/components/DashboardCards.tsx +++ b/Frontend/project-CL/src/features/financial/components/DashboardCards.tsx @@ -1,12 +1,13 @@ import React from 'react'; import type { DashboardMetrics } from '../hooks/useFinancialDashboard'; -import { Wallet, PiggyBank, ShieldAlert, AlertTriangle, CheckCircle, TrendingUp, Layers, Target } from 'lucide-react'; +import { Wallet, PiggyBank, ShieldAlert, AlertTriangle, CheckCircle, TrendingUp, Layers, Target, Tags, ArrowRightLeft, PieChart } from 'lucide-react'; interface DashboardCardsProps { metrics: DashboardMetrics; + onViewChange: (view: 'home' | 'category' | 'transaction' | 'budgeting' | 'goals') => void; } -export const DashboardCards: React.FC = ({ metrics }) => { +export const DashboardCards: React.FC = ({ metrics, onViewChange }) => { const formatCurrency = (amount: number) => { return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(amount); }; @@ -25,11 +26,11 @@ export const DashboardCards: React.FC = ({ metrics }) => {
- {/* LEFT COLUMN: PRIMARY STATS (Balance, Safe to Spend, Budget Status) */} + {/* LEFT COLUMN: PRIMARY STATS (Balance, Safe to Spend, Budget Status) + NAVIGATION */}
- {/* Main Balance Card (Like IPK Card) */} -
+ {/* Main Balance Card */} +
@@ -43,7 +44,7 @@ export const DashboardCards: React.FC = ({ metrics }) => {
{/* Safe to Spend & Budget Status Row */} -
+
{/* Daily Safe to Spend */}
@@ -68,7 +69,7 @@ export const DashboardCards: React.FC = ({ metrics }) => {
- {metrics.budgetStatus.toFixed(1)}% Used + {metrics.budgetStatus.toFixed(1)}% Used
= ({ metrics }) => {
+ + {/* Navigation Grid (Quick Actions) */} +
+ } + color="bg-pink-50 text-pink-600 hover:bg-pink-100" + onClick={() => onViewChange('category')} + /> + } + color="bg-indigo-50 text-indigo-600 hover:bg-indigo-100" + onClick={() => onViewChange('transaction')} + /> + } + color="bg-cyan-50 text-cyan-600 hover:bg-cyan-100" + onClick={() => onViewChange('budgeting')} + /> + } + color="bg-teal-50 text-teal-600 hover:bg-teal-100" + onClick={() => onViewChange('goals')} + /> +
- {/* RIGHT COLUMN: BREAKDOWNS & SAVINGS (Like Nav Buttons) */} + {/* RIGHT COLUMN: BREAKDOWNS & SAVINGS */}
{/* Total Savings - Featured Top Right */} = ({ metrics }) => {
-
+
} // Using Target for Needs/Essentials + icon={} color="bg-rose-50 text-rose-600" /> = ({ metrics }) => { icon={} color="bg-blue-50 text-blue-600" /> - } - color="bg-slate-100 text-slate-600" - />
@@ -135,7 +161,7 @@ export const DashboardCards: React.FC = ({ metrics }) => { ); }; -// Helper Component similar to Academic's NavButton but for Stats +// Helper Component for Stats interface StatRowProps { title: string; value: string; @@ -150,7 +176,7 @@ const StatRow: React.FC = ({ title, value, desc, icon, color }) =>
{icon}
-
{/* min-w-0 for truncate to work */} +

{title}

{value}

{desc}

@@ -158,3 +184,29 @@ const StatRow: React.FC = ({ title, value, desc, icon, color }) =>
); } + +// Helper Component for Navigation +interface NavCardProps { + title: string; + desc: string; + icon: React.ReactNode; + color: string; + onClick: () => void; +} + +const NavCard: React.FC = ({ title, desc, icon, color, onClick }) => { + return ( + + ) +} diff --git a/Frontend/project-CL/src/features/financial/components/DeleteCategoryModal.tsx b/Frontend/project-CL/src/features/financial/components/DeleteCategoryModal.tsx new file mode 100644 index 0000000..cb33ee5 --- /dev/null +++ b/Frontend/project-CL/src/features/financial/components/DeleteCategoryModal.tsx @@ -0,0 +1,38 @@ +import React from 'react'; + +interface DeleteCategoryModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + categoryName: string; +} + +export default function DeleteCategoryModal({ isOpen, onClose, onConfirm, categoryName }: DeleteCategoryModalProps) { + if (!isOpen) return null; + + return ( +
+
+

Delete Category

+

+ Are you sure you want to delete "{categoryName}"? This action cannot be undone. +

+ +
+ + +
+
+
+ ); +} diff --git a/Frontend/project-CL/src/features/financial/components/views/BudgetingView.tsx b/Frontend/project-CL/src/features/financial/components/views/BudgetingView.tsx new file mode 100644 index 0000000..20bf989 --- /dev/null +++ b/Frontend/project-CL/src/features/financial/components/views/BudgetingView.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const BudgetingView: React.FC = () => { + return ( +
+
+

Budgeting Tools

+

Features coming soon...

+
+
+ ); +}; + +export default BudgetingView; diff --git a/Frontend/project-CL/src/features/financial/components/views/CategoryView.tsx b/Frontend/project-CL/src/features/financial/components/views/CategoryView.tsx new file mode 100644 index 0000000..b60e78c --- /dev/null +++ b/Frontend/project-CL/src/features/financial/components/views/CategoryView.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { Target, Wallet, TrendingUp, PiggyBank, Pencil, Trash2 } from 'lucide-react'; +import type { Category } from '../../services/financialService'; + +interface CategoryViewProps { + categories: Category[]; + onEdit?: (category: Category) => void; + onDelete?: (id: number) => void; +} + +const CategoryView: React.FC = ({ categories, onEdit, onDelete }) => { + + // Group categories by bucket + const buckets = { + needs: categories.filter(c => c.type === 'expense' && c.bucket.toLowerCase() === 'needs'), + wants: categories.filter(c => c.type === 'expense' && c.bucket.toLowerCase() === 'wants'), + saving: categories.filter(c => c.type === 'expense' && (c.bucket.toLowerCase() === 'saving' || c.bucket.toLowerCase() === 'savings')), + invest: categories.filter(c => c.type === 'expense' && c.bucket.toLowerCase() === 'invest'), + income: categories.filter(c => c.type === 'income') + }; + + const BucketColumn = ({ title, items, color, icon: Icon }: any) => ( +
+ {/* Header */} +
+
+ +
+
+

{title}

+

{items.length} Categories

+
+
+ + {/* List */} +
+ {items.length === 0 ? ( +
+ No categories yet +
+ ) : ( + items.map((cat: Category) => ( +
+ {cat.name} + + {/* Hover Actions */} +
+ + +
+
+ )) + )} +
+
+ ); + + return ( +
+
+ {/* Income Column (Optional but good to have) */} + + + {/* Expense Buckets */} + + + + +
+
+ ); +}; + +export default CategoryView; diff --git a/Frontend/project-CL/src/features/financial/components/views/GoalsView.tsx b/Frontend/project-CL/src/features/financial/components/views/GoalsView.tsx new file mode 100644 index 0000000..c856e43 --- /dev/null +++ b/Frontend/project-CL/src/features/financial/components/views/GoalsView.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const GoalsView: React.FC = () => { + return ( +
+
+

Savings Goals

+

Features coming soon...

+
+
+ ); +}; + +export default GoalsView; diff --git a/Frontend/project-CL/src/features/financial/components/views/TransactionView.tsx b/Frontend/project-CL/src/features/financial/components/views/TransactionView.tsx new file mode 100644 index 0000000..c0597ee --- /dev/null +++ b/Frontend/project-CL/src/features/financial/components/views/TransactionView.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +const TransactionView: React.FC = () => { + return ( +
+
+

Transaction History

+

Features coming soon...

+
+
+ ); +}; + +export default TransactionView; diff --git a/Frontend/project-CL/src/features/financial/financial.tsx b/Frontend/project-CL/src/features/financial/financial.tsx index b0d7cf3..fa774b4 100644 --- a/Frontend/project-CL/src/features/financial/financial.tsx +++ b/Frontend/project-CL/src/features/financial/financial.tsx @@ -1,10 +1,82 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useFinancialDashboard } from './hooks/useFinancialDashboard'; import { DashboardCards } from './components/DashboardCards'; import Header from "../../components/Header"; +import { Layout, Tags, ArrowRightLeft, PieChart, Target } from 'lucide-react'; +import { financialService } from './services/financialService'; + +// Views +import CategoryView from './components/views/CategoryView'; +import TransactionView from './components/views/TransactionView'; +import BudgetingView from './components/views/BudgetingView'; +import GoalsView from './components/views/GoalsView'; + +// Modals +import AddCategoryModal from './components/AddCategoryModal'; +import DeleteCategoryModal from './components/DeleteCategoryModal'; const Financial: React.FC = () => { - const { metrics, loading, error } = useFinancialDashboard(); + const [view, setView] = useState<'home' | 'category' | 'transaction' | 'budgeting' | 'goals'>('home'); + const { metrics, loading, error, categories, refreshData } = useFinancialDashboard(); + + // Modal States + const [isAddingCategory, setIsAddingCategory] = useState(false); + const [isDeletingCategory, setIsDeletingCategory] = useState(false); + const [selectedCategory, setSelectedCategory] = useState(null); + const [categoryToDelete, setCategoryToDelete] = useState(null); + + // Handlers + const handleSaveCategory = async (name: string, type: 'income' | 'expense', bucket: string) => { + try { + if (selectedCategory) { + // Update + await financialService.updateCategory(selectedCategory.id, { + name, + type, + bucket: type === 'income' ? 'income' : bucket + }); + } else { + // Create + await financialService.createCategory({ + name, + type, + bucket: type === 'income' ? 'income' : bucket, + user_id: 1 // TODO: Get from auth context + }); + } + await refreshData(); + setIsAddingCategory(false); + setSelectedCategory(null); + } catch (error) { + console.error("Failed to save category", error); + // Optional: Show toast error + } + }; + + const handleEditCategory = (category: any) => { + setSelectedCategory(category); + setIsAddingCategory(true); + }; + + const handleDeleteCategory = (id: number) => { + const category = categories.find(c => c.id === id); + if (category) { + setCategoryToDelete(category); + setIsDeletingCategory(true); + } + }; + + const confirmDeleteCategory = async () => { + if (!categoryToDelete) return; + try { + await financialService.deleteCategory(categoryToDelete.id); + await refreshData(); + setIsDeletingCategory(false); + setCategoryToDelete(null); + } catch (error) { + console.error("Failed to delete category", error); + } + }; if (loading) { return ( @@ -14,24 +86,94 @@ const Financial: React.FC = () => { ); } + const ViewButton = ({ id, label, icon: Icon }: any) => ( + + ); + return (
- {/* Future: Toolbar / Filters similar to Academic could go here */} +
+ {/* Controls Row */} +
+ + {/* View Switcher - Main Nav */} +
+ + + + + +
+ + {/* Context Action Button */} + +
+
{/* View Area */}
- {!error ? ( + {error ? (
{error}
) : ( - + <> + {view === 'home' && } + {view === 'category' && ( + + )} + {view === 'transaction' && } + {view === 'budgeting' && } + {view === 'goals' && } + )}
+ + {/* Modals */} + { + setIsAddingCategory(false); + setSelectedCategory(null); + }} + onSave={handleSaveCategory} + category={selectedCategory} + /> + + { + setIsDeletingCategory(false); + setCategoryToDelete(null); + }} + onConfirm={confirmDeleteCategory} + categoryName={categoryToDelete?.name || ''} + />
); }; diff --git a/Frontend/project-CL/src/features/financial/hooks/useFinancialDashboard.ts b/Frontend/project-CL/src/features/financial/hooks/useFinancialDashboard.ts index 4e7e9a8..64bc190 100644 --- a/Frontend/project-CL/src/features/financial/hooks/useFinancialDashboard.ts +++ b/Frontend/project-CL/src/features/financial/hooks/useFinancialDashboard.ts @@ -34,29 +34,30 @@ export const useFinancialDashboard = () => { 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); - } - }; + const refreshData = async () => { + setLoading(true); + 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(); + useEffect(() => { + refreshData(); }, []); const calculateMetrics = (txs: Transaction[], cats: Category[], goals: Goal[]) => { @@ -115,5 +116,5 @@ export const useFinancialDashboard = () => { }); }; - return { metrics, loading, error, transactions, categories, goals }; + return { metrics, loading, error, transactions, categories, goals, refreshData }; }; From 7c782cb079c93e62d97836d66dce14ab28a4af1f Mon Sep 17 00:00:00 2001 From: Arjuna Date: Sat, 3 Jan 2026 12:51:51 +0700 Subject: [PATCH 2/2] feat: fin transaction added --- .../components/AddTransactionModal.tsx | 161 +++++++++++ .../components/DeleteTransactionModal.tsx | 35 +++ .../components/views/TransactionView.tsx | 261 +++++++++++++++++- .../src/features/financial/financial.tsx | 87 +++++- 4 files changed, 535 insertions(+), 9 deletions(-) create mode 100644 Frontend/project-CL/src/features/financial/components/AddTransactionModal.tsx create mode 100644 Frontend/project-CL/src/features/financial/components/DeleteTransactionModal.tsx diff --git a/Frontend/project-CL/src/features/financial/components/AddTransactionModal.tsx b/Frontend/project-CL/src/features/financial/components/AddTransactionModal.tsx new file mode 100644 index 0000000..7e4ef98 --- /dev/null +++ b/Frontend/project-CL/src/features/financial/components/AddTransactionModal.tsx @@ -0,0 +1,161 @@ +import React, { useState, useEffect } from 'react'; +import { X, Calendar, Tag } from 'lucide-react'; +import type { Category, Transaction } from '../services/financialService'; + +interface AddTransactionModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (data: { category_id: number; amount: number; date: string; description: string }) => Promise; + categories: Category[]; + transaction?: Transaction | null; // Optional for edit mode +} + +export default function AddTransactionModal({ isOpen, onClose, onSave, categories, transaction }: AddTransactionModalProps) { + const [amount, setAmount] = useState(''); + const [categoryId, setCategoryId] = useState(''); + const [date, setDate] = useState(new Date().toISOString().split('T')[0]); + const [description, setDescription] = useState(''); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (isOpen && transaction) { + setAmount(transaction.amount.toString()); + setCategoryId(transaction.category_id); + // Ensure date is in YYYY-MM-DD format for input + setDate(new Date(transaction.date).toISOString().split('T')[0]); + setDescription(transaction.description); + } else if (isOpen) { + // Reset + setAmount(''); + setCategoryId(''); + setDate(new Date().toISOString().split('T')[0]); + setDescription(''); + } + }, [isOpen, transaction]); + + if (!isOpen) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!categoryId || !amount) return; + + setLoading(true); + await onSave({ + category_id: Number(categoryId), + amount: Number(amount), + date: new Date(date).toISOString(), + description + }); + setLoading(false); + + // Reset form + setAmount(''); + setCategoryId(''); + setDate(new Date().toISOString().split('T')[0]); + setDescription(''); + }; + + return ( +
+
+
+

{transaction ? 'Edit Transaction' : 'Add Transaction'}

+ +
+ +
+ + {/* Amount Input */} +
+ +
+
+ Rp +
+ setAmount(e.target.value)} + placeholder="0" + className="w-full pl-10 pr-4 py-2 rounded-xl border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition-all font-mono font-medium" + /> +
+
+ + {/* Category Selection */} +
+ +
+
+ +
+ +
+
+ + {/* Date Input */} +
+ +
+
+ +
+ setDate(e.target.value)} + className="w-full pl-10 pr-4 py-2 rounded-xl border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-100 outline-none transition-all" + /> +
+
+ + {/* Description Input */} +
+ +