From e793d81caee8d3ce40f15e0e82b5db2f2425431f Mon Sep 17 00:00:00 2001 From: Rafal Paluch Date: Mon, 23 Feb 2026 15:48:19 +0100 Subject: [PATCH] first commit --- .Dockerfile.un~ | Bin 0 -> 523 bytes .env | 3 + .requirements.txt.un~ | Bin 0 -> 523 bytes Dockerfile | 19 ++ Dockerfile~ | 0 app.py | 574 ++++++++++++++++++++++++++++++++++++ autostart | 1 + data/finanse.db | Bin 0 -> 32768 bytes docker-compose.override.yml | 7 + docker-compose.yml | 17 ++ name | 1 + requirements.txt | 2 + requirements.txt~ | 0 13 files changed, 624 insertions(+) create mode 100644 .Dockerfile.un~ create mode 100644 .env create mode 100644 .requirements.txt.un~ create mode 100644 Dockerfile create mode 100644 Dockerfile~ create mode 100644 app.py create mode 100644 autostart create mode 100644 data/finanse.db create mode 100644 docker-compose.override.yml create mode 100644 docker-compose.yml create mode 100644 name create mode 100644 requirements.txt create mode 100644 requirements.txt~ diff --git a/.Dockerfile.un~ b/.Dockerfile.un~ new file mode 100644 index 0000000000000000000000000000000000000000..fb2e4d92e701ffdbdb9ae04af768c385dccf31c5 GIT binary patch literal 523 zcmWH`%$*;a=aT=Ff$4d;>cd4F;vPNP@+S7O*Fe4%Aslq0HfmyG&;UYpqUJg JFVn{7s{jgfB4Gdk literal 0 HcmV?d00001 diff --git a/.env b/.env new file mode 100644 index 0000000..da2909f --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +# .env +APP_PASSWORD=tajne1234! +TIMEZONE=Europe/Warsaw diff --git a/.requirements.txt.un~ b/.requirements.txt.un~ new file mode 100644 index 0000000000000000000000000000000000000000..94a40f6610f1afc757b4e7275bb0a5aa7807b055 GIT binary patch literal 523 zcmWH`%$*;a=aT=Ffyuq_`BF{Oy>|@`6yC8>%+q(;bmUIxw(3P~vK`D`@AUw JOdFrC0s#6EA6@_e literal 0 HcmV?d00001 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d8d753f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt + +COPY . . + +# Tworzymy katalog na dane +RUN mkdir -p data + +EXPOSE 8501 + +ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"] diff --git a/Dockerfile~ b/Dockerfile~ new file mode 100644 index 0000000..e69de29 diff --git a/app.py b/app.py new file mode 100644 index 0000000..84c66e3 --- /dev/null +++ b/app.py @@ -0,0 +1,574 @@ +import streamlit as st +import pandas as pd +import sqlite3 +import os +import calendar +from datetime import date, datetime + +# --- KONFIGURACJA --- +st.set_page_config(page_title="Finanse Domowe", layout="wide", page_icon="💰") +DB_FILE = 'data/finanse.db' + +# --- CSS (STYLIZACJA) --- +st.markdown(""" + +""", unsafe_allow_html=True) + +# --- BEZPIECZEŃSTWO --- +def check_password(): + if "password_correct" not in st.session_state: + st.session_state.password_correct = False + if st.session_state.password_correct: + return True + + col1, col2, col3 = st.columns([1,2,1]) + with col2: + st.markdown("### 🔒 Dostęp autoryzowany") + password = st.text_input("Podaj hasło", type="password") + if st.button("Zaloguj"): + secret_pass = os.environ.get("APP_PASSWORD", "admin") + if password == secret_pass: + st.session_state.password_correct = True + st.rerun() + else: + st.error("Błędne hasło!") + return False + +if not check_password(): + st.stop() + +# --- BAZA DANYCH --- +def init_db(): + os.makedirs(os.path.dirname(DB_FILE), exist_ok=True) + conn = sqlite3.connect(DB_FILE) + c = conn.cursor() + + c.execute(''' + CREATE TABLE IF NOT EXISTS wydatki ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nazwa TEXT NOT NULL, + kategoria TEXT, + kwota REAL, + termin DATE, + cykliczne BOOLEAN, + zaplacone BOOLEAN + ) + ''') + try: c.execute("ALTER TABLE wydatki ADD COLUMN interwal_ilosc INTEGER DEFAULT 1") + except sqlite3.OperationalError: pass + try: c.execute("ALTER TABLE wydatki ADD COLUMN interwal_typ TEXT DEFAULT 'miesiące'") + except sqlite3.OperationalError: pass + + c.execute('''CREATE TABLE IF NOT EXISTS kategorie (nazwa TEXT PRIMARY KEY)''') + c.execute('''CREATE TABLE IF NOT EXISTS budzety (kategoria TEXT PRIMARY KEY, limit_kwota REAL)''') + + c.execute(''' + CREATE TABLE IF NOT EXISTS cele ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nazwa TEXT NOT NULL, + kwota_cel REAL, + kwota_obecna REAL DEFAULT 0, + termin DATE, + ikona TEXT + ) + ''') + + c.execute("SELECT count(*) FROM kategorie") + if c.fetchone()[0] == 0: + default_cats = [("Domowe",), ("Firmowe",), ("Podatki",), ("Paliwo",), ("Jedzenie",)] + c.executemany("INSERT INTO kategorie (nazwa) VALUES (?)", default_cats) + + conn.commit() + conn.close() + +def get_categories(): + conn = sqlite3.connect(DB_FILE) + df = pd.read_sql_query("SELECT nazwa FROM kategorie ORDER BY nazwa", conn) + conn.close() + return df['nazwa'].tolist() + +def add_category_db(name): + conn = sqlite3.connect(DB_FILE) + try: + conn.execute("INSERT INTO kategorie (nazwa) VALUES (?)", (name,)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False + finally: + conn.close() + +def delete_category_db(name): + conn = sqlite3.connect(DB_FILE) + conn.execute("DELETE FROM kategorie WHERE nazwa = ?", (name,)) + conn.execute("DELETE FROM budzety WHERE kategoria = ?", (name,)) + conn.commit() + conn.close() + +def get_budgets(): + conn = sqlite3.connect(DB_FILE) + df = pd.read_sql_query("SELECT * FROM budzety", conn) + conn.close() + if df.empty: return {} + return dict(zip(df['kategoria'], df['limit_kwota'])) + +def set_budget_db(category, limit): + conn = sqlite3.connect(DB_FILE) + conn.execute("INSERT INTO budzety (kategoria, limit_kwota) VALUES (?, ?) ON CONFLICT(kategoria) DO UPDATE SET limit_kwota=?", (category, limit, limit)) + conn.commit() + conn.close() + +def delete_budget_db(category): + conn = sqlite3.connect(DB_FILE) + conn.execute("DELETE FROM budzety WHERE kategoria = ?", (category,)) + conn.commit() + conn.close() + +def add_goal(nazwa, cel, termin, ikona="🎯"): + conn = sqlite3.connect(DB_FILE) + conn.execute("INSERT INTO cele (nazwa, kwota_cel, kwota_obecna, termin, ikona) VALUES (?, ?, ?, ?, ?)", (nazwa, cel, 0.0, termin, ikona)) + conn.commit() + conn.close() + +def get_goals(): + conn = sqlite3.connect(DB_FILE) + df = pd.read_sql_query("SELECT * FROM cele", conn) + conn.close() + if not df.empty: + df['termin'] = pd.to_datetime(df['termin'], errors='coerce').dt.date + return df + +def update_goal_amount(goal_id, amount_to_add): + conn = sqlite3.connect(DB_FILE) + conn.execute("UPDATE cele SET kwota_obecna = kwota_obecna + ? WHERE id = ?", (amount_to_add, goal_id)) + conn.commit() + conn.close() + +def delete_goal(goal_id): + conn = sqlite3.connect(DB_FILE) + conn.execute("DELETE FROM cele WHERE id = ?", (goal_id,)) + conn.commit() + conn.close() + +def load_data(): + conn = sqlite3.connect(DB_FILE) + df = pd.read_sql_query("SELECT * FROM wydatki", conn) + conn.close() + if not df.empty: + df['termin'] = pd.to_datetime(df['termin']).dt.date + df['cykliczne'] = df['cykliczne'].astype(bool) + df['zaplacone'] = df['zaplacone'].astype(bool) + + if 'interwal_ilosc' in df.columns: + df['interwal_ilosc'] = pd.to_numeric(df['interwal_ilosc'], errors='coerce') + df['interwal_ilosc'] = df['interwal_ilosc'].fillna(1).astype(int) + + if 'interwal_typ' in df.columns: + df['interwal_typ'] = df['interwal_typ'].fillna('miesiące').astype(str) + return df + +def add_expense(nazwa, kategoria, kwota, termin, cykliczne, interwal_ilosc=1, interwal_typ='miesiące'): + conn = sqlite3.connect(DB_FILE) + c = conn.cursor() + + safe_qty = int(interwal_ilosc) + safe_type = str(interwal_typ) + + c.execute('''INSERT INTO wydatki (nazwa, kategoria, kwota, termin, cykliczne, zaplacone, interwal_ilosc, interwal_typ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', + (nazwa, kategoria, kwota, termin, cykliczne, False, safe_qty, safe_type)) + conn.commit() + conn.close() + +def update_status(expense_id, is_paid): + conn = sqlite3.connect(DB_FILE) + c = conn.cursor() + c.execute("UPDATE wydatki SET zaplacone = ? WHERE id = ?", (is_paid, expense_id)) + conn.commit() + conn.close() + +def delete_expense(expense_id): + conn = sqlite3.connect(DB_FILE) + c = conn.cursor() + c.execute("DELETE FROM wydatki WHERE id = ?", (expense_id,)) + conn.commit() + conn.close() + +init_db() + +# --- CALLBACKS --- +if "view_date_picker" not in st.session_state: + st.session_state.view_date_picker = date.today() + +def set_today(): + st.session_state.view_date_picker = date.today() + +# --- DIALOGI --- +@st.dialog("Zmień status płatności") +def confirm_status_change(row): + st.write(f"Pozycja: **{row['nazwa']}**") + st.write(f"Kwota: **{row['kwota']:.2f} PLN**") + + current_status = row['zaplacone'] + new_status = not current_status + action_text = "Oznaczyć jako ZAPŁACONE? 🟢" if not current_status else "Cofnąć do NIEZAPŁACONE? 🔴" + st.markdown(f"### {action_text}") + + create_next = False + next_date = None + + if not current_status and row['cykliczne']: + try: + i_ilosc = int(row.get('interwal_ilosc', 1)) + except: + i_ilosc = 1 + i_typ = str(row.get('interwal_typ', 'miesiące')) + + st.info(f"🔄 To płatność cykliczna: co {i_ilosc} {i_typ}.") + create_next = st.checkbox("Utwórz automatycznie następną płatność?", value=True) + if create_next: + current_date_dt = pd.to_datetime(row['termin']) + offset = pd.DateOffset(months=1) + if i_typ == 'dni': offset = pd.DateOffset(days=i_ilosc) + elif i_typ == 'tygodnie': offset = pd.DateOffset(weeks=i_ilosc) + elif i_typ == 'miesiące': offset = pd.DateOffset(months=i_ilosc) + elif i_typ == 'lata': offset = pd.DateOffset(years=i_ilosc) + next_date_dt = current_date_dt + offset + next_date = next_date_dt.date() + st.write(f"📅 Data nowej płatności: **{next_date}**") + + st.divider() + if st.button("Zatwierdź zmianę", type="primary"): + update_status(row['id'], new_status) + if create_next and new_status == True and next_date: + add_expense(row['nazwa'], row['kategoria'], row['kwota'], next_date, True, i_ilosc, i_typ) + st.toast(f"Zaplanowano następną płatność na {next_date}", icon="🚀") + st.rerun() + +@st.dialog("Usuń budżet") +def confirm_delete_budget(category): + st.warning(f"Czy na pewno chcesz usunąć limit budżetowy dla kategorii: **{category}**?") + st.caption("Kategoria oraz przypisane do niej wydatki NIE zostaną usunięte. Usunięty zostanie tylko limit.") + col_no, col_yes = st.columns(2) + with col_no: + if st.button("Anuluj"): st.rerun() + with col_yes: + if st.button("Tak, usuń", type="primary"): + delete_budget_db(category) + st.rerun() + +# --- LOGIKA UI --- +categories_list = get_categories() + +with st.sidebar: + st.title("🎛️ Nawigacja") + page = st.radio("Widok", ["Lista & Budżet", "Kalendarz", "Cele (Skarbonka)", "Ustawienia"]) + st.divider() + + if page != "Cele (Skarbonka)": + st.header("➕ Dodaj wydatek") + is_cyclic_mode = st.toggle("Tryb cykliczny", value=False) + with st.form("add_form", clear_on_submit=True): + name = st.text_input("Nazwa") + cat = st.selectbox("Kategoria", categories_list) + amt = st.number_input("Kwota", min_value=0.0, step=10.0) + due = st.date_input("Termin", value=date.today()) + int_qty, int_type = 1, "miesiące" + if is_cyclic_mode: + st.markdown("---") + col_i1, col_i2 = st.columns(2) + with col_i1: int_qty = st.number_input("Co ile?", min_value=1, value=1) + with col_i2: int_type = st.selectbox("Jednostka", ["dni", "tygodnie", "miesiące", "lata"], index=2) + + if st.form_submit_button("Zapisz wydatek"): + add_expense(name, cat, amt, due, is_cyclic_mode, int_qty, int_type) + st.success("Dodano!") + st.rerun() + else: st.info("Formularz celów znajduje się na stronie głównej.") + +df = load_data() +today = date.today() + +# --- WIDOK 1: LISTA I BUDŻET --- +if page == "Lista & Budżet": + st.title("📊 Finanse & Budżet") + + if not df.empty: + col_btn, col_date, _ = st.columns([1, 2, 3]) + with col_btn: st.button("📅 Dzisiaj", on_click=set_today) + with col_date: view_date = st.date_input("Pokaż miesiąc:", key="view_date_picker", format="DD.MM.YYYY") + + start_month = view_date.replace(day=1) + _, last_day = calendar.monthrange(view_date.year, view_date.month) + end_month = view_date.replace(day=last_day) + + mask_month_strict = (df['termin'] >= start_month) & (df['termin'] <= end_month) + mask_overdue_global = (df['termin'] < today) & (~df['zaplacone']) + final_mask = mask_month_strict | mask_overdue_global + + df_filtered = df[final_mask].copy() + + # KPI + mask_unpaid_view = ~df_filtered['zaplacone'] + mask_overdue_view = mask_unpaid_view & (df_filtered['termin'] < today) + + c1, c2 = st.columns(2) + c1.metric("Do zapłacenia (Widok)", f"{df_filtered[mask_unpaid_view]['kwota'].sum():.2f} PLN") + overdue_cnt = len(df_filtered[mask_overdue_view]) + c2.metric("Przeterminowane (Pilne)", f"{df_filtered[mask_overdue_view]['kwota'].sum():.2f} PLN", + delta=f"-{overdue_cnt} szt." if overdue_cnt else "Ok", delta_color="normal") + + st.divider() + + with st.expander("📉 Realizacja Budżetów (Ten miesiąc)", expanded=True): + budgets = get_budgets() + if budgets: + df_month_only = df[mask_month_strict] + spending_by_cat = df_month_only.groupby('kategoria')['kwota'].sum() + for cat_name, limit in budgets.items(): + spent = spending_by_cat.get(cat_name, 0.0) + + # --- NOWA LOGIKA WYŚWIETLANIA BUDŻETU --- + diff = spent - limit + + # Jeśli budżet jest 0 lub ujemny (błąd), traktujemy jako 100% przekroczenia + if limit > 0: + percent = spent / limit + else: + percent = 1.0 if spent > 0 else 0.0 + + if diff > 0: + # PRZEKROCZENIE - CZERWONY TEKST I PEŁNY PASEK + st.markdown(f"**{cat_name}**: {spent:.2f} / {limit:.2f} PLN :red[**⚠️ Przekroczono o {diff:.2f} PLN!**]") + st.progress(1.0) + else: + # W NORMIE + st.write(f"**{cat_name}**: {spent:.2f} / {limit:.2f} PLN ({percent*100:.0f}%)") + st.progress(min(percent, 1.0)) + else: + st.info("Brak budżetów. Ustaw je w zakładce Ustawienia.") + + # Tabela + termin_as_dt = pd.to_datetime(df_filtered['termin']) + today_as_dt = pd.to_datetime(today) + df_filtered['dni_opoznienia'] = (today_as_dt - termin_as_dt).dt.days + df_filtered.loc[df_filtered['zaplacone'] | (df_filtered['dni_opoznienia'] <= 0), 'dni_opoznienia'] = None + + df_view = df_filtered.sort_values(by=['zaplacone', 'termin']) + editor_key = f"main_editor_{view_date.strftime('%Y%m')}" + + edited_df = st.data_editor( + df_view, + column_config={ + "id": None, "interwal_ilosc": None, "interwal_typ": None, + "kwota": st.column_config.NumberColumn(format="%.2f PLN"), + "termin": st.column_config.DateColumn(format="YYYY-MM-DD"), + "zaplacone": st.column_config.CheckboxColumn(label="Zapłacone?"), + "dni_opoznienia": st.column_config.NumberColumn(label="Spóźnienie", format="%d dni"), + }, + disabled=["id", "nazwa", "kategoria", "kwota", "termin", "cykliczne", "dni_opoznienia", "interwal_ilosc", "interwal_typ"], + hide_index=True, use_container_width=True, key=editor_key + ) + + # Sync + old_status = dict(zip(df_filtered['id'], df_filtered['zaplacone'])) + new_status = dict(zip(edited_df['id'], edited_df['zaplacone'])) + changes_made = False + for eid, paid in new_status.items(): + if old_status.get(eid) != paid: + update_status(eid, paid) + changes_made = True + row = df_filtered[df_filtered['id'] == eid].iloc[0] + if paid and row['cykliczne']: + try: i_ilosc = int(row.get('interwal_ilosc', 1)) + except: i_ilosc = 1 + i_typ = str(row.get('interwal_typ', 'miesiące')) + + current_date_dt = pd.to_datetime(row['termin']) + if i_typ == 'dni': offset = pd.DateOffset(days=i_ilosc) + elif i_typ == 'tygodnie': offset = pd.DateOffset(weeks=i_ilosc) + elif i_typ == 'miesiące': offset = pd.DateOffset(months=i_ilosc) + elif i_typ == 'lata': offset = pd.DateOffset(years=i_ilosc) + else: offset = pd.DateOffset(months=1) + next_date = (current_date_dt + offset).date() + add_expense(row['nazwa'], row['kategoria'], row['kwota'], next_date, True, i_ilosc, i_typ) + st.toast(f"Automatycznie dodano kolejną płatność: {next_date}") + if changes_made: st.rerun() + + with st.expander("🗑️ Usuwanie"): + options = {f"{row['nazwa']} ({row['kwota']} zł)": row['id'] for _, row in df_filtered.iterrows()} + selected = st.multiselect("Wybierz pozycje:", list(options.keys())) + if st.button("Usuń zaznaczone") and selected: + for label in selected: delete_expense(options[label]) + st.rerun() + else: st.info("Brak wpisów.") + +# --- WIDOK 2: KALENDARZ --- +elif page == "Kalendarz": + st.title("📅 Kalendarz") + col_btn, col_date, _ = st.columns([1, 2, 3]) + with col_btn: st.button("📅 Dzisiaj", on_click=set_today, key="cal_today_btn") + with col_date: + selected_date = st.date_input("Miesiąc", key="view_date_picker") + year, month = selected_date.year, selected_date.month + + cal = calendar.Calendar(firstweekday=0) + month_days = cal.monthdayscalendar(year, month) + + cols = st.columns(7) + for i, d in enumerate(["Pn", "Wt", "Śr", "Cz", "Pt", "Sb", "Nd"]): + cols[i].markdown(f"
{d}
", unsafe_allow_html=True) + + if not df.empty: + df['year'] = pd.to_datetime(df['termin']).dt.year + df['month'] = pd.to_datetime(df['termin']).dt.month + df['day'] = pd.to_datetime(df['termin']).dt.day + monthly_data = df[(df['year'] == year) & (df['month'] == month)] + else: + monthly_data = pd.DataFrame(columns=['day', 'kwota', 'nazwa', 'zaplacone', 'id', 'termin']) + + for week in month_days: + cols = st.columns(7) + for i, day_num in enumerate(week): + with cols[i]: + if day_num == 0: continue + is_today = (date(year, month, day_num) == today) + day_header = f":red[**{day_num}**]" if is_today else f"**{day_num}**" + with st.container(height=140, border=True): + st.markdown(day_header) + if not monthly_data.empty: + day_items = monthly_data[monthly_data['day'] == day_num] + for _, row in day_items.iterrows(): + kwota = f"{row['kwota']:.0f}" + if row['zaplacone']: label, btn_type = f"✅ {kwota} | {row['nazwa']}", "secondary" + elif row['termin'] < today: label, btn_type = f"⚠️ {kwota} | {row['nazwa']}", "primary" + else: label, btn_type = f"⭕ {kwota} | {row['nazwa']}", "secondary" + if st.button(label, key=f"btn_{row['id']}", type=btn_type, use_container_width=True): + confirm_status_change(row) + +# --- WIDOK 3: CELE (SKARBONKA) --- +elif page == "Cele (Skarbonka)": + st.title("🎯 Moje Cele Finansowe") + with st.expander("➕ Dodaj nowy cel", expanded=False): + has_deadline = st.toggle("Ustal termin realizacji?", value=True) + with st.form("new_goal_form"): + c1, c2, c3 = st.columns([2, 1, 1]) + with c1: g_name = st.text_input("Nazwa celu (np. Wakacje)") + with c2: g_target = st.number_input("Kwota docelowa", min_value=1.0, step=100.0) + with c3: g_icon = st.selectbox("Ikona", ["🎯", "🚗", "🏠", "💻", "🌴", "💍", "💰", "🆘"]) + g_date = None + if has_deadline: g_date = st.date_input("Planowana data", value=date.today()) + if st.form_submit_button("Utwórz cel"): + add_goal(g_name, g_target, g_date, g_icon) + st.success("Dodano cel!") + st.rerun() + st.divider() + goals_df = get_goals() + if not goals_df.empty: + grid = st.columns(2) + for idx, row in goals_df.iterrows(): + with grid[idx % 2]: + with st.container(border=True): + col_head1, col_head2 = st.columns([4, 1]) + with col_head1: st.subheader(f"{row['ikona']} {row['nazwa']}") + with col_head2: + if st.button("🗑️", key=f"del_goal_{row['id']}", help="Usuń ten cel"): + delete_goal(row['id']) + st.rerun() + progress = row['kwota_obecna'] / row['kwota_cel'] if row['kwota_cel'] > 0 else 0 + st.progress(min(progress, 1.0)) + st.write(f"Uzbierano: **{row['kwota_obecna']:.2f}** / {row['kwota_cel']:.2f} PLN ({progress*100:.1f}%)") + missing = row['kwota_cel'] - row['kwota_obecna'] + if missing <= 0: + st.balloons() + st.success("🎉 Cel osiągnięty!") + else: + st.caption(f"Brakuje: {missing:.2f} PLN") + if pd.notnull(row['termin']): + days_left = (row['termin'] - today).days + if days_left > 0: st.caption(f"📅 Czas do końca: {days_left} dni") + else: st.caption(f"📅 Termin minął!") + else: st.caption("📅 Termin: Bezterminowo") + st.divider() + c_in, c_btn = st.columns([2, 1]) + with c_in: amount = st.number_input("Kwota", min_value=0.0, step=50.0, key=f"pay_{row['id']}", label_visibility="collapsed") + with c_btn: + if st.button("Wpłać", key=f"deposit_{row['id']}", type="primary"): + update_goal_amount(row['id'], amount) + st.rerun() + if st.button("Wypłać", key=f"withdraw_{row['id']}"): + update_goal_amount(row['id'], -amount) + st.rerun() + else: st.info("Nie masz jeszcze żadnych celów. Dodaj pierwszy powyżej!") + +# --- WIDOK 4: USTAWIENIA --- +elif page == "Ustawienia": + st.title("⚙️ Konfiguracja") + col1, col2 = st.columns(2) + with col1: + st.subheader("Kategorie") + new_cat = st.text_input("Nowa kategoria") + if st.button("Dodaj kategorię"): + if new_cat and add_category_db(new_cat): + st.success(f"Dodano: {new_cat}") + st.rerun() + else: st.error("Błąd lub duplikat.") + st.divider() + cat_to_del = st.selectbox("Wybierz do usunięcia", categories_list) + if st.button("Usuń", type="primary"): + delete_category_db(cat_to_del) + st.rerun() + + with col2: + st.subheader("Budżety") + current_budgets = get_budgets() + + st.caption("Wybierz kategorię, aby ustawić limit:") + selected_cat_budget = st.selectbox("Kategoria", categories_list, key="budget_cat_sel") + + current_val = current_budgets.get(selected_cat_budget, 0.0) + + with st.form("budget_form"): + new_limit = st.number_input("Limit miesięczny (PLN)", value=current_val, step=100.0, key=f"bud_{selected_cat_budget}") + if st.form_submit_button("Zapisz budżet"): + set_budget_db(selected_cat_budget, new_limit) + st.rerun() + + st.divider() + st.markdown("#### Aktualne limity:") + + if current_budgets: + for c, l in current_budgets.items(): + bc1, bc2 = st.columns([3, 1]) + with bc1: st.write(f"**{c}**: {l:.2f} PLN") + with bc2: + if st.button("🗑️", key=f"del_bud_{c}", help="Usuń limit"): + confirm_delete_budget(c) + else: + st.info("Brak zdefiniowanych budżetów.") diff --git a/autostart b/autostart new file mode 100644 index 0000000..f32a580 --- /dev/null +++ b/autostart @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/data/finanse.db b/data/finanse.db new file mode 100644 index 0000000000000000000000000000000000000000..ffc7d131aff84e57e0f4a048bc799cc746b5f432 GIT binary patch literal 32768 zcmeI5e{3699l+n66Z?Fzb1hv6o@xEEGBsHe>~rFz>F8P~xpbjU(k1Rfr)uHFzJ;4F zKF2vXizOu5Q3TQiLI@!S+A4|{=%)=}Co(x##n>Fj)7IiW1lQ;V~6 z*~K&Tq;iI4Pv;kA=RtT*na>ZkNR(N9i_yIDg*=^KfXC@vuJwY_mSM6LutmXk+jGXM zS|~I7G@VvvvZr%-nrc;Is?~L^OsB#5))QLE08z`l@72J)ouS@%oIAhIj(xROtgGg> z`x0y+;2xy?C1$E?MpbhoX^MS_>e{+yuGnGNJ`P-9oNeG@%r>yKU1VlSb6@xt1Mb29 z0_BzfS{XH5Z#{lpsDrfzD_T`Z`F22Sz8-oZicj7@lRLx=I{%BH$eQ_BT)*KVMN z_^y-vWwx#kf#iTxCii^ccGXtFa4^`LjdMgR7u5$``>bXf_Wp{SV=J<|5t9DljHu@V z!QMoIE7>7>38?xT9W7z`hbctlx45P}9gsVMy-MPGUik7WvcK60(b!}AC~Vvk`Wum6 zlzt?Yq@nINx*zFY?%o^yZS* zd^X%Cyea%jct|)c8~`O)kN^@u0!RP}Ac2o2fx%lv5{vPDLO7gQs?}>HZQ58jw$#v- zbSgbIoJtSNhy4#@na_wM6X*NFLb&IY0iSi%61@{XRaMJcp~UFuj8?T|12V{t+p-y* znJjbA6$`(=m}Alw8@WX!@p#B~?c|oBgE7~@@$)&GrtjjI@8ZEekwjxcpCE)2P9*+C z@C}N} zqn_v;2ShSFXwyq%%WImdZe@dKyp}Cn-ks1*s+LD zVCs{g#%)p+x0SS4<0L5GN+$56080WMhYecp4OGtCb@2auL?qfR_#@h%T`2;|;Y(t_<8Ae5|(y{c& zggidswZC(Jc@fG>jw~c;+7JH7-gdHw(O=lXid)mo@hhE2W{OLU} zpY6Z)8_#Z2*K|Z`Cp#vO%bw4VZ(RHJx6TguvW<*Wx?R=~?`%i^W(9r>Hy@B#Xmq?~uUnbc$)SzuW znI5B?l`1V)DcmhFnHSVg*AFzDfi}@_W?B@oc78Xfh~Jo2;e zLwVS9r%8(vZV+y(C4G2e#B=7-cbCPZl^=U*(xW~ZMR*@Ismw^qQ*-}guQsb`>Lip< zqiwZ#lW}4KC8KFi&E?LEz~-;uO=egIG!1W#21Rl^bQe}LSyJ_LY;8?5pnFKBN=Z3o zRanMVB3*LCAzd0|Go8?#yWK>cS$A2WWoB!eCfs{++Ggi#9)l*a{tl7sbH%1si%u6p z9`dhjykDdP@K)tsxy&kt&Z^sW2EB-*o%0Ddp@~ZG5?*?pNUuv*t<=JT1dsp{Kmter z2_OL^fCP{L5=Z09*fPQaK5j2$o%c@92X0e|PjBu(sXdKOYXn!p`{Ln*ZmdzZ3Ywf&`EN5xdUL$|MSudM0yLJupj{>fCP{L5T>auZk3Rgpe*sCU_Co*w literal 0 HcmV?d00001 diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..e06d13d --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,7 @@ +services: + finanse-app: + labels: + net.unraid.docker.managed: 'composeman' + net.unraid.docker.icon: '' + net.unraid.docker.webui: '' + net.unraid.docker.shell: '' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..56c1c2d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +services: + finanse-app: + container_name: finanse-domowe + # 'build: .' mówi Unraidowi, żeby zbudował obraz z Dockerfile w tym samym folderze + build: . + image: finanse-domowe:latest + restart: unless-stopped + ports: + - "8501:8501" + env_file: + - .env + environment: + - TZ=${TIMEZONE} + volumes: + # Mapowanie folderu data, aby baza danych przetrwała restarty + # Ścieżka ./data oznacza folder 'data' w katalogu, gdzie leży plik compose + - ./data:/app/data \ No newline at end of file diff --git a/name b/name new file mode 100644 index 0000000..c977369 --- /dev/null +++ b/name @@ -0,0 +1 @@ +Wydatki \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a930bdb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +streamlit +pandas diff --git a/requirements.txt~ b/requirements.txt~ new file mode 100644 index 0000000..e69de29