File size: 37,806 Bytes
22d0163
 
 
b9fb5f7
22d0163
 
 
 
 
094128e
22d0163
 
 
b9fb5f7
 
 
 
 
 
 
 
 
 
 
dabc31f
6e24d1b
dabc31f
 
49a88ce
 
dabc31f
 
 
 
 
9afeafe
49a88ce
dabc31f
 
 
 
 
 
9afeafe
dabc31f
49a88ce
dabc31f
 
 
49a88ce
dabc31f
 
 
 
49a88ce
6e24d1b
49a88ce
 
dabc31f
 
6e24d1b
 
 
49a88ce
6e24d1b
49a88ce
 
6e24d1b
dabc31f
 
 
094128e
 
 
22d0163
 
 
 
 
 
 
 
 
094128e
 
 
 
 
 
 
 
 
 
 
 
 
 
b9fb5f7
094128e
 
 
 
b9fb5f7
094128e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9fb5f7
49a88ce
 
 
 
 
 
 
 
924405c
 
 
22d0163
 
 
 
 
 
 
 
 
 
 
49a88ce
22d0163
 
b9fb5f7
22d0163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
094128e
22d0163
 
 
 
 
 
 
094128e
 
22d0163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9fb5f7
49a88ce
b9fb5f7
094128e
dabc31f
b9fb5f7
094128e
 
 
dabc31f
094128e
49a88ce
094128e
dabc31f
 
 
49a88ce
094128e
dabc31f
 
 
 
b9fb5f7
49a88ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b9fb5f7
 
49a88ce
9afeafe
 
094128e
 
 
49a88ce
6e24d1b
b9fb5f7
9afeafe
 
 
49a88ce
 
 
9afeafe
 
 
 
 
 
 
 
 
 
 
 
b9fb5f7
 
 
 
49a88ce
 
 
 
6e24d1b
49a88ce
 
6e24d1b
49a88ce
 
 
 
 
 
 
6e24d1b
49a88ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e24d1b
924405c
 
 
094128e
924405c
 
 
094128e
 
924405c
 
 
094128e
 
 
 
 
 
924405c
 
 
 
 
 
 
 
 
 
 
 
 
 
b9fb5f7
 
 
 
094128e
 
 
 
924405c
094128e
924405c
094128e
 
 
49a88ce
094128e
6e24d1b
49a88ce
 
 
 
924405c
22d0163
 
b9fb5f7
 
22d0163
 
 
b9fb5f7
22d0163
 
dabc31f
22d0163
 
 
 
 
 
924405c
 
 
094128e
 
 
49a88ce
094128e
6e24d1b
49a88ce
 
 
 
924405c
 
094128e
b9fb5f7
094128e
 
49a88ce
b9fb5f7
 
6e24d1b
094128e
 
 
49a88ce
094128e
 
 
 
49a88ce
094128e
 
 
22d0163
 
 
 
924405c
22d0163
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
094128e
 
22d0163
49a88ce
 
 
 
 
 
 
 
 
6e24d1b
 
 
 
 
49a88ce
 
 
 
 
6e24d1b
 
49a88ce
6e24d1b
 
 
 
49a88ce
 
 
 
6e24d1b
49a88ce
6e24d1b
 
 
49a88ce
6e24d1b
49a88ce
 
 
 
 
 
 
 
 
 
 
 
 
6e24d1b
22d0163
094128e
22d0163
6e24d1b
 
 
 
 
 
 
49a88ce
6e24d1b
49a88ce
 
6e24d1b
22d0163
 
 
 
6e24d1b
 
 
 
 
 
 
49a88ce
6e24d1b
49a88ce
 
6e24d1b
22d0163
 
6e24d1b
 
 
 
 
 
 
49a88ce
6e24d1b
49a88ce
 
6e24d1b
22d0163
 
 
 
 
 
 
 
 
6e24d1b
 
 
 
 
 
 
49a88ce
6e24d1b
49a88ce
 
6e24d1b
22d0163
094128e
22d0163
6e24d1b
 
 
 
 
 
49a88ce
 
 
 
6e24d1b
22d0163
 
 
6e24d1b
 
 
 
 
 
49a88ce
 
 
 
6e24d1b
22d0163
 
6e24d1b
 
 
 
 
 
49a88ce
 
 
 
6e24d1b
 
 
49a88ce
 
6e24d1b
49a88ce
 
6e24d1b
 
49a88ce
 
 
 
 
 
 
 
 
 
 
6e24d1b
22d0163
094128e
22d0163
 
924405c
 
 
094128e
924405c
49a88ce
 
 
 
 
 
 
 
 
 
 
924405c
22d0163
 
 
 
dabc31f
22d0163
 
 
094128e
 
 
 
 
22d0163
 
 
 
094128e
22d0163
 
094128e
 
 
 
924405c
22d0163
 
b9fb5f7
 
924405c
22d0163
 
 
094128e
924405c
22d0163
 
094128e
22d0163
b9fb5f7
 
22d0163
094128e
22d0163
 
094128e
 
dabc31f
094128e
924405c
b9fb5f7
 
 
094128e
b9fb5f7
 
 
 
094128e
dabc31f
094128e
dabc31f
 
094128e
 
49a88ce
 
22d0163
 
 
 
 
 
924405c
22d0163
 
 
 
094128e
22d0163
 
49a88ce
22d0163
 
6e24d1b
22d0163
 
6e24d1b
 
 
 
 
 
 
49a88ce
 
 
 
 
 
 
 
 
 
 
 
6e24d1b
22d0163
49a88ce
22d0163
 
 
094128e
22d0163
 
094128e
 
 
924405c
 
094128e
924405c
 
 
 
094128e
 
 
 
6e24d1b
49a88ce
 
6e24d1b
49a88ce
924405c
 
 
 
094128e
 
 
 
 
 
 
 
6e24d1b
094128e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49a88ce
094128e
 
 
49a88ce
 
 
 
 
 
 
 
 
 
 
6e24d1b
 
 
49a88ce
6e24d1b
49a88ce
6e24d1b
49a88ce
094128e
 
 
49a88ce
094128e
6e24d1b
094128e
49a88ce
6e24d1b
49a88ce
 
 
094128e
6e24d1b
49a88ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
094128e
 
 
 
 
 
6e24d1b
 
 
 
 
 
 
49a88ce
6e24d1b
 
49a88ce
6e24d1b
094128e
 
 
 
 
 
6e24d1b
 
 
 
 
 
49a88ce
 
6e24d1b
49a88ce
6e24d1b
094128e
 
 
 
 
 
6e24d1b
 
49a88ce
 
6e24d1b
094128e
 
 
22d0163
 
094128e
924405c
 
 
 
 
 
094128e
b9fb5f7
924405c
6e24d1b
49a88ce
924405c
094128e
6e24d1b
49a88ce
 
924405c
22d0163
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
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()