Created
March 12, 2026 11:40
-
-
Save benjcabalona1029/dd73f5e3daa7507c4e5f1b0f85638a08 to your computer and use it in GitHub Desktop.
Vibe Coded Budget Tracker
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import dash | |
| from dash import html, dcc, Input, Output, State, ctx | |
| import plotly.express as px | |
| import pandas as pd | |
| import base64 | |
| import io | |
| app = dash.Dash(__name__, external_scripts=["https://cdn.tailwindcss.com"]) | |
| def create_empty_figure(title): | |
| return { | |
| "layout": { | |
| "title": title, | |
| "xaxis": {"visible": False}, | |
| "yaxis": {"visible": False}, | |
| "annotations": [{ | |
| "text": "No data available", | |
| "xref": "paper", | |
| "yref": "paper", | |
| "showarrow": False, | |
| "font": {"size": 16, "color": "#94a3b8"} | |
| }], | |
| "plot_bgcolor": "rgba(0,0,0,0)", | |
| "paper_bgcolor": "rgba(0,0,0,0)" | |
| } | |
| } | |
| modal_layout = html.Div( | |
| id="modal-container", | |
| className="fixed inset-0 bg-slate-900/50 z-50 hidden items-center justify-center p-4", | |
| children=[ | |
| html.Div(className="bg-white rounded-2xl shadow-xl w-full max-w-lg overflow-hidden flex flex-col max-h-[90vh]", children=[ | |
| html.Div(className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50", children=[ | |
| html.H2("Add Transactions", className="text-lg font-bold text-slate-800"), | |
| html.Button("✕", id="close-modal-btn", n_clicks=0, className="text-slate-400 hover:text-slate-600 transition-colors text-xl font-bold") | |
| ]), | |
| html.Div(className="p-6 overflow-y-auto", children=[ | |
| html.H3("Upload CSV", className="text-slate-700 font-semibold mb-4"), | |
| dcc.Upload( | |
| id='upload-data', | |
| children=html.Div(['Drag and Drop or ', html.A('Select Files', className="text-indigo-600 font-semibold hover:underline cursor-pointer")]), | |
| className="w-full h-16 border-2 border-dashed border-slate-300 rounded-lg flex items-center justify-center text-slate-500 mb-6 hover:bg-slate-50 transition-colors" | |
| ), | |
| html.H3("Manual Entry", className="text-slate-700 font-semibold mb-4 border-t pt-4"), | |
| html.Div(className="space-y-3", children=[ | |
| dcc.DatePickerSingle( | |
| id='input-date', | |
| placeholder='Transaction Date', | |
| className="w-full", | |
| style={"width": "100%"} | |
| ), | |
| dcc.Input(id='input-txn-type', type='text', placeholder='TransactionType (e.g., Food, Rent)', className="w-full p-2 border border-slate-300 rounded focus:outline-none focus:border-indigo-500"), | |
| dcc.Dropdown(id='input-currency', options=[{'label': 'USD', 'value': 'USD'}, {'label': 'PHP', 'value': 'PHP'}], placeholder='Currency', className="w-full"), | |
| dcc.Input(id='input-amount', type='number', placeholder='TransactionAmount', className="w-full p-2 border border-slate-300 rounded focus:outline-none focus:border-indigo-500"), | |
| dcc.Dropdown(id='input-type', options=[{'label': 'Debit (Income)', 'value': 'Debit'}, {'label': 'Credit (Expense)', 'value': 'Credit'}], placeholder='Type', className="w-full"), | |
| dcc.Input(id='input-notes', type='text', placeholder='Notes', className="w-full p-2 border border-slate-300 rounded focus:outline-none focus:border-indigo-500"), | |
| dcc.Input(id='input-from', type='text', placeholder='Transaction From (e.g., CIMB)', className="w-full p-2 border border-slate-300 rounded focus:outline-none focus:border-indigo-500"), | |
| html.Button('Add Transaction', id='add-button', n_clicks=0, className="w-full bg-indigo-600 text-white font-semibold py-2 rounded hover:bg-indigo-700 transition-colors mt-2") | |
| ]), | |
| html.Div(id='entry-error', className="text-rose-500 text-sm mt-2") | |
| ]) | |
| ]) | |
| ] | |
| ) | |
| app.layout = html.Div(className="min-h-screen bg-slate-50 p-8 font-sans relative", children=[ | |
| dcc.Store(id='memory-store', data=[]), | |
| modal_layout, | |
| html.Div(className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-8 gap-4", children=[ | |
| html.H1("Budget Tracker", className="text-3xl font-extrabold text-slate-800 tracking-tight"), | |
| html.Button("+ Add Transaction", id="open-modal-btn", n_clicks=0, className="bg-indigo-600 text-white px-5 py-2.5 rounded-lg font-semibold hover:bg-indigo-700 transition-colors shadow-sm whitespace-nowrap") | |
| ]), | |
| html.Div(className="flex flex-col gap-6", children=[ | |
| html.Div(className="grid grid-cols-1 md:grid-cols-3 gap-6", children=[ | |
| html.Div(className="bg-white p-6 rounded-xl shadow-sm border border-slate-100", children=[ | |
| html.H3("Total Income", className="text-slate-500 text-sm font-medium uppercase tracking-wider mb-2"), | |
| html.P(id="total-income-display", children="₱0.00", className="text-3xl font-bold text-emerald-600") | |
| ]), | |
| html.Div(className="bg-white p-6 rounded-xl shadow-sm border border-slate-100", children=[ | |
| html.H3("Total Expenses", className="text-slate-500 text-sm font-medium uppercase tracking-wider mb-2"), | |
| html.P(id="total-expense-display", children="₱0.00", className="text-3xl font-bold text-rose-600") | |
| ]), | |
| html.Div(className="bg-white p-6 rounded-xl shadow-sm border border-slate-100", children=[ | |
| html.H3("Net Balance", className="text-slate-500 text-sm font-medium uppercase tracking-wider mb-2"), | |
| html.P(id="net-balance-display", children="₱0.00", className="text-3xl font-bold text-indigo-600") | |
| ]) | |
| ]), | |
| html.Div(className="grid grid-cols-1 lg:grid-cols-2 gap-6 flex-grow", children=[ | |
| html.Div(className="bg-white p-4 rounded-xl shadow-sm border border-slate-100 h-full", children=[ | |
| dcc.Graph(id='expense-breakdown-chart', figure=create_empty_figure("Average Monthly Expense by Type (PHP)"), config={'displayModeBar': False}, style={"height": "100%", "min-height": "350px"}) | |
| ]), | |
| html.Div(className="bg-white p-4 rounded-xl shadow-sm border border-slate-100 h-full", children=[ | |
| dcc.Graph(id='cumulative-balance-chart', figure=create_empty_figure("Cumulative Net Balance (PHP)"), config={'displayModeBar': False}, style={"height": "100%", "min-height": "350px"}) | |
| ]) | |
| ]) | |
| ]) | |
| ]) | |
| @app.callback( | |
| Output("modal-container", "className"), | |
| Input("open-modal-btn", "n_clicks"), | |
| Input("close-modal-btn", "n_clicks"), | |
| State("modal-container", "className"), | |
| prevent_initial_call=True | |
| ) | |
| def toggle_modal(open_clicks, close_clicks, current_class): | |
| trigger = ctx.triggered_id | |
| if trigger == "open-modal-btn": | |
| return current_class.replace("hidden", "flex") | |
| elif trigger == "close-modal-btn": | |
| return current_class.replace("flex", "hidden") | |
| return current_class | |
| @app.callback( | |
| Output('memory-store', 'data'), | |
| Output('entry-error', 'children'), | |
| Input('upload-data', 'contents'), | |
| Input('add-button', 'n_clicks'), | |
| State('memory-store', 'data'), | |
| State('input-date', 'date'), | |
| State('input-txn-type', 'value'), | |
| State('input-currency', 'value'), | |
| State('input-amount', 'value'), | |
| State('input-type', 'value'), | |
| State('input-notes', 'value'), | |
| State('input-from', 'value'), | |
| prevent_initial_call=True | |
| ) | |
| def update_store(contents, n_clicks, current_data, date, txn_type, currency, amount, tx_type, notes, tx_from): | |
| trigger = ctx.triggered_id | |
| if trigger == 'upload-data' and contents is not None: | |
| try: | |
| content_type, content_string = contents.split(',') | |
| decoded = base64.b64decode(content_string) | |
| df_new = pd.read_csv(io.StringIO(decoded.decode('utf-8'))) | |
| required_cols = ['TransactionDate', 'TransactionType', 'Currency', 'TransactionAmount', 'Type'] | |
| if not all(col in df_new.columns for col in required_cols): | |
| return current_data, "CSV missing required columns." | |
| updated_data = current_data + df_new.to_dict('records') | |
| return updated_data, "" | |
| except Exception as e: | |
| return current_data, f"Error processing file: {str(e)}" | |
| elif trigger == 'add-button': | |
| if not all([date, txn_type, currency, amount, tx_type]): | |
| return current_data, "Please fill in all required fields (Date, TransactionType, Currency, Amount, Type)." | |
| new_row = { | |
| 'TransactionDate': date, | |
| 'TransactionType': txn_type, | |
| 'Currency': currency, | |
| 'TransactionAmount': float(amount), | |
| 'Type': tx_type, | |
| 'Notes': notes or "", | |
| 'Transaction From': tx_from or "" | |
| } | |
| updated_data = current_data + [new_row] | |
| return updated_data, "" | |
| return current_data, "" | |
| @app.callback( | |
| Output('total-income-display', 'children'), | |
| Output('total-expense-display', 'children'), | |
| Output('net-balance-display', 'children'), | |
| Output('expense-breakdown-chart', 'figure'), | |
| Output('cumulative-balance-chart', 'figure'), | |
| Input('memory-store', 'data') | |
| ) | |
| def update_dashboard(data): | |
| if not data: | |
| return "₱0.00", "₱0.00", "₱0.00", create_empty_figure("Average Monthly Expense by Type (PHP)"), create_empty_figure("Cumulative Net Balance (PHP)") | |
| df = pd.DataFrame(data) | |
| df['TransactionDate'] = pd.to_datetime(df['TransactionDate']) | |
| df['Amount_PHP'] = df.apply( | |
| lambda row: row['TransactionAmount'] * 58 if row['Currency'] == 'USD' else row['TransactionAmount'], | |
| axis=1 | |
| ) | |
| df = df[df['TransactionType'] != 'Move Money'].copy() | |
| if df.empty: | |
| return "₱0.00", "₱0.00", "₱0.00", create_empty_figure("Average Monthly Expense by Type (PHP)"), create_empty_figure("Cumulative Net Balance (PHP)") | |
| df_income = df[df['Type'] == 'Debit'] | |
| df_expense = df[df['Type'] == 'Credit'] | |
| total_income = df_income['Amount_PHP'].sum() | |
| total_expense = df_expense['Amount_PHP'].sum() | |
| net_balance = total_income - total_expense | |
| fig_expense_breakdown = create_empty_figure("Average Monthly Expense by Type (PHP)") | |
| if not df_expense.empty: | |
| expense_monthly = df_expense.assign(YearMonth=df_expense['TransactionDate'].dt.to_period('M')) | |
| monthly_expense_by_type = expense_monthly.groupby(['YearMonth', 'TransactionType'], as_index=False)['Amount_PHP'].sum() | |
| avg_expense_by_type = monthly_expense_by_type.groupby('TransactionType', as_index=False)['Amount_PHP'].mean() | |
| avg_expense_by_type = avg_expense_by_type.sort_values(by='Amount_PHP', ascending=True) | |
| fig_expense_breakdown = px.bar( | |
| avg_expense_by_type, | |
| x='Amount_PHP', | |
| y='TransactionType', | |
| orientation='h', | |
| title='Average Monthly Expense by Type (PHP)', | |
| color='Amount_PHP', | |
| color_continuous_scale='Reds' | |
| ) | |
| fig_expense_breakdown.update_layout( | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| paper_bgcolor='rgba(0,0,0,0)', | |
| margin=dict(l=20, r=20, t=50, b=20), | |
| coloraxis_showscale=False, | |
| xaxis_title="Amount (PHP)", | |
| xaxis_rangemode="tozero", | |
| yaxis_title="", | |
| font=dict(family="sans-serif", color="#475569") | |
| ) | |
| df['Net_Amount_PHP'] = df.apply( | |
| lambda x: x['Amount_PHP'] if x['Type'] == 'Debit' else -x['Amount_PHP'], axis=1 | |
| ) | |
| daily_net = df.groupby('TransactionDate', as_index=False)['Net_Amount_PHP'].sum() | |
| daily_net = daily_net.sort_values('TransactionDate') | |
| daily_net['Cumulative_Balance'] = daily_net['Net_Amount_PHP'].cumsum() | |
| fig_cumulative_balance = px.line( | |
| daily_net, | |
| x='TransactionDate', | |
| y='Cumulative_Balance', | |
| title='Cumulative Net Balance (PHP)', | |
| markers=True | |
| ) | |
| fig_cumulative_balance.update_traces(line_color='#4f46e5') | |
| fig_cumulative_balance.update_layout( | |
| plot_bgcolor='rgba(0,0,0,0)', | |
| paper_bgcolor='rgba(0,0,0,0)', | |
| margin=dict(l=20, r=20, t=50, b=20), | |
| xaxis_title="", | |
| yaxis_title="Balance (PHP)", | |
| yaxis_rangemode="tozero", | |
| font=dict(family="sans-serif", color="#475569") | |
| ) | |
| return f"₱{total_income:,.2f}", f"₱{total_expense:,.2f}", f"₱{net_balance:,.2f}", fig_expense_breakdown, fig_cumulative_balance | |
| if __name__ == '__main__': | |
| app.run(debug=True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment