Skip to content

Commit 5591204

Browse files
committed
re-arranged order of summary table in page 4. added found buttons to parts found in sets.
1 parent 45641ba commit 5591204

File tree

3 files changed

+169
-72
lines changed

3 files changed

+169
-72
lines changed

core/state/find_wanted_parts.py

Lines changed: 90 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -136,73 +136,62 @@ def merge_set_results(original_df: pd.DataFrame, set_results: Dict) -> pd.DataFr
136136
return result_df
137137

138138

139-
def render_missing_parts_by_set(set_results: Dict, merged_df: pd.DataFrame,
139+
def render_missing_parts_by_set(set_results: Dict, merged_df: pd.DataFrame,
140140
part_images_map: Dict, ba_part_names: Dict,
141141
color_lookup: Dict) -> None:
142142
"""
143-
Render missing parts grouped by set in a separate section.
144-
145-
This function displays set search results in a dedicated section,
146-
showing which parts from the wanted list can be found in owned sets.
147-
Parts are grouped by set for easy identification.
148-
149-
Args:
150-
set_results: Dictionary mapping (part_num, color_name) to list of set locations
151-
merged_df: Original merged dataframe (for part details)
152-
part_images_map: Dictionary mapping part numbers to image URLs
153-
ba_part_names: Dictionary mapping part numbers to BrickArchitect names
154-
color_lookup: Dictionary for color rendering (maps color_id to color info dict)
155-
156-
Requirements: 7.2, 7.8, 8.3
143+
Render missing parts grouped by set, with color/missing/available/found columns
144+
matching the location-based layout.
157145
"""
158146
import streamlit as st
159147
from core.data.colors import render_color_cell
160-
148+
from core.infrastructure.session import short_key
149+
161150
if not set_results:
162151
return
163-
152+
164153
st.markdown("---")
165-
st.markdown("### 📦 Missing Parts Grouped by Set")
154+
st.markdown("### 📦 Missing Parts Found in Owned Sets")
166155
st.markdown("Parts from your wanted list that can be found in your owned sets:")
167-
156+
168157
# Build reverse color lookup: color_name -> color_id
169158
color_name_to_id = {}
170159
for color_id, color_info in color_lookup.items():
171160
color_name = color_info.get("name", "")
172161
if color_name:
173162
color_name_to_id[color_name] = color_id
174-
175-
# Reorganize results by set instead of by part
163+
164+
# Initialize set_found_counts in session state
165+
if "set_found_counts" not in st.session_state:
166+
st.session_state["set_found_counts"] = {}
167+
168+
# Reorganize results by set
176169
sets_dict = {}
177170
for (part_num, color_name), locations in set_results.items():
178171
for location_info in locations:
179172
set_number = location_info["set_number"]
180173
set_name = location_info["set_name"]
181174
set_key = f"{set_number} - {set_name}"
182-
175+
183176
if set_key not in sets_dict:
184177
sets_dict[set_key] = []
185-
186-
# Convert color name back to color ID for matching with merged_df
178+
187179
color_id = color_name_to_id.get(color_name)
188-
189-
# Find the wanted and have quantities from merged_df
180+
181+
# Find wanted and have quantities from merged_df
190182
qty_wanted = 0
191183
qty_have = 0
192-
193184
if color_id is not None:
194185
matching_rows = merged_df[
195-
(merged_df["Part"].astype(str) == str(part_num)) &
186+
(merged_df["Part"].astype(str) == str(part_num)) &
196187
(merged_df["Color"].astype(str) == str(color_id))
197188
]
198-
199189
if not matching_rows.empty:
200190
qty_wanted = int(matching_rows.iloc[0].get("Quantity_wanted", 0))
201191
qty_have = int(matching_rows.iloc[0].get("Quantity_have", 0))
202-
203-
# Calculate missing quantity (wanted - have in collection)
192+
204193
qty_missing = max(0, qty_wanted - qty_have)
205-
194+
206195
sets_dict[set_key].append({
207196
"part_num": part_num,
208197
"color_name": color_name,
@@ -211,8 +200,8 @@ def render_missing_parts_by_set(set_results: Dict, merged_df: pd.DataFrame,
211200
"qty_missing": qty_missing,
212201
"is_spare": location_info.get("is_spare", False)
213202
})
214-
215-
# Display each set with its parts
203+
204+
# Display each set with its parts (layout matching location cards)
216205
for set_key, parts_list in sorted(sets_dict.items()):
217206
with st.expander(f"📦 {set_key} ({len(parts_list)} part type(s))", expanded=True):
218207
for part_info in parts_list:
@@ -222,46 +211,76 @@ def render_missing_parts_by_set(set_results: Dict, merged_df: pd.DataFrame,
222211
quantity = part_info["quantity"]
223212
qty_missing = part_info["qty_missing"]
224213
is_spare = part_info["is_spare"]
225-
226-
# Get part image and name
214+
227215
img_url = part_images_map.get(str(part_num), "")
228216
ba_name = ba_part_names.get(str(part_num), "")
229-
230-
# Display part
217+
231218
left, right = st.columns([1, 4])
232-
219+
233220
with left:
234-
st.markdown(f"**{part_num}**")
221+
st.markdown(f"##### **{part_num}**")
235222
if ba_name:
236223
st.markdown(f"{ba_name}")
237-
238224
if img_url:
239225
st.image(img_url, width=100)
240226
else:
241227
st.text("🚫 No image")
242-
228+
243229
with right:
244-
# Render color
230+
# Header row matching location card style
231+
header = st.columns([2.5, 1, 1, 2])
232+
header[0].markdown("**Color**")
233+
header[1].markdown("**Missing**")
234+
header[2].markdown("**Available**")
235+
header[3].markdown("**Found**")
236+
237+
# Color cell
245238
if color_id is not None:
246239
color_html = render_color_cell(color_id, color_lookup)
247240
else:
248241
color_html = f"<span>{color_name}</span>"
249-
250-
st.markdown(f"**Color:** {color_html}", unsafe_allow_html=True)
251-
st.markdown(f"**Missing:** {qty_missing}")
252-
st.markdown(f"**Available in set:** {quantity}")
253-
254-
if is_spare:
255-
st.markdown("**(Spare part)**")
256-
257-
if quantity >= qty_missing:
258-
st.markdown(f"✅ **Sufficient quantity available**")
242+
243+
# Available display with status indicator
244+
available_in_set = min(quantity, qty_missing)
245+
if available_in_set >= qty_missing:
246+
available_display = f"✅ {available_in_set}"
259247
else:
260-
st.markdown(f"⚠️ **Partial match** ({quantity}/{qty_missing})")
261-
248+
available_display = f"⚠️ {available_in_set}"
249+
250+
if is_spare:
251+
available_display += " *(spare)*"
252+
253+
# Found input
254+
found_key = (part_num, color_name, set_key)
255+
current_found = st.session_state.get("set_found_counts", {}).get(found_key, 0)
256+
max_found = min(quantity, qty_missing)
257+
258+
cols = st.columns([2.5, 1, 1, 2])
259+
cols[0].markdown(color_html, unsafe_allow_html=True)
260+
cols[1].markdown(f"{qty_missing}")
261+
cols[2].markdown(available_display)
262+
263+
widget_key = short_key("set_found", part_num, color_name, set_key)
264+
new_found = cols[3].number_input(
265+
" ", min_value=0, max_value=max(max_found, 1), value=int(current_found), step=1,
266+
key=widget_key, label_visibility="collapsed"
267+
)
268+
if int(new_found) != int(current_found):
269+
if "set_found_counts" not in st.session_state:
270+
st.session_state["set_found_counts"] = {}
271+
st.session_state["set_found_counts"][found_key] = int(new_found)
272+
273+
complete = new_found >= qty_missing
274+
cols[3].markdown(
275+
f"✅ Found all ({new_found}/{qty_missing})"
276+
if complete
277+
else f"**Found:** {new_found}/{qty_missing}"
278+
)
279+
262280
st.markdown("---")
263281

264282

283+
265284
def render_set_search_section(merged_df: pd.DataFrame, sets_manager, color_lookup: Dict) -> None:
266285
"""
267286
Render set search interface for parts not found or insufficient.
@@ -352,15 +371,19 @@ def render_set_search_section(merged_df: pd.DataFrame, sets_manager, color_looku
352371
col1, col2 = st.columns([1, 1])
353372
with col1:
354373
if st.button(f"Select All", key=f"select_all_{source_name}"):
355-
# Add all sets from this source to selected sets
356374
for set_data in fetched_sets:
357-
st.session_state["selected_sets_for_search"].add(set_data["set_number"])
375+
set_num = set_data["set_number"]
376+
st.session_state["selected_sets_for_search"].add(set_num)
377+
# Sync the checkbox widget state so it doesn't override on rerun
378+
st.session_state[f"set_checkbox_{set_num}"] = True
358379
st.rerun()
359380
with col2:
360381
if st.button(f"Deselect All", key=f"deselect_all_{source_name}"):
361-
# Remove all sets from this source from selected sets
362382
for set_data in fetched_sets:
363-
st.session_state["selected_sets_for_search"].discard(set_data["set_number"])
383+
set_num = set_data["set_number"]
384+
st.session_state["selected_sets_for_search"].discard(set_num)
385+
# Sync the checkbox widget state so it doesn't override on rerun
386+
st.session_state[f"set_checkbox_{set_num}"] = False
364387
st.rerun()
365388

366389
# Display checkboxes for each set
@@ -369,22 +392,22 @@ def render_set_search_section(merged_df: pd.DataFrame, sets_manager, color_looku
369392
set_name = set_data.get("set_name", set_number)
370393
part_count = set_data.get("part_count", 0)
371394

372-
# Check if this set is selected (read from session state)
373-
is_selected = set_number in st.session_state["selected_sets_for_search"]
374395
checkbox_label = f"{set_number} - {set_name} ({part_count} parts)"
375396

376-
# Use on_change callback to update session state
377-
def toggle_set(set_num=set_number):
378-
if set_num in st.session_state["selected_sets_for_search"]:
379-
st.session_state["selected_sets_for_search"].discard(set_num)
380-
else:
397+
# Use on_change callback to sync checkbox state back to selected_sets_for_search
398+
def sync_checkbox_to_set(set_num=set_number):
399+
cb_key = f"set_checkbox_{set_num}"
400+
if st.session_state.get(cb_key, False):
381401
st.session_state["selected_sets_for_search"].add(set_num)
402+
else:
403+
st.session_state["selected_sets_for_search"].discard(set_num)
382404

405+
is_selected = set_number in st.session_state["selected_sets_for_search"]
383406
st.checkbox(
384407
checkbox_label,
385408
value=is_selected,
386409
key=f"set_checkbox_{set_number}",
387-
on_change=toggle_set,
410+
on_change=sync_checkbox_to_set,
388411
args=(set_number,)
389412
)
390413

core/state/progress.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,81 @@
11
# ui/summary.py
22
import streamlit as st
3+
import pandas as pd
34

45

5-
def render_summary_table(merged_df):
6+
def render_summary_table(merged_df, set_search_results=None, set_found_counts=None, color_lookup=None):
7+
"""
8+
Render summary & progress table by location, including set-found counts.
9+
10+
Args:
11+
merged_df: Merged dataframe with Found column already populated
12+
set_search_results: Dict from set search {(part_num, color_name): [locations...]}
13+
set_found_counts: Dict {(part_num, color_name, set_key): found_count}
14+
color_lookup: Dict mapping color_id to color info (with 'name' key)
15+
"""
16+
# Start with the location-based summary from merged_df
617
summary = merged_df.groupby("Location").agg(
718
parts_count=("Part", "count"),
819
found_parts=("Found", "sum"),
920
total_wanted=("Quantity_wanted", "sum")
1021
).reset_index()
22+
23+
# Add set-based rows if we have set search results
24+
if set_search_results and set_found_counts:
25+
# Build reverse color lookup: color_name -> color_id
26+
color_name_to_id = {}
27+
if color_lookup:
28+
for color_id, color_info in color_lookup.items():
29+
color_name = color_info.get("name", "")
30+
if color_name:
31+
color_name_to_id[color_name] = color_id
32+
33+
# Reorganize by set
34+
sets_dict = {}
35+
for (part_num, color_name), locations in set_search_results.items():
36+
for location_info in locations:
37+
set_number = location_info["set_number"]
38+
set_name = location_info["set_name"]
39+
set_key = f"{set_number} - {set_name}"
40+
41+
if set_key not in sets_dict:
42+
sets_dict[set_key] = {"parts_count": 0, "found_parts": 0, "total_wanted": 0}
43+
44+
color_id = color_name_to_id.get(color_name)
45+
# Find qty_missing from merged_df
46+
qty_wanted = 0
47+
qty_have = 0
48+
if color_id is not None:
49+
matching = merged_df[
50+
(merged_df["Part"].astype(str) == str(part_num)) &
51+
(merged_df["Color"].astype(str) == str(color_id))
52+
]
53+
if not matching.empty:
54+
qty_wanted = int(matching.iloc[0].get("Quantity_wanted", 0))
55+
qty_have = int(matching.iloc[0].get("Quantity_have", 0))
56+
57+
qty_missing = max(0, qty_wanted - qty_have)
58+
qty_in_set = min(location_info.get("quantity", 0), qty_missing)
59+
60+
sets_dict[set_key]["parts_count"] += 1
61+
sets_dict[set_key]["total_wanted"] += qty_in_set
62+
63+
found_key = (part_num, color_name, set_key)
64+
found = set_found_counts.get(found_key, 0)
65+
sets_dict[set_key]["found_parts"] += found
66+
67+
# Append set rows
68+
set_rows = []
69+
for set_key, data in sorted(sets_dict.items()):
70+
set_rows.append({
71+
"Location": f"📦 Set {set_key}",
72+
"parts_count": data["parts_count"],
73+
"found_parts": data["found_parts"],
74+
"total_wanted": data["total_wanted"]
75+
})
76+
if set_rows:
77+
summary = pd.concat([summary, pd.DataFrame(set_rows)], ignore_index=True)
78+
1179
summary["completion_%"] = (100 * summary["found_parts"] / summary["total_wanted"]).round(1).fillna(0)
1280

1381
st.markdown("### 📈 Summary & Progress by Location")

pages/4_Find_Wanted_Parts.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
if st.button("💾 Save Progress", width='stretch', type="primary"):
4949
session_data = {
5050
"found_counts": st.session_state.get("found_counts", {}),
51-
"locations_index": st.session_state.get("locations_index", {})
51+
"locations_index": st.session_state.get("locations_index", {}),
52+
"set_found_counts": st.session_state.get("set_found_counts", {})
5253
}
5354
if st.session_state.get("auth_manager"):
5455
st.session_state.auth_manager.save_user_session(username, session_data, paths.user_data_dir)
@@ -63,6 +64,7 @@
6364
if saved_data:
6465
st.session_state["found_counts"] = saved_data.get("found_counts", {})
6566
st.session_state["locations_index"] = saved_data.get("locations_index", {})
67+
st.session_state["set_found_counts"] = saved_data.get("set_found_counts", {})
6668
st.success("Progress loaded!")
6769
st.rerun()
6870
else:
@@ -458,9 +460,6 @@ def _df_bytes(df):
458460
merged["Found"] = [found_map.get(k, 0) for k in keys_tuples]
459461
merged["Complete"] = merged["Found"] >= merged["Quantity_wanted"]
460462

461-
# Render summary table
462-
render_summary_table(merged)
463-
464463
# Download button
465464
csv = merged.to_csv(index=False).encode("utf-8")
466465
st.download_button("💾 Download merged CSV", csv, "lego_wanted_with_location.csv", type="primary")
@@ -472,6 +471,7 @@ def _df_bytes(df):
472471
# --- Set Search Section
473472
# ---------------------------------------------------------------------
474473
# Initialize SetsManager for set search functionality
474+
set_search_results = {}
475475
try:
476476
sets_manager = SetsManager(paths.user_data_dir / username, paths.cache_set_inventories)
477477
render_set_search_section(merged, sets_manager, color_lookup)
@@ -490,3 +490,9 @@ def _df_bytes(df):
490490
# If there's an error initializing sets manager, just skip this section
491491
# This ensures the main functionality continues to work
492492
pass
493+
494+
# ---------------------------------------------------------------------
495+
# --- Summary & Progress (at the end, includes set-found counts)
496+
# ---------------------------------------------------------------------
497+
set_found_counts = st.session_state.get("set_found_counts", {})
498+
render_summary_table(merged, set_search_results, set_found_counts, color_lookup)

0 commit comments

Comments
 (0)