diff --git a/app.py b/app.py index 84c66e3..3038480 100644 --- a/app.py +++ b/app.py @@ -213,6 +213,14 @@ def update_status(expense_id, is_paid): conn.commit() conn.close() +def update_expense_details(expense_id, nazwa, kategoria, kwota, termin): + conn = sqlite3.connect(DB_FILE) + c = conn.cursor() + c.execute("UPDATE wydatki SET nazwa=?, kategoria=?, kwota=?, termin=? WHERE id=?", + (nazwa, kategoria, kwota, termin, expense_id)) + conn.commit() + conn.close() + def delete_expense(expense_id): conn = sqlite3.connect(DB_FILE) c = conn.cursor() @@ -223,11 +231,14 @@ def delete_expense(expense_id): init_db() # --- CALLBACKS --- -if "view_date_picker" not in st.session_state: - st.session_state.view_date_picker = date.today() +if "list_date_picker" not in st.session_state: + st.session_state.list_date_picker = date.today() +if "cal_date_picker" not in st.session_state: + st.session_state.cal_date_picker = date.today() def set_today(): - st.session_state.view_date_picker = date.today() + st.session_state.list_date_picker = date.today() + st.session_state.cal_date_picker = date.today() # --- DIALOGI --- @st.dialog("Zmień status płatności") @@ -264,13 +275,50 @@ def confirm_status_change(row): 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) + c1, c2 = st.columns(2) + if c1.button("Nie, anuluj"): + st.rerun() + + if c2.button("Tak, zatwierdź", type="primary"): + update_status(int(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("✏️ Edytuj płatność") +def edit_expense_dialog(row, categories_list): + with st.form(f"edit_form_{row['id']}"): + e_name = st.text_input("Nazwa", value=row['nazwa']) + + cat_index = categories_list.index(row['kategoria']) if row['kategoria'] in categories_list else 0 + e_cat = st.selectbox("Kategoria", categories_list, index=cat_index) + + e_amt = st.number_input("Kwota", value=float(row['kwota']), min_value=0.0, step=10.0) + + try: e_date_val = pd.to_datetime(row['termin']).date() + except: e_date_val = date.today() + + e_due = st.date_input("Termin", value=e_date_val) + + if st.form_submit_button("Potwierdź zmiany"): + update_expense_details(int(row['id']), e_name, e_cat, e_amt, e_due) + st.success("Zapisano!") + st.rerun() + +@st.dialog("🗑️ Potwierdź usunięcie") +def delete_expense_dialog(row): + st.warning(f"Czy na pewno chcesz usunąć płatność: **{row['nazwa']}** na kwotę **{row['kwota']} PLN**?") + c1, c2 = st.columns(2) + if c1.button("Nie, anuluj"): + st.rerun() + if c2.button("Tak, usuń", type="primary"): + delete_expense(int(row['id'])) + st.success("Usunięto!") + 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}**?") @@ -320,9 +368,10 @@ if page == "Lista & Budżet": st.title("📊 Finanse & Budżet") if not df.empty: - col_btn, col_date, _ = st.columns([1, 2, 3]) + col_btn, col_date, col_search = 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") + with col_date: view_date = st.date_input("Pokaż miesiąc:", key="list_date_picker", format="DD.MM.YYYY") + with col_search: search_query = st.text_input("🔍 Szukaj płatności (nazwa, kategoria)...", "") start_month = view_date.replace(day=1) _, last_day = calendar.monthrange(view_date.year, view_date.month) @@ -334,6 +383,13 @@ if page == "Lista & Budżet": df_filtered = df[final_mask].copy() + # Wyszukiwanie + if search_query: + df_filtered = df_filtered[ + df_filtered['nazwa'].str.contains(search_query, case=False, na=False) | + df_filtered['kategoria'].str.contains(search_query, case=False, na=False) + ] + # KPI mask_unpaid_view = ~df_filtered['zaplacone'] mask_overdue_view = mask_unpaid_view & (df_filtered['termin'] < today) @@ -353,80 +409,99 @@ if page == "Lista & Budżet": 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 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 + # --- ZAKŁADKI: Tabela vs Podsumowanie --- + tab1, tab2 = st.tabs(["📋 Lista Płatności", "📈 Zestawienie Kategorii"]) - df_view = df_filtered.sort_values(by=['zaplacone', 'termin']) - editor_key = f"main_editor_{view_date.strftime('%Y%m')}" + with tab1: + 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 - 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 - ) + df_view = df_filtered.sort_values(by=['zaplacone', 'termin']) - # 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')) + st.caption("👈 Kliknij w kwadracik po lewej stronie wiersza, aby go zaznaczyć i wywołać opcje edycji/usuwania.") - 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() + event = st.dataframe( + df_view, + on_select="rerun", + selection_mode="single-row", + column_config={ + "id": None, "interwal_ilosc": None, "interwal_typ": None, + "kategoria": "Kategoria", + "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"), + }, + use_container_width=True, + hide_index=True + ) + + # Obsługa zaznaczonego wiersza + selected_rows = event.selection.rows + if selected_rows: + selected_idx = selected_rows[0] + selected_row = df_view.iloc[selected_idx] + + st.markdown(f"Wybrana pozycja: **{selected_row['nazwa']}**") + + col_ed, col_del, col_status, _ = st.columns([1, 1, 2, 3]) + + if col_ed.button("✏️ Edytuj", key="btn_edit"): + edit_expense_dialog(selected_row, categories_list) + + if col_del.button("🗑️ Usuń", key="btn_delete"): + delete_expense_dialog(selected_row) + + status_label = "⭕ Oznacz jako NIEZAPŁACONE" if selected_row['zaplacone'] else "✅ Oznacz jako ZAPŁACONE" + if col_status.button(status_label, key="btn_status", type="primary"): + confirm_status_change(selected_row) + + with tab2: + st.subheader("Bieżący miesiąc - Zobowiązania wg kategorii") + summary_data = [] + + for cat in df_filtered['kategoria'].unique(): + cat_df = df_filtered[df_filtered['kategoria'] == cat] + do_zaplaty = cat_df[~cat_df['zaplacone']]['kwota'].sum() + przeterm = cat_df[(~cat_df['zaplacone']) & (cat_df['termin'] < today)]['kwota'].sum() + + if do_zaplaty > 0 or przeterm > 0: + summary_data.append({ + "Kategoria": cat, + "Do zapłacenia (PLN)": float(do_zaplaty), + "Przeterminowane (PLN)": float(przeterm) + }) + + if summary_data: + sum_df = pd.DataFrame(summary_data) + st.dataframe( + sum_df, + use_container_width=True, + hide_index=True, + column_config={ + "Do zapłacenia (PLN)": st.column_config.NumberColumn(format="%.2f"), + "Przeterminowane (PLN)": st.column_config.NumberColumn(format="%.2f") + } + ) + else: + st.success("Wszystko opłacone w tym widoku! 🎉") - 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 --- @@ -435,7 +510,7 @@ elif page == "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") + selected_date = st.date_input("Miesiąc", key="cal_date_picker") year, month = selected_date.year, selected_date.month cal = calendar.Calendar(firstweekday=0) diff --git a/data/finanse.db b/data/finanse.db index ffc7d13..7b47e73 100644 Binary files a/data/finanse.db and b/data/finanse.db differ