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}
"""
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"""
提示価格:{yen(b['price'])}
工期:{b['duration_days']}日
"""
# =========================
# 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()