Skip to content

Commit 8572791

Browse files
committed
added alternative workflow to find wanted parts in collection: A. parts, then sets; B. only in sets.
1 parent c954ef7 commit 8572791

File tree

3 files changed

+617
-327
lines changed

3 files changed

+617
-327
lines changed

core/state/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
get_unfound_parts,
1212
merge_set_results,
1313
render_missing_parts_by_set,
14-
render_set_search_section
14+
render_set_search_section,
15+
render_direct_set_search_section
1516
)
1617

1718
__all__ = [
@@ -20,4 +21,5 @@
2021
'merge_set_results',
2122
'render_missing_parts_by_set',
2223
'render_set_search_section',
24+
'render_direct_set_search_section',
2325
]

core/state/find_wanted_parts.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,17 @@ def render_missing_parts_by_set(set_results: Dict, merged_df: pd.DataFrame,
201201
"is_spare": location_info.get("is_spare", False)
202202
})
203203

204+
# Deduplicate parts within each set (same part+color: sum quantities)
205+
for set_key in sets_dict:
206+
seen = {}
207+
for entry in sets_dict[set_key]:
208+
dedup_key = (entry["part_num"], entry["color_name"])
209+
if dedup_key in seen:
210+
seen[dedup_key]["quantity"] += entry["quantity"]
211+
else:
212+
seen[dedup_key] = entry.copy()
213+
sets_dict[set_key] = list(seen.values())
214+
204215
# Display each set with its parts (layout matching location cards)
205216
for set_key, parts_list in sorted(sets_dict.items()):
206217
with st.expander(f"📦 {set_key} ({len(parts_list)} part type(s))", expanded=True):
@@ -714,3 +725,131 @@ def render_missing_parts_export(merged: pd.DataFrame) -> None:
714725

715726
st.download_button("📥 Export Missing Parts (Rebrickable Format)",
716727
export_csv, "missing_parts_rebrickable.csv", type="primary")
728+
729+
730+
def render_direct_set_search_section(wanted_parts: list, sets_manager) -> None:
731+
"""
732+
Render set search interface for Alternative B (search in owned sets only).
733+
734+
Unlike render_set_search_section, this does not depend on a merged_df.
735+
It takes a pre-built list of (part_num, color_name) tuples and searches
736+
directly in owned sets. Uses separate session state keys (*_b) to avoid
737+
conflicts with Alternative A.
738+
739+
Args:
740+
wanted_parts: List of (part_number, color_name) tuples to search for
741+
sets_manager: SetsManager instance for accessing set data
742+
"""
743+
import streamlit as st
744+
745+
if not wanted_parts:
746+
st.info("No wanted parts to search for.")
747+
return
748+
749+
# Load sets metadata
750+
if st.session_state.get("sets_data_loaded", False) and st.session_state.get("sets_metadata") is not None:
751+
all_sets = st.session_state["sets_metadata"]
752+
sets_by_source = {}
753+
for set_data in all_sets:
754+
source = set_data["source_csv"]
755+
if source not in sets_by_source:
756+
sets_by_source[source] = []
757+
sets_by_source[source].append(set_data)
758+
else:
759+
sets_by_source = sets_manager.get_sets_by_source()
760+
761+
if not sets_by_source:
762+
st.info("📭 No sets found. Add sets on the 'My Collection - Sets' page.")
763+
return
764+
765+
total_fetched = sum(
766+
1 for sets_list in sets_by_source.values()
767+
for s in sets_list if s.get("inventory_fetched", False)
768+
)
769+
if total_fetched == 0:
770+
st.info("📭 No set inventories available. Add sets and retrieve inventories on the 'My Collection - Sets' page.")
771+
return
772+
773+
# Set selection interface
774+
st.markdown("#### Select Sets to Search")
775+
st.markdown("Choose which sets to search for the wanted parts:")
776+
777+
if "selected_sets_for_search_b" not in st.session_state:
778+
st.session_state["selected_sets_for_search_b"] = set()
779+
780+
for source_name, sets_list in sorted(sets_by_source.items()):
781+
fetched_sets = [s for s in sets_list if s.get("inventory_fetched", False)]
782+
unfetched_sets = [s for s in sets_list if not s.get("inventory_fetched", False)]
783+
784+
with st.expander(f"📁 {source_name} ({len(fetched_sets)}/{len(sets_list)} set(s) with inventory)", expanded=True):
785+
if fetched_sets:
786+
col1, col2 = st.columns([1, 1])
787+
with col1:
788+
if st.button("Select All", key=f"b_select_all_{source_name}"):
789+
for set_data in fetched_sets:
790+
set_num = set_data["set_number"]
791+
st.session_state["selected_sets_for_search_b"].add(set_num)
792+
st.session_state[f"b_set_checkbox_{set_num}"] = True
793+
st.rerun()
794+
with col2:
795+
if st.button("Deselect All", key=f"b_deselect_all_{source_name}"):
796+
for set_data in fetched_sets:
797+
set_num = set_data["set_number"]
798+
st.session_state["selected_sets_for_search_b"].discard(set_num)
799+
st.session_state[f"b_set_checkbox_{set_num}"] = False
800+
st.rerun()
801+
802+
for set_data in fetched_sets:
803+
set_number = set_data["set_number"]
804+
set_name = set_data.get("set_name", set_number)
805+
part_count = set_data.get("part_count", 0)
806+
807+
checkbox_label = f"{set_number} - {set_name} ({part_count} parts)"
808+
809+
def sync_checkbox_b(set_num=set_number):
810+
cb_key = f"b_set_checkbox_{set_num}"
811+
if st.session_state.get(cb_key, False):
812+
st.session_state["selected_sets_for_search_b"].add(set_num)
813+
else:
814+
st.session_state["selected_sets_for_search_b"].discard(set_num)
815+
816+
is_selected = set_number in st.session_state["selected_sets_for_search_b"]
817+
st.checkbox(
818+
checkbox_label, value=is_selected,
819+
key=f"b_set_checkbox_{set_number}",
820+
on_change=sync_checkbox_b, args=(set_number,)
821+
)
822+
823+
if unfetched_sets:
824+
st.markdown(f"⚠️ {len(unfetched_sets)} set(s) without inventory — fetch on 'My Collection - Sets' page:")
825+
for set_data in unfetched_sets:
826+
set_number = set_data["set_number"]
827+
set_name = set_data.get("set_name", set_number)
828+
st.checkbox(
829+
f"{set_number} - {set_name} (no inventory)",
830+
value=False, disabled=True,
831+
key=f"b_set_checkbox_disabled_{set_number}"
832+
)
833+
834+
# Search button
835+
st.markdown("---")
836+
selected_count = len(st.session_state["selected_sets_for_search_b"])
837+
if selected_count == 0:
838+
st.button("🔍 Search Selected Sets", key="b_search_sets_btn", disabled=True, type="primary")
839+
else:
840+
if st.button(f"🔍 Search Selected Sets ({selected_count})", key="b_search_sets_btn", type="primary"):
841+
with st.spinner(f"Searching {selected_count} set(s)..."):
842+
inventories_cache = st.session_state.get("sets_inventories_cache", {})
843+
selected_sets_list = list(st.session_state["selected_sets_for_search_b"])
844+
set_results = sets_manager.search_parts(
845+
wanted_parts,
846+
selected_sets=selected_sets_list,
847+
inventories_cache=inventories_cache
848+
)
849+
if set_results:
850+
st.session_state["set_search_results_b"] = set_results
851+
st.success(f"✅ Found parts in {len(set_results)} part/color combination(s)!")
852+
st.rerun()
853+
else:
854+
st.session_state["set_search_results_b"] = {}
855+
st.warning("No matching parts found in selected sets.")

0 commit comments

Comments
 (0)