File size: 7,794 Bytes
ad813fc
 
91258bf
ad813fc
 
 
fd42852
 
ad813fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd42852
 
ad813fc
 
 
 
 
 
fd42852
 
 
 
0d872e9
 
ad813fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd42852
 
 
 
 
 
 
 
 
 
 
ad813fc
fd42852
ad813fc
 
 
 
fd42852
ad813fc
 
 
 
a706a46
 
fd42852
 
 
 
 
 
 
 
a706a46
fd42852
a706a46
 
 
 
 
 
 
ad813fc
fd42852
 
 
ad813fc
 
 
fd42852
 
0d872e9
 
fd42852
 
 
 
0d872e9
fd42852
 
 
 
 
57e2194
fd42852
 
 
 
 
 
 
91258bf
 
ad813fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd42852
ad813fc
 
 
 
 
fd42852
0d872e9
 
 
 
 
 
fd42852
0d872e9
fd42852
0d872e9
 
fd42852
0d872e9
 
fd42852
 
0d872e9
fd42852
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ad813fc
 
 
fd42852
 
ad813fc
 
 
 
 
aca9d3a
fd42852
 
0d872e9
aa0d878
 
 
 
0d872e9
aa0d878
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import streamlit as st

from components.buttons import render_top_buttons
from components.card import render_card
from components.header import render_header
from services.api import delete_card as api_delete_card
from services.api import load_cards, load_tabs, save_card, save_tab, update_card
from utils.time_utils import build_card_payload, normalize_cards, sort_cards_by_upcoming_time


st.set_page_config(
    page_title="Time Card Generator",
    page_icon="⏱️",
    layout="wide",
    initial_sidebar_state="collapsed",
)


def load_css() -> None:
    with open("assets/style.css", "r", encoding="utf-8") as css_file:
        st.markdown(f"<style>{css_file.read()}</style>", unsafe_allow_html=True)


def initialize_state() -> None:
    if "cards" not in st.session_state:
        st.session_state.cards = []
    if "tabs" not in st.session_state:
        st.session_state.tabs = []
    if "cards_loaded" not in st.session_state:
        st.session_state.cards_loaded = False
    if "status_message" not in st.session_state:
        st.session_state.status_message = None
    if "status_type" not in st.session_state:
        st.session_state.status_type = "info"
    if "current_tab_id" not in st.session_state:
        st.session_state.current_tab_id = ""
    if "new_tab_counter" not in st.session_state:
        st.session_state.new_tab_counter = 1
    if "new_tab_name" not in st.session_state:
        st.session_state.new_tab_name = ""


def set_status(message: str, status_type: str = "info") -> None:
    st.session_state.status_message = message
    st.session_state.status_type = status_type


def clear_status() -> None:
    st.session_state.status_message = None
    st.session_state.status_type = "info"


def fetch_cards_once() -> None:
    if st.session_state.cards_loaded:
        return

    try:
        st.session_state.tabs = load_tabs()
        if not st.session_state.tabs:
            default_tab = save_tab({"title": "Tab 1"})
            st.session_state.tabs = [default_tab]
            st.session_state.new_tab_counter = 2
        else:
            st.session_state.new_tab_counter = len(st.session_state.tabs) + 1

        if not st.session_state.current_tab_id:
            st.session_state.current_tab_id = st.session_state.tabs[0]["id"]

        st.session_state.cards = normalize_cards(load_cards())
        assign_missing_tab_ids()
        st.session_state.cards_loaded = True
        clear_status()
    except Exception as exc:  # noqa: BLE001
        st.session_state.cards = []
        st.session_state.tabs = []
        st.session_state.cards_loaded = True
        set_status(f"Unable to load saved cards: {exc}", "error")


def reload_cards() -> None:
    try:
        st.session_state.tabs = load_tabs()
        if not st.session_state.tabs:
            default_tab = save_tab({"title": "Tab 1"})
            st.session_state.tabs = [default_tab]
        tab_ids = {tab["id"] for tab in st.session_state.tabs}
        if st.session_state.current_tab_id not in tab_ids:
            st.session_state.current_tab_id = st.session_state.tabs[0]["id"]
        st.session_state.new_tab_counter = len(st.session_state.tabs) + 1
        st.session_state.cards = normalize_cards(load_cards())
        assign_missing_tab_ids()
        st.session_state.cards_loaded = True
        clear_status()
        set_status("Cards refreshed.", "success")
    except Exception as exc:  # noqa: BLE001
        set_status(f"Refresh failed: {exc}", "error")


def add_card() -> None:
    card = build_card_payload()
    card["tabId"] = st.session_state.current_tab_id
    st.session_state.cards.append(card)
    clear_status()


def add_tab() -> None:
    try:
        typed_title = st.session_state.get("new_tab_name", "").strip()
        title = typed_title or f"Tab {st.session_state.new_tab_counter}"
        new_tab = save_tab({"title": title})
        st.session_state.tabs.append(new_tab)
        st.session_state.current_tab_id = new_tab["id"]
        st.session_state.new_tab_counter += 1
        st.session_state.new_tab_name = ""
        clear_status()
        set_status("Tab created.", "success")
    except Exception as exc:  # noqa: BLE001
        set_status(f"Unable to create tab: {exc}", "error")


def assign_missing_tab_ids() -> None:
    if not st.session_state.tabs:
        return
    fallback_tab_id = st.session_state.tabs[0]["id"]
    for card in st.session_state.cards:
        if not card.get("tabId"):
            card["tabId"] = fallback_tab_id


def delete_local_card(index: int) -> None:
    st.session_state.cards.pop(index)
    clear_status()


def handle_delete(index: int) -> None:
    card = st.session_state.cards[index]
    card_id = card.get("id")

    if not card_id:
        delete_local_card(index)
        return

    try:
        api_delete_card(card_id)
        delete_local_card(index)
        set_status("Card deleted.", "success")
    except Exception as exc:  # noqa: BLE001
        set_status(f"Delete failed: {exc}", "error")


def handle_save(index: int) -> None:
    card = st.session_state.cards[index]

    if not card["title"].strip():
        set_status("Title is required before saving.", "warning")
        return

    try:
        if card.get("id"):
            updated_card = update_card(card)
            st.session_state.cards[index] = updated_card
            set_status("Card updated.", "success")
        else:
            saved_card = save_card(card)
            st.session_state.cards[index] = saved_card
            set_status("Card saved.", "success")
    except Exception as exc:  # noqa: BLE001
        set_status(f"Save failed: {exc}", "error")


load_css()
initialize_state()
fetch_cards_once()
render_top_buttons(add_card, add_tab, reload_cards)
render_header()

if st.session_state.status_message:
    getattr(st, st.session_state.status_type)(st.session_state.status_message)

if st.session_state.tabs:
    tab_title_by_id = {tab["id"]: tab["title"] for tab in st.session_state.tabs}
    tab_ids = [tab["id"] for tab in st.session_state.tabs]
    current_tab_id = (
        st.session_state.current_tab_id
        if st.session_state.current_tab_id in tab_ids
        else tab_ids[0]
    )
    selected_tab_id = st.radio(
        "Tabs",
        options=tab_ids,
        format_func=lambda tab_id: tab_title_by_id.get(tab_id, "Untitled Tab"),
        horizontal=True,
        key="selected_tab_id",
        index=tab_ids.index(current_tab_id),
        label_visibility="collapsed",
    )
    st.session_state.current_tab_id = selected_tab_id

visible_cards = [
    card for card in st.session_state.cards
    if card.get("tabId", "") == st.session_state.current_tab_id
]
visible_cards = sort_cards_by_upcoming_time(visible_cards)

visible_card_ids = [card.get("id") or card.get("clientKey") for card in visible_cards]
display_indices = []
for visible_id in visible_card_ids:
    for index, card in enumerate(st.session_state.cards):
        candidate_id = card.get("id") or card.get("clientKey")
        if card.get("tabId", "") == st.session_state.current_tab_id and candidate_id == visible_id:
            display_indices.append(index)
            break

if not display_indices:
    st.markdown(
        """
        <div class="empty-state">
            <h3>No cards in this tab</h3>
            <p>Add a card or switch to another tab.</p>
        </div>
        """,
        unsafe_allow_html=True,
    )

card_columns = st.columns(3, gap="medium")
for position, index in enumerate(display_indices):
    with card_columns[position % 3]:
        rank_tone = "danger" if position < 2 else "warning" if position < 4 else "default"
        render_card(
            index=index,
            on_save=handle_save,
            on_delete=handle_delete,
            rank_tone=rank_tone,
        )