import gradio as gr import pandas as pd import uuid from datetime import datetime, date # ========================= # In-memory "DB" # ========================= STATE = { "jobs": [], # {job_id, title, area, budget_min, budget_max, deadline_raw, details, created_at, status, approved_bid_id} "bids": [] # {bid_id, job_id, contractor, price, duration_days, note, created_at, status} } # ========================= # Prefecture -> City master (prototype) # ========================= PREF_CITIES = { "静岡県": ["静岡市", "浜松市", "藤枝市", "焼津市", "沼津市", "富士市", "三島市"], "東京都": ["千代田区", "港区", "新宿区", "渋谷区", "世田谷区", "台東区"], "神奈川県": ["横浜市", "川崎市", "相模原市", "藤沢市", "鎌倉市"], "愛知県": ["名古屋市", "豊田市", "岡崎市", "一宮市"], "大阪府": ["大阪市", "堺市", "吹田市", "豊中市"], } # ========================= # CSS (Card UI) # ========================= CARD_CSS = """ /* ====== カードRadioのグリッド化 ====== */ #job_cards .wrap, #union_job_cards .wrap, #union_bid_cards .wrap { display: grid !important; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 12px; } /* label をカード化(常に文字色を濃色に固定) */ #job_cards label, #union_job_cards label, #union_bid_cards label { border: 1px solid #e5e7eb !important; border-radius: 16px !important; padding: 14px !important; background: #ffffff !important; cursor: pointer !important; line-height: 1.45 !important; white-space: pre-wrap !important; transition: all .12s ease; color: #111827 !important; } /* hover */ #job_cards label:hover, #union_job_cards label:hover, #union_bid_cards label:hover { border-color: #6366f1 !important; box-shadow: 0 6px 18px rgba(17, 24, 39, 0.08); } /* 選択時:背景 + 枠 + 文字色 */ #job_cards input:checked + label, #union_job_cards input:checked + label, #union_bid_cards input:checked + label { border-color: #4f46e5 !important; background: #eef2ff !important; color: #111827 !important; } /* 内部要素も濃色固定 */ #job_cards label span, #job_cards label div, #job_cards label p, #union_job_cards label span, #union_job_cards label div, #union_job_cards label p, #union_bid_cards label span, #union_bid_cards label div, #union_bid_cards label p { color: #111827 !important; } """ # ========================= # Utilities # ========================= def now_str(): return datetime.now().strftime("%Y-%m-%d %H:%M:%S") def yen(x): try: return f"¥{int(x):,}" except: return "-" def validate_yyyymmdd(s: str): if not isinstance(s, str): return None s = s.strip() if len(s) != 8 or not s.isdigit(): return None try: datetime.strptime(s, "%Y%m%d") return s except ValueError: return None def format_deadline(deadline_raw: str, mode: str): if not deadline_raw: return "" if mode == "YYYYMMDD": return deadline_raw try: d = datetime.strptime(deadline_raw, "%Y%m%d").date() return d.strftime("%Y-%m-%d") except Exception: return deadline_raw def days_remaining(deadline_raw: str): try: d = datetime.strptime(deadline_raw, "%Y%m%d").date() today = date.today() return (d - today).days except Exception: return None def remaining_label(deadline_raw: str): r = days_remaining(deadline_raw) if r is None: return "残り: -" if r >= 0: return f"残り: {r}日" return f"期限超過: {-r}日" def status_badge(status: str): # 見た目はここで統一(HTMLとして描画するので崩れない) if status == "OPEN": return ('募集中', '#eef2ff', '#111827') if status == "MATCHED": return ('承認済み', '#ecfeff', '#0f172a') return ('クローズ', '#f3f4f6', '#111827') # ========================= # View builders # ========================= def summarize(): jobs = STATE["jobs"] bids = STATE["bids"] open_jobs = [j for j in jobs if j["status"] == "OPEN"] matched_jobs = [j for j in jobs if j["status"] == "MATCHED"] closed_jobs = [j for j in jobs if j["status"] == "CLOSED"] total_bids = len(bids) avg_bids_per_job = round(total_bids / len(jobs), 2) if jobs else 0.0 return f""" # ギルドビルド(プロトタイプ) **目的:** 建築案件の「依頼 → 入札 → 組合管理 → 成約」を透明化し、適正価格・適正期間で成立させる。 --- ## いま起きていること(リアルタイム) - 案件数:**{len(jobs)}** - 募集中(OPEN):**{len(open_jobs)}** - 承認済み(MATCHED):**{len(matched_jobs)}** - クローズ(CLOSED):**{len(closed_jobs)}** - 入札数:**{total_bids}** - 平均入札数/案件:**{avg_bids_per_job}** --- ## 体験の流れ(このアプリでできること) 1. **施主**:案件を投稿(予算・希望納期・内容) 2. **職人**:案件に入札(価格・工期・コメント) 3. **組合**:入札状況を可視化し、承認してマッチング(透明なルール運用) """ def jobs_table(date_mode: str): rows = [] for j in STATE["jobs"]: rows.append({ "job_id": j["job_id"], "タイトル": j["title"], "エリア": j["area"], "予算": f"{yen(j['budget_min'])} 〜 {yen(j['budget_max'])}", "希望納期": format_deadline(j["deadline_raw"], date_mode), "残り": remaining_label(j["deadline_raw"]), "状態": j["status"], "作成": j["created_at"], "採択bid_id": j["approved_bid_id"] or "" }) return pd.DataFrame(rows) def bids_table(job_id=None): rows = [] for b in STATE["bids"]: if job_id and b["job_id"] != job_id: continue rows.append({ "bid_id": b["bid_id"], "job_id": b["job_id"], "職人名": b["contractor"], "提示価格": yen(b["price"]), "工期(日)": b["duration_days"], "コメント": b["note"], "状態": b["status"], "作成": b["created_at"], }) return pd.DataFrame(rows) # ========================= # Cards # ========================= def open_job_cards(date_mode: str): items = [] for j in STATE["jobs"]: if j["status"] != "OPEN": continue dl = format_deadline(j["deadline_raw"], date_mode) rem_days = days_remaining(j["deadline_raw"]) rem = remaining_label(j["deadline_raw"]) # ★カードはシンプル:ID/概要は出さない label = ( f"{j['title']}\n" f"・エリア:{j['area']}\n" f"・予算:{yen(j['budget_min'])}〜{yen(j['budget_max'])}\n" f"・納期:{dl}({rem})" ) sort_key = rem_days if rem_days is not None else 10**9 items.append((sort_key, (label, j["job_id"]))) items.sort(key=lambda x: x[0]) return [x[1] for x in items] def union_job_cards(date_mode: str): items = [] for j in STATE["jobs"]: dl = format_deadline(j["deadline_raw"], date_mode) rem_days = days_remaining(j["deadline_raw"]) rem = remaining_label(j["deadline_raw"]) label = ( f"{j['title']}\n" f"・状態:{j['status']}\n" f"・エリア:{j['area']}\n" f"・予算:{yen(j['budget_min'])}〜{yen(j['budget_max'])}\n" f"・納期:{dl}({rem})" ) sort_key = rem_days if rem_days is not None else 10**9 items.append((sort_key, (label, j["job_id"]))) items.sort(key=lambda x: x[0]) return [x[1] for x in items] def union_bid_cards(job_id: str): """ 組合:選択案件に紐づく入札をカード化 """ if not job_id: return [] related = [b for b in STATE["bids"] if b["job_id"] == job_id] # PENDING → ACCEPTED/REJECTED の順が見やすい rank = {"PENDING": 0, "ACCEPTED": 1, "REJECTED": 2} related.sort(key=lambda b: (rank.get(b["status"], 9), int(b.get("price", 10**18)))) items = [] for b in related: st = b.get("status", "") label = ( f"{b['contractor']}\n" f"・提示価格:{yen(b['price'])}\n" f"・工期:{b['duration_days']}日\n" f"・状態:{st}" ) items.append((label, b["bid_id"])) return items def job_detail_html(job_id: str, date_mode: str): j = next((x for x in STATE["jobs"] if x["job_id"] == job_id), None) if not j: return "
(案件が見つかりません)
" details = (j.get("details") or "").strip() or "(詳細なし)" dl = format_deadline(j["deadline_raw"], date_mode) rem = remaining_label(j["deadline_raw"]) badge_text, badge_bg, badge_fg = status_badge(j.get("status", "")) return f"""
{j['title']}
{badge_text}
エリア:{j['area']}
予算:{yen(j['budget_min'])}〜{yen(j['budget_max'])}
納期:{dl}
{rem}
詳細
{details}
""" def bid_detail_html(bid_id: str): b = next((x for x in STATE["bids"] if x["bid_id"] == bid_id), None) if not b: return "
(入札が見つかりません)
" note = (b.get("note") or "").strip() or "(コメントなし)" st = b.get("status", "") # 状態バッジ if st == "PENDING": bt, bg = "審査待ち", "#eef2ff" elif st == "ACCEPTED": bt, bg = "採択", "#ecfeff" else: bt, bg = "不採択", "#f3f4f6" return f"""
{b['contractor']}
{bt}
提示価格:{yen(b['price'])}
工期:{b['duration_days']}日
コメント
{note}
""" # ========================= # Dropdown choices builders # ========================= def list_open_jobs_for_bid_choices(date_mode: str): opts = [] for j in STATE["jobs"]: if j["status"] == "OPEN": dl = format_deadline(j["deadline_raw"], date_mode) label = f"{j['job_id']}|{j['title']}|{j['area']}|予算:{yen(j['budget_min'])}〜{yen(j['budget_max'])}|納期:{dl}" opts.append((label, j["job_id"])) return opts def union_job_choices(date_mode: str): opts = [] for j in STATE["jobs"]: dl = format_deadline(j["deadline_raw"], date_mode) opts.append((f"{j['job_id']}|{j['title']}|{j['status']}|納期:{dl}", j["job_id"])) return opts def union_bid_choices_for_job(job_id): if not job_id: return [] related = [b for b in STATE["bids"] if b["job_id"] == job_id] opts = [] for b in related: label = f"{b['bid_id']}|{b['contractor']}|{yen(b['price'])}|{b['duration_days']}日|{b['status']}" opts.append((label, b["bid_id"])) return opts # ========================= # Actions # ========================= def update_city_choices(selected_pref): cities = PREF_CITIES.get(selected_pref, []) return gr.update(choices=cities, value=(cities[0] if cities else None)) def create_job(title, pref, city, budget_min, budget_max, deadline_raw_input, details, date_mode): deadline_raw = validate_yyyymmdd(deadline_raw_input) if not title or not pref or not city or deadline_raw is None: return ( gr.Warning("タイトル・都道府県・市区町村・希望納期(YYYYMMDD 8桁)は必須です。"), summarize(), jobs_table(date_mode), gr.update(choices=list_open_jobs_for_bid_choices(date_mode), value=None), gr.update(choices=open_job_cards(date_mode), value=None), "
(案件を選択してください)
", gr.update(choices=union_job_choices(date_mode), value=None), gr.update(choices=union_job_cards(date_mode), value=None), gr.update(choices=[], value=None), # union_bid_cards gr.update(choices=[], value=None), # union_bid dropdown "
(案件カードを選択してください)
", "
(入札カードを選択してください)
", ) job_id = "JOB-" + uuid.uuid4().hex[:8].upper() area = f"{pref}{city}" STATE["jobs"].append({ "job_id": job_id, "title": title.strip(), "area": area, "budget_min": int(budget_min) if budget_min else 0, "budget_max": int(budget_max) if budget_max else 0, "deadline_raw": deadline_raw, "details": (details or "").strip(), "created_at": now_str(), "status": "OPEN", "approved_bid_id": None }) return ( gr.Info(f"案件を作成しました:{job_id}"), summarize(), jobs_table(date_mode), gr.update(choices=list_open_jobs_for_bid_choices(date_mode), value=None), gr.update(choices=open_job_cards(date_mode), value=None), "
(案件カードを選択すると詳細が表示されます)
", gr.update(choices=union_job_choices(date_mode), value=None), gr.update(choices=union_job_cards(date_mode), value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), "
(案件カードを選択してください)
", "
(入札カードを選択してください)
", ) def refresh_contractor_view(date_mode): return ( gr.update(choices=list_open_jobs_for_bid_choices(date_mode), value=None), gr.update(choices=open_job_cards(date_mode), value=None), "
(案件カードを選択すると詳細が表示されます)
" ) def on_contractor_card_select(job_id, date_mode): if not job_id: return ( gr.update(value=None), "
(案件を選択してください)
", bids_table() ) return ( gr.update(value=job_id), job_detail_html(job_id, date_mode), bids_table(job_id) ) def place_bid(job_id, contractor, price, duration_days, note): if not job_id: return gr.Warning("入札先の案件を選択してください。"), bids_table() if not contractor or price is None or duration_days is None: return gr.Warning("職人名・価格・工期は必須です。"), bids_table(job_id) job = next((j for j in STATE["jobs"] if j["job_id"] == job_id), None) if not job: return gr.Warning("案件が見つかりません。"), bids_table() if job["status"] != "OPEN": return gr.Warning("この案件は募集中ではありません。"), bids_table(job_id) bid_id = "BID-" + uuid.uuid4().hex[:8].upper() STATE["bids"].append({ "bid_id": bid_id, "job_id": job_id, "contractor": contractor.strip(), "price": int(price), "duration_days": int(duration_days), "note": (note or "").strip(), "created_at": now_str(), "status": "PENDING" }) return gr.Info(f"入札を登録しました:{bid_id}"), bids_table(job_id) def union_refresh(selected_job_id, date_mode): jt = jobs_table(date_mode) bt = bids_table(selected_job_id) if selected_job_id else bids_table() # カード類も更新 job_cards = gr.update(choices=union_job_cards(date_mode), value=selected_job_id or None) bid_cards = gr.update(choices=union_bid_cards(selected_job_id), value=None) bid_dd = gr.update(choices=union_bid_choices_for_job(selected_job_id), value=None) detail_job = job_detail_html(selected_job_id, date_mode) if selected_job_id else "
(案件カードを選択してください)
" detail_bid = "
(入札カードを選択してください)
" return ( summarize(), jt, bt, gr.update(choices=union_job_choices(date_mode), value=selected_job_id or None), job_cards, bid_cards, bid_dd, detail_job, detail_bid ) def on_union_job_card_select(job_id, date_mode): if not job_id: return ( gr.update(value=None), bids_table(), gr.update(choices=[], value=None), # bid cards gr.update(choices=[], value=None), # bid dropdown "
(案件カードを選択してください)
", "
(入札カードを選択してください)
", ) return ( gr.update(value=job_id), bids_table(job_id), gr.update(choices=union_bid_cards(job_id), value=None), gr.update(choices=union_bid_choices_for_job(job_id), value=None), job_detail_html(job_id, date_mode), "
(入札カードを選択してください)
", ) def on_union_bid_card_select(bid_id): if not bid_id: return ( gr.update(value=None), "
(入札カードを選択してください)
" ) return ( gr.update(value=bid_id), bid_detail_html(bid_id) ) def approve_bid(job_id, bid_id, date_mode): if not job_id or not bid_id: return ( gr.Warning("案件と入札を選択してください。"), summarize(), jobs_table(date_mode), bids_table(job_id), gr.update(choices=union_job_choices(date_mode), value=job_id or None), gr.update(choices=union_job_cards(date_mode), value=job_id or None), gr.update(choices=union_bid_cards(job_id), value=None), gr.update(choices=union_bid_choices_for_job(job_id), value=None), job_detail_html(job_id, date_mode) if job_id else "
(案件カードを選択してください)
", "
(入札カードを選択してください)
", ) job = next((j for j in STATE["jobs"] if j["job_id"] == job_id), None) bid = next((b for b in STATE["bids"] if b["bid_id"] == bid_id), None) if not job or not bid or bid["job_id"] != job_id: return ( gr.Warning("対応する案件/入札が見つかりません。"), summarize(), jobs_table(date_mode), bids_table(job_id), gr.update(choices=union_job_choices(date_mode), value=job_id or None), gr.update(choices=union_job_cards(date_mode), value=job_id or None), gr.update(choices=union_bid_cards(job_id), value=None), gr.update(choices=union_bid_choices_for_job(job_id), value=None), job_detail_html(job_id, date_mode), "
(入札カードを選択してください)
", ) if job["status"] != "OPEN": return ( gr.Warning("この案件は募集中ではありません。"), summarize(), jobs_table(date_mode), bids_table(job_id), gr.update(choices=union_job_choices(date_mode), value=job_id or None), gr.update(choices=union_job_cards(date_mode), value=job_id or None), gr.update(choices=union_bid_cards(job_id), value=None), gr.update(choices=union_bid_choices_for_job(job_id), value=None), job_detail_html(job_id, date_mode), "
(入札カードを選択してください)
", ) job["status"] = "MATCHED" job["approved_bid_id"] = bid_id for b in STATE["bids"]: if b["job_id"] == job_id: b["status"] = "REJECTED" bid["status"] = "ACCEPTED" return ( gr.Info(f"承認しました:{job_id} → {bid_id}"), summarize(), jobs_table(date_mode), bids_table(job_id), gr.update(choices=union_job_choices(date_mode), value=job_id), gr.update(choices=union_job_cards(date_mode), value=job_id), gr.update(choices=union_bid_cards(job_id), value=None), gr.update(choices=union_bid_choices_for_job(job_id), value=None), job_detail_html(job_id, date_mode), "
(入札カードを選択してください)
", ) def close_job(job_id, date_mode): if not job_id: return ( gr.Warning("案件を選択してください。"), summarize(), jobs_table(date_mode), gr.update(choices=union_job_choices(date_mode), value=None), gr.update(choices=union_job_cards(date_mode), value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), "
(案件カードを選択してください)
", "
(入札カードを選択してください)
", ) job = next((j for j in STATE["jobs"] if j["job_id"] == job_id), None) if not job: return ( gr.Warning("案件が見つかりません。"), summarize(), jobs_table(date_mode), gr.update(choices=union_job_choices(date_mode), value=None), gr.update(choices=union_job_cards(date_mode), value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), "
(案件カードを選択してください)
", "
(入札カードを選択してください)
", ) job["status"] = "CLOSED" return ( gr.Info(f"案件をクローズしました:{job_id}"), summarize(), jobs_table(date_mode), gr.update(choices=union_job_choices(date_mode), value=job_id), gr.update(choices=union_job_cards(date_mode), value=job_id), gr.update(choices=union_bid_cards(job_id), value=None), gr.update(choices=union_bid_choices_for_job(job_id), value=None), job_detail_html(job_id, date_mode), "
(入札カードを選択してください)
", ) def on_date_mode_change(date_mode, contractor_selected_job, union_selected_job): contractor_card = job_detail_html(contractor_selected_job, date_mode) if contractor_selected_job else \ "
(案件カードを選択すると詳細が表示されます)
" union_card = job_detail_html(union_selected_job, date_mode) if union_selected_job else \ "
(案件カードを選択してください)
" return ( jobs_table(date_mode), gr.update(choices=list_open_jobs_for_bid_choices(date_mode), value=contractor_selected_job or None), gr.update(choices=open_job_cards(date_mode), value=contractor_selected_job or None), contractor_card, jobs_table(date_mode), gr.update(choices=union_job_choices(date_mode), value=union_selected_job or None), gr.update(choices=union_job_cards(date_mode), value=union_selected_job or None), gr.update(choices=union_bid_cards(union_selected_job), value=None), gr.update(choices=union_bid_choices_for_job(union_selected_job), value=None), union_card, "
(入札カードを選択してください)
", ) def reset_all(date_mode): STATE["jobs"].clear() STATE["bids"].clear() return ( gr.Info("全データをリセットしました。"), summarize(), jobs_table(date_mode), bids_table(), gr.update(choices=[], value=None), gr.update(choices=[], value=None), "
(案件カードを選択すると詳細が表示されます)
", gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), gr.update(choices=[], value=None), jobs_table(date_mode), bids_table(), "
(案件カードを選択してください)
", "
(入札カードを選択してください)
", ) # ========================= # UI # ========================= with gr.Blocks(title="ギルドビルド|プロトタイプ", theme=gr.themes.Soft(), css=CARD_CSS) as demo: header = gr.Markdown(summarize()) with gr.Row(): date_mode = gr.Radio( label="日付表示", choices=["YYYYMMDD", "YYYY-MM-DD"], value="YYYY-MM-DD" ) reset_btn = gr.Button("全データをリセット", variant="stop") with gr.Tabs(): # ------------------------- # Owner (施主) # ------------------------- with gr.Tab("施主:案件投稿"): gr.Markdown( "## 案件を投稿する\n" "希望納期は **YYYYMMDD(例:20260304)** の8桁数字で入力してください。" ) with gr.Row(): title = gr.Textbox(label="タイトル(例:外壁塗装)", placeholder="外壁塗装/屋根塗装など") pref = gr.Dropdown(label="都道府県", choices=list(PREF_CITIES.keys()), value="静岡県") city = gr.Dropdown(label="市区町村", choices=PREF_CITIES["静岡県"], value=PREF_CITIES["静岡県"][0]) with gr.Row(): budget_min = gr.Number(label="予算下限(円)", value=300000, precision=0) budget_max = gr.Number(label="予算上限(円)", value=800000, precision=0) deadline_raw_input = gr.Textbox(label="希望納期(YYYYMMDD・8桁)", placeholder="例:20260304") details = gr.Textbox(label="詳細(任意)", lines=4, placeholder="建物の状態、面積の目安、希望条件など") create_btn = gr.Button("募集を開始(案件作成)", variant="primary") jobs_df_owner = gr.Dataframe(label="案件一覧", value=jobs_table(date_mode.value), interactive=False) pref.change(fn=update_city_choices, inputs=[pref], outputs=[city], show_progress=False) # ------------------------- # Contractor (職人) # ------------------------- with gr.Tab("職人:入札"): gr.Markdown( "## 募集中の案件に入札する\n" "下の **案件カード** をクリック(選択)すると、入札先が自動セットされます。" ) with gr.Row(): job_choice = gr.Dropdown( label="入札先案件(選択中)", choices=list_open_jobs_for_bid_choices(date_mode.value), value=None ) refresh_jobs_btn = gr.Button("案件リスト更新") job_cards = gr.Radio( label="募集中案件(カード)", choices=open_job_cards(date_mode.value), value=None, elem_id="job_cards", ) # ★HTMLで詳細表示(右上の意図しない挙動を根本解決) selected_job_card = gr.HTML("
(案件カードを選択すると詳細が表示されます)
") with gr.Row(): contractor = gr.Textbox(label="職人名(仮)", placeholder="例:山田塗装") price = gr.Number(label="提示価格(円)", value=600000, precision=0) duration_days = gr.Number(label="工期(日)", value=10, precision=0) note = gr.Textbox(label="コメント(任意)", lines=3, placeholder="材料/工程/保証/対応可能日など") bid_btn = gr.Button("入札する", variant="primary") bids_df_contractor = gr.Dataframe(label="この案件の入札一覧", value=bids_table(), interactive=False) # ------------------------- # Union (組合) # ------------------------- with gr.Tab("組合:可視化・承認"): gr.Markdown("## 組合ダッシュボード\n案件カードと入札カードで、承認(マッチング)まで進められます。") with gr.Row(): union_job = gr.Dropdown(label="案件を選択(手動)", choices=union_job_choices(date_mode.value), value=None) union_refresh_btn = gr.Button("更新") union_job_cards_ui = gr.Radio( label="案件(カード)", choices=union_job_cards(date_mode.value), value=None, elem_id="union_job_cards" ) union_selected_detail = gr.HTML("
(案件カードを選択してください)
") gr.Markdown("### 入札(職人選択)") union_bid_cards_ui = gr.Radio( label="入札(カード)", choices=[], value=None, elem_id="union_bid_cards" ) union_selected_bid_detail = gr.HTML("
(入札カードを選択してください)
") with gr.Row(): union_bid = gr.Dropdown(label="入札を選択(手動)", choices=[], value=None) approve_btn = gr.Button("この入札を承認(マッチング)", variant="primary") close_btn = gr.Button("案件をクローズ", variant="secondary") jobs_df_union = gr.Dataframe(label="案件一覧(全体)", value=jobs_table(date_mode.value), interactive=False) bids_df_union = gr.Dataframe(label="入札一覧(選択案件)", value=bids_table(), interactive=False) # ========================= # Wiring # ========================= create_btn.click( fn=create_job, inputs=[title, pref, city, budget_min, budget_max, deadline_raw_input, details, date_mode], outputs=[ gr.Textbox(visible=False), header, jobs_df_owner, job_choice, job_cards, selected_job_card, union_job, union_job_cards_ui, union_bid_cards_ui, union_bid, union_selected_detail, union_selected_bid_detail, ], show_progress=False ) refresh_jobs_btn.click( fn=refresh_contractor_view, inputs=[date_mode], outputs=[job_choice, job_cards, selected_job_card], show_progress=False ) job_cards.change( fn=on_contractor_card_select, inputs=[job_cards, date_mode], outputs=[job_choice, selected_job_card, bids_df_contractor], show_progress=False ) job_choice.change( fn=lambda jid: bids_table(jid) if jid else bids_table(), inputs=[job_choice], outputs=[bids_df_contractor], show_progress=False ) bid_btn.click( fn=place_bid, inputs=[job_choice, contractor, price, duration_days, note], outputs=[gr.Textbox(visible=False), bids_df_contractor], show_progress=False ) # 組合:更新 union_refresh_btn.click( fn=union_refresh, inputs=[union_job, date_mode], outputs=[ header, jobs_df_union, bids_df_union, union_job, union_job_cards_ui, union_bid_cards_ui, union_bid, union_selected_detail, union_selected_bid_detail ], show_progress=False ) # 組合:案件カード選択 union_job_cards_ui.change( fn=on_union_job_card_select, inputs=[union_job_cards_ui, date_mode], outputs=[union_job, bids_df_union, union_bid_cards_ui, union_bid, union_selected_detail, union_selected_bid_detail], show_progress=False ) # 組合:案件dropdown手動変更(カード・入札カードも同期) union_job.change( fn=lambda jid, dm: ( bids_table(jid) if jid else bids_table(), gr.update(choices=union_bid_cards(jid), value=None), gr.update(choices=union_bid_choices_for_job(jid), value=None), job_detail_html(jid, dm) if jid else "
(案件カードを選択してください)
", "
(入札カードを選択してください)
", gr.update(value=jid), ), inputs=[union_job, date_mode], outputs=[bids_df_union, union_bid_cards_ui, union_bid, union_selected_detail, union_selected_bid_detail, union_job_cards_ui], show_progress=False ) # 組合:入札カード選択 → union_bid(手動dropdown)にも同期 + 詳細表示 union_bid_cards_ui.change( fn=on_union_bid_card_select, inputs=[union_bid_cards_ui], outputs=[union_bid, union_selected_bid_detail], show_progress=False ) # 組合:入札dropdown手動変更 → 入札詳細表示(カード選択がなくてもOK) union_bid.change( fn=lambda bid_id: bid_detail_html(bid_id) if bid_id else "
(入札カードを選択してください)
", inputs=[union_bid], outputs=[union_selected_bid_detail], show_progress=False ) approve_btn.click( fn=approve_bid, inputs=[union_job, union_bid, date_mode], outputs=[ gr.Textbox(visible=False), header, jobs_df_union, bids_df_union, union_job, union_job_cards_ui, union_bid_cards_ui, union_bid, union_selected_detail, union_selected_bid_detail, ], show_progress=False ) close_btn.click( fn=close_job, inputs=[union_job, date_mode], outputs=[ gr.Textbox(visible=False), header, jobs_df_union, union_job, union_job_cards_ui, union_bid_cards_ui, union_bid, union_selected_detail, union_selected_bid_detail, ], show_progress=False ) date_mode.change( fn=on_date_mode_change, inputs=[date_mode, job_choice, union_job], outputs=[ jobs_df_owner, job_choice, job_cards, selected_job_card, jobs_df_union, union_job, union_job_cards_ui, union_bid_cards_ui, union_bid, union_selected_detail, union_selected_bid_detail ], show_progress=False ) reset_btn.click( fn=reset_all, inputs=[date_mode], outputs=[ gr.Textbox(visible=False), header, jobs_df_owner, bids_df_union, job_choice, job_cards, selected_job_card, union_job, union_job_cards_ui, union_bid_cards_ui, union_bid, jobs_df_union, bids_df_union, union_selected_detail, union_selected_bid_detail ], show_progress=False ) demo.launch()