Spaces:
Runtime error
Runtime error
Download app.py from Ke-san/GuildBuild: direct link, hf CLI and curl.
- Browser
- Download file 37.8 kB
-
https://huggingface.co/spaces/Ke-san/GuildBuild/resolve/main/app.py
- Command line
-
hf download hf://spaces/Ke-san/GuildBuild/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Ke-san/GuildBuild/resolve/main/app.py
37.8 kB
| 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 "<div style='color:#6b7280;'>(案件が見つかりません)</div>" | |
| 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""" | |
| <div style="border:1px solid #e5e7eb;border-radius:18px;padding:16px;background:#ffffff;"> | |
| <div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;"> | |
| <div style="font-size:20px;font-weight:900;line-height:1.2;">{j['title']}</div> | |
| <div style="padding:6px 10px;border-radius:999px;background:{badge_bg};color:{badge_fg};font-weight:800;"> | |
| {badge_text} | |
| </div> | |
| </div> | |
| <div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap;"> | |
| <div style="padding:7px 10px;border-radius:999px;background:#f3f4f6;">エリア:<b>{j['area']}</b></div> | |
| <div style="padding:7px 10px;border-radius:999px;background:#f3f4f6;">予算:<b>{yen(j['budget_min'])}〜{yen(j['budget_max'])}</b></div> | |
| <div style="padding:7px 10px;border-radius:999px;background:#f3f4f6;">納期:<b>{dl}</b></div> | |
| <div style="padding:7px 10px;border-radius:999px;background:#eef2ff;color:#111827;"><b>{rem}</b></div> | |
| </div> | |
| <div style="margin-top:14px;padding:12px;border-radius:14px;background:#fafafa;border:1px solid #f3f4f6;"> | |
| <div style="font-weight:800;margin-bottom:6px;color:#111827;">詳細</div> | |
| <div style="color:#374151;white-space:pre-wrap;line-height:1.6;">{details}</div> | |
| </div> | |
| </div> | |
| """ | |
| 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 "<div style='color:#6b7280;'>(入札が見つかりません)</div>" | |
| 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""" | |
| <div style="border:1px solid #e5e7eb;border-radius:18px;padding:16px;background:#ffffff;"> | |
| <div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;"> | |
| <div style="font-size:18px;font-weight:900;line-height:1.2;">{b['contractor']}</div> | |
| <div style="padding:6px 10px;border-radius:999px;background:{bg};color:#111827;font-weight:800;">{bt}</div> | |
| </div> | |
| <div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap;"> | |
| <div style="padding:7px 10px;border-radius:999px;background:#f3f4f6;">提示価格:<b>{yen(b['price'])}</b></div> | |
| <div style="padding:7px 10px;border-radius:999px;background:#f3f4f6;">工期:<b>{b['duration_days']}日</b></div> | |
| </div> | |
| <div style="margin-top:14px;padding:12px;border-radius:14px;background:#fafafa;border:1px solid #f3f4f6;"> | |
| <div style="font-weight:800;margin-bottom:6px;color:#111827;">コメント</div> | |
| <div style="color:#374151;white-space:pre-wrap;line-height:1.6;">{note}</div> | |
| </div> | |
| </div> | |
| """ | |
| # ========================= | |
| # 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), | |
| "<div style='color:#6b7280;'>(案件を選択してください)</div>", | |
| 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 | |
| "<div style='color:#6b7280;'>(案件カードを選択してください)</div>", | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(案件カードを選択すると詳細が表示されます)</div>", | |
| 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), | |
| "<div style='color:#6b7280;'>(案件カードを選択してください)</div>", | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(案件カードを選択すると詳細が表示されます)</div>" | |
| ) | |
| def on_contractor_card_select(job_id, date_mode): | |
| if not job_id: | |
| return ( | |
| gr.update(value=None), | |
| "<div style='color:#6b7280;'>(案件を選択してください)</div>", | |
| 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 "<div style='color:#6b7280;'>(案件カードを選択してください)</div>" | |
| detail_bid = "<div style='color:#6b7280;'>(入札カードを選択してください)</div>" | |
| 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 | |
| "<div style='color:#6b7280;'>(案件カードを選択してください)</div>", | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| def on_union_bid_card_select(bid_id): | |
| if not bid_id: | |
| return ( | |
| gr.update(value=None), | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>" | |
| ) | |
| 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 "<div style='color:#6b7280;'>(案件カードを選択してください)</div>", | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(案件カードを選択してください)</div>", | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(案件カードを選択してください)</div>", | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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 \ | |
| "<div style='color:#6b7280;'>(案件カードを選択すると詳細が表示されます)</div>" | |
| union_card = job_detail_html(union_selected_job, date_mode) if union_selected_job else \ | |
| "<div style='color:#6b7280;'>(案件カードを選択してください)</div>" | |
| 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, | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| 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), | |
| "<div style='color:#6b7280;'>(案件カードを選択すると詳細が表示されます)</div>", | |
| 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(), | |
| "<div style='color:#6b7280;'>(案件カードを選択してください)</div>", | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| ) | |
| # ========================= | |
| # 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("<div style='color:#6b7280;'>(案件カードを選択すると詳細が表示されます)</div>") | |
| 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("<div style='color:#6b7280;'>(案件カードを選択してください)</div>") | |
| gr.Markdown("### 入札(職人選択)") | |
| union_bid_cards_ui = gr.Radio( | |
| label="入札(カード)", | |
| choices=[], | |
| value=None, | |
| elem_id="union_bid_cards" | |
| ) | |
| union_selected_bid_detail = gr.HTML("<div style='color:#6b7280;'>(入札カードを選択してください)</div>") | |
| 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 "<div style='color:#6b7280;'>(案件カードを選択してください)</div>", | |
| "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| 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 "<div style='color:#6b7280;'>(入札カードを選択してください)</div>", | |
| 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() | |