FluxFinance commited on
Commit
b2619a9
Β·
verified Β·
1 Parent(s): 349c212

Upload Advisernote.py

Browse files
Files changed (1) hide show
  1. Advisernote.py +274 -6
Advisernote.py CHANGED
@@ -136,7 +136,9 @@ if 'case_comments' not in st.session_state: st.session_state['case_comments'] =
136
  # =========================================================================
137
  # [λ³΄μ•ˆ λνŒμ™•] 졜초 둜그인 λΉ„λ²ˆ μ„ΈνŒ… & κ΄€λ¦¬μž μ‹€μ‹œκ°„ 동기화/μ‚­μ œ ν†΅μ œ μ‹œμŠ€ν…œ
138
  # =========================================================================
139
- USER_DB_FILE = "user_passwords.db"
 
 
140
 
141
  def init_user_db(seed_users):
142
  conn = sqlite3.connect(USER_DB_FILE)
@@ -391,9 +393,227 @@ def fetch_notion_comments(page_id):
391
  comments_list.append(f"πŸ’¬ [{created_at}] {c_text}")
392
  return "\n".join(comments_list) if comments_list else ""
393
  except:
394
- pass
395
- return ""
396
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  def fetch_client_tasks_from_notion(client_name):
398
  # .streamlit/secrets.toml νŒŒμΌμ—μ„œ λ³΄μ•ˆ 정보λ₯Ό μ•ˆμ „ν•˜κ²Œ λ‘œλ“œν•©λ‹ˆλ‹€.
399
  notion_token = NOTION_TOKEN
@@ -1317,7 +1537,7 @@ if check_password():
1317
  st.session_state['active_mode'] = "Notion"
1318
 
1319
  # πŸ’‘ [νƒ­ μˆœμ„œ ν˜μ‹ ] λ…Έμ…˜ 탭을 λ°°μ—΄μ˜ 맨 μ•žμœΌλ‘œ λ°°μΉ˜ν•˜μ—¬ μ›Ή 접속 μ‹œ 첫 화면에 무쑰건 λ…Έμ…˜ μž…λ ₯창이 λ¨Όμ € λœ¨λ„λ‘ μ œμ–΄ν•©λ‹ˆλ‹€.
1320
- tab_notion, tab_email = st.tabs(["πŸ“ Pull Notion", "βœ‰οΈ Scan Emails"])
1321
 
1322
  # TAB 1: λ…Έμ…˜ 검색창
1323
  with tab_notion:
@@ -1359,7 +1579,55 @@ if check_password():
1359
  </div>
1360
  """, unsafe_allow_html=True)
1361
  # TAB 2: 이메일 검색창
1362
- with tab_email:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1363
  with st.container(border=True):
1364
 
1365
  st.session_state['folder_path'] = st.text_input("πŸ“‚ G: Drive Folder Path", value=st.session_state['folder_path'], key="fixed_mail_path")
 
136
  # =========================================================================
137
  # [λ³΄μ•ˆ λνŒμ™•] 졜초 둜그인 λΉ„λ²ˆ μ„ΈνŒ… & κ΄€λ¦¬μž μ‹€μ‹œκ°„ 동기화/μ‚­μ œ ν†΅μ œ μ‹œμŠ€ν…œ
138
  # =========================================================================
139
+ PERSISTENT_DATA_DIR = os.environ.get("ADVISERNOTE_DATA_DIR", "/data")
140
+ USING_PERSISTENT_STORAGE = os.path.isdir(PERSISTENT_DATA_DIR) and os.access(PERSISTENT_DATA_DIR, os.W_OK)
141
+ USER_DB_FILE = os.path.join(PERSISTENT_DATA_DIR, "user_passwords.db") if USING_PERSISTENT_STORAGE else "user_passwords.db"
142
 
143
  def init_user_db(seed_users):
144
  conn = sqlite3.connect(USER_DB_FILE)
 
393
  comments_list.append(f"πŸ’¬ [{created_at}] {c_text}")
394
  return "\n".join(comments_list) if comments_list else ""
395
  except:
396
+ pass
397
+ return ""
398
+
399
+ def notion_api_url(endpoint_path):
400
+ return "https://api.notion.com/" + endpoint_path.lstrip("/")
401
+
402
+ def extract_notion_plain_text(prop):
403
+ if not isinstance(prop, dict):
404
+ return ""
405
+
406
+ prop_type = prop.get("type")
407
+ if prop_type in ("title", "rich_text"):
408
+ parts = prop.get(prop_type, [])
409
+ return "".join(item.get("plain_text", "") for item in parts if isinstance(item, dict)).strip()
410
+ if prop_type == "phone_number":
411
+ return str(prop.get("phone_number") or "").strip()
412
+ if prop_type == "email":
413
+ return str(prop.get("email") or "").strip()
414
+ if prop_type == "select" and prop.get("select"):
415
+ return str(prop["select"].get("name", "")).strip()
416
+ if prop_type == "multi_select":
417
+ return ", ".join(item.get("name", "") for item in prop.get("multi_select", []) if item.get("name")).strip()
418
+ if prop_type == "people":
419
+ return ", ".join(item.get("name", "") for item in prop.get("people", []) if item.get("name")).strip()
420
+ if prop_type == "number":
421
+ value = prop.get("number")
422
+ return "" if value is None else str(value).strip()
423
+ return ""
424
+
425
+ def find_notion_property_value(props, preferred_keys, fallback_types=None):
426
+ normalized_lookup = {str(key).strip().lower(): value for key, value in props.items()}
427
+ for key in preferred_keys:
428
+ key_l = key.lower()
429
+ for prop_name, prop in normalized_lookup.items():
430
+ if key_l in prop_name:
431
+ value = extract_notion_plain_text(prop)
432
+ if value:
433
+ return value
434
+
435
+ if fallback_types:
436
+ for prop in props.values():
437
+ if isinstance(prop, dict) and prop.get("type") in fallback_types:
438
+ value = extract_notion_plain_text(prop)
439
+ if value:
440
+ return value
441
+ return ""
442
+
443
+ def fetch_client_contacts_from_notion(search_text=""):
444
+ notion_token = NOTION_TOKEN
445
+ target_data_source_id = NOTION_DB_ID
446
+
447
+ if not notion_token:
448
+ return [], "Missing NOTION_API_TOKEN in secrets.toml"
449
+ if not target_data_source_id:
450
+ return [], "Missing NOTION_DATABASE_ID in secrets.toml"
451
+
452
+ headers = {
453
+ "Authorization": f"Bearer {notion_token.strip()}",
454
+ "Content-Type": "application/json",
455
+ "Notion-Version": "2025-09-03"
456
+ }
457
+
458
+ name_keys = ["case name", "client name", "applicant", "customer", "borrower", "name", "고객", "이름"]
459
+ nickname_keys = ["nickname", "nick name", "preferred name", "alias", "short name", "λ‹‰λ„€μž„", "별λͺ…"]
460
+ phone_keys = ["phone", "mobile", "telephone", "contact number", "cell", "μ „ν™”", "연락", "νœ΄λŒ€"]
461
+ query_url = notion_api_url(f"v1/data_sources/{target_data_source_id.strip()}/query/")
462
+ contacts = []
463
+ start_cursor = None
464
+
465
+ try:
466
+ while True:
467
+ payload = {"page_size": 100}
468
+ if start_cursor:
469
+ payload["start_cursor"] = start_cursor
470
+
471
+ response = requests.post(query_url, json=payload, headers=headers, proxies={"http": None, "https": None}, timeout=20)
472
+ if response.status_code != 200:
473
+ return [], f"Notion API Failed: {response.status_code} - {response.text[:200]}"
474
+
475
+ data = response.json()
476
+ for item in data.get("results", []):
477
+ props = item.get("properties", {})
478
+ client_name = find_notion_property_value(props, name_keys, fallback_types={"title"})
479
+ nickname = find_notion_property_value(props, nickname_keys)
480
+ phone_number = find_notion_property_value(props, phone_keys, fallback_types={"phone_number"})
481
+
482
+ if not client_name and not nickname and not phone_number:
483
+ continue
484
+
485
+ searchable = " ".join([client_name, nickname, phone_number]).lower()
486
+ if search_text and search_text.strip().lower() not in searchable:
487
+ continue
488
+
489
+ contacts.append({
490
+ "Client Name": client_name,
491
+ "Nickname": nickname,
492
+ "Phone": phone_number
493
+ })
494
+
495
+ if not data.get("has_more"):
496
+ break
497
+ start_cursor = data.get("next_cursor")
498
+
499
+ except Exception as exc:
500
+ return [], f"Notion contact pull failed: {exc}"
501
+
502
+ contacts = sorted(contacts, key=lambda row: row.get("Client Name", "").lower())
503
+ return contacts, "Success"
504
+
505
+ NOTION_CONTACT_FIELD_CONFIG = {
506
+ "Client Name": {
507
+ "keys": ["case name", "client name", "applicant", "customer", "borrower", "name", "고객", "이름"],
508
+ "fallback_types": {"title"},
509
+ },
510
+ "Nickname": {
511
+ "keys": ["nickname", "nick name", "preferred name", "alias", "short name", "λ‹‰λ„€μž„", "별λͺ…"],
512
+ "fallback_types": set(),
513
+ },
514
+ "Phone": {
515
+ "keys": ["phone", "mobile", "telephone", "contact number", "cell", "μ „ν™”", "연락", "νœ΄λŒ€"],
516
+ "fallback_types": {"phone_number"},
517
+ },
518
+ "Email": {
519
+ "keys": ["email", "e-mail", "mail", "이메일"],
520
+ "fallback_types": {"email"},
521
+ },
522
+ "Status": {
523
+ "keys": ["status", "stage", "progress", "μƒνƒœ", "μ§„ν–‰"],
524
+ "fallback_types": set(),
525
+ },
526
+ "Adviser": {
527
+ "keys": ["adviser", "advisor", "broker", "owner", "λ‹΄λ‹Ή", "μ–΄λ“œλ°”μ΄μ €"],
528
+ "fallback_types": set(),
529
+ },
530
+ "Case Number": {
531
+ "keys": ["case number", "case no", "ref", "reference", "application number", "번호"],
532
+ "fallback_types": set(),
533
+ },
534
+ }
535
+
536
+ def find_notion_property_by_exact_or_contains(props, property_name):
537
+ target = str(property_name or "").strip().lower()
538
+ if not target:
539
+ return ""
540
+
541
+ for key, prop in props.items():
542
+ if str(key).strip().lower() == target:
543
+ return extract_notion_plain_text(prop)
544
+
545
+ for key, prop in props.items():
546
+ if target in str(key).strip().lower():
547
+ return extract_notion_plain_text(prop)
548
+ return ""
549
+
550
+ def fetch_client_contacts_from_notion(search_text="", selected_fields=None, custom_fields=None):
551
+ notion_token = NOTION_TOKEN
552
+ target_data_source_id = NOTION_DB_ID
553
+
554
+ if not notion_token:
555
+ return [], "Missing NOTION_API_TOKEN in secrets.toml"
556
+ if not target_data_source_id:
557
+ return [], "Missing NOTION_DATABASE_ID in secrets.toml"
558
+
559
+ headers = {
560
+ "Authorization": f"Bearer {notion_token.strip()}",
561
+ "Content-Type": "application/json",
562
+ "Notion-Version": "2025-09-03"
563
+ }
564
+
565
+ selected_fields = selected_fields or ["Client Name", "Nickname", "Phone"]
566
+ custom_fields = [field.strip() for field in (custom_fields or []) if field.strip()]
567
+ output_fields = list(dict.fromkeys(selected_fields + custom_fields))
568
+ query_url = notion_api_url(f"v1/data_sources/{target_data_source_id.strip()}/query/")
569
+ contacts = []
570
+ start_cursor = None
571
+
572
+ try:
573
+ while True:
574
+ payload = {"page_size": 100}
575
+ if start_cursor:
576
+ payload["start_cursor"] = start_cursor
577
+
578
+ response = requests.post(query_url, json=payload, headers=headers, proxies={"http": None, "https": None}, timeout=20)
579
+ if response.status_code != 200:
580
+ return [], f"Notion API Failed: {response.status_code} - {response.text[:200]}"
581
+
582
+ data = response.json()
583
+ for item in data.get("results", []):
584
+ props = item.get("properties", {})
585
+ row = {}
586
+ for field_name in output_fields:
587
+ config = NOTION_CONTACT_FIELD_CONFIG.get(field_name)
588
+ if config:
589
+ row[field_name] = find_notion_property_value(
590
+ props,
591
+ config["keys"],
592
+ fallback_types=config["fallback_types"],
593
+ )
594
+ else:
595
+ row[field_name] = find_notion_property_by_exact_or_contains(props, field_name)
596
+
597
+ if not any(str(value).strip() for value in row.values()):
598
+ continue
599
+
600
+ searchable = " ".join(str(value) for value in row.values()).lower()
601
+ if search_text and search_text.strip().lower() not in searchable:
602
+ continue
603
+
604
+ contacts.append(row)
605
+
606
+ if not data.get("has_more"):
607
+ break
608
+ start_cursor = data.get("next_cursor")
609
+
610
+ except Exception as exc:
611
+ return [], f"Notion contact pull failed: {exc}"
612
+
613
+ sort_field = "Client Name" if "Client Name" in output_fields else output_fields[0]
614
+ contacts = sorted(contacts, key=lambda row: str(row.get(sort_field, "")).lower())
615
+ return contacts, "Success"
616
+
617
  def fetch_client_tasks_from_notion(client_name):
618
  # .streamlit/secrets.toml νŒŒμΌμ—μ„œ λ³΄μ•ˆ 정보λ₯Ό μ•ˆμ „ν•˜κ²Œ λ‘œλ“œν•©λ‹ˆλ‹€.
619
  notion_token = NOTION_TOKEN
 
1537
  st.session_state['active_mode'] = "Notion"
1538
 
1539
  # πŸ’‘ [νƒ­ μˆœμ„œ ν˜μ‹ ] λ…Έμ…˜ 탭을 λ°°μ—΄μ˜ 맨 μ•žμœΌλ‘œ λ°°μΉ˜ν•˜μ—¬ μ›Ή 접속 μ‹œ 첫 화면에 무쑰건 λ…Έμ…˜ μž…λ ₯창이 λ¨Όμ € λœ¨λ„λ‘ μ œμ–΄ν•©λ‹ˆλ‹€.
1540
+ tab_notion, tab_contacts, tab_email = st.tabs(["πŸ“ Pull Notion", "πŸ‘₯ Notion Contacts", "βœ‰οΈ Scan Emails"])
1541
 
1542
  # TAB 1: λ…Έμ…˜ 검색창
1543
  with tab_notion:
 
1579
  </div>
1580
  """, unsafe_allow_html=True)
1581
  # TAB 2: 이메일 검색창
1582
+ with tab_contacts:
1583
+ with st.container(border=True):
1584
+ st.caption("Pull customer name, nickname, and phone number from Notion.")
1585
+ contact_col1, contact_col2 = st.columns([5, 1])
1586
+ contact_search = contact_col1.text_input(
1587
+ "Search customer / nickname / phone",
1588
+ key="notion_contact_search",
1589
+ placeholder="Leave blank to pull all contacts"
1590
+ )
1591
+ contact_pull_btn = contact_col2.button("Pull Contacts", use_container_width=True, key="notion_contact_pull")
1592
+
1593
+ selected_contact_fields = st.multiselect(
1594
+ "Fields to pull",
1595
+ options=list(NOTION_CONTACT_FIELD_CONFIG.keys()),
1596
+ default=["Client Name", "Nickname", "Phone"],
1597
+ key="notion_contact_fields"
1598
+ )
1599
+ custom_contact_fields_raw = st.text_input(
1600
+ "Additional Notion property names",
1601
+ key="notion_contact_custom_fields",
1602
+ placeholder="Example: DOB; Address; Loan Amount"
1603
+ )
1604
+ custom_contact_fields = [
1605
+ field.strip()
1606
+ for field in custom_contact_fields_raw.replace(",", ";").split(";")
1607
+ if field.strip()
1608
+ ]
1609
+
1610
+ if contact_pull_btn:
1611
+ with st.status("Pulling Notion contacts...", expanded=True) as status:
1612
+ contact_rows, contact_msg = fetch_client_contacts_from_notion(
1613
+ contact_search,
1614
+ selected_fields=selected_contact_fields,
1615
+ custom_fields=custom_contact_fields,
1616
+ )
1617
+ if contact_rows:
1618
+ st.session_state["notion_contacts"] = pd.DataFrame(contact_rows)
1619
+ status.update(label=f"Loaded {len(contact_rows)} contacts.", state="complete")
1620
+ elif contact_msg == "Success":
1621
+ st.session_state["notion_contacts"] = pd.DataFrame(columns=selected_contact_fields + custom_contact_fields)
1622
+ status.update(label="No contacts found.", state="complete")
1623
+ else:
1624
+ status.update(label=f"Failed: {contact_msg}", state="error")
1625
+
1626
+ contacts_df = st.session_state.get("notion_contacts")
1627
+ if isinstance(contacts_df, pd.DataFrame):
1628
+ st.dataframe(contacts_df, use_container_width=True, hide_index=True)
1629
+
1630
+ with tab_email:
1631
  with st.container(border=True):
1632
 
1633
  st.session_state['folder_path'] = st.text_input("πŸ“‚ G: Drive Folder Path", value=st.session_state['folder_path'], key="fixed_mail_path")