File size: 9,109 Bytes
a336cbc
b82a848
 
00f5bba
 
d414a5b
00f5bba
 
 
d414a5b
a336cbc
 
 
 
 
 
00f5bba
b82a848
 
 
 
 
 
 
 
 
00f5bba
 
 
b82a848
 
 
 
 
 
 
 
 
00f5bba
 
 
 
 
 
 
b82a848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00f5bba
b82a848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00f5bba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b82a848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00f5bba
 
b82a848
ad5b04d
b82a848
 
ad5b04d
 
 
 
00f5bba
b82a848
00f5bba
b82a848
 
 
 
 
 
 
 
 
 
80bbb73
 
b82a848
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
00f5bba
476d8fd
 
 
 
 
 
 
 
 
 
00f5bba
 
 
476d8fd
b82a848
 
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
import os, json, time, tempfile
import gradio as gr
from dataclasses import asdict
from huggingface_hub import HfApi, HfFolder
from equipment_catalog import load_catalog  # add_asset_from_catalog not used here

# ------------ Hub dataset config ------------
DATA_REPO = os.environ.get("DATA_REPO", "jeffrey1963/Farm_Sim_Data")
DATA_REPO_TYPE = "dataset"  # you created a Dataset, not a Space

def _hf_token():
    tok = HfFolder.get_token()
    if not tok:
        raise RuntimeError("No HF token found. Set HF_TOKEN secret in Space settings.")
    return tok

# ------------ Catalog helpers ------------
def _catalog_list():
    cat = load_catalog()
    items = []
    for k, v in cat.items():
        d = asdict(v)
        d["id"] = k
        items.append(d)
    return items

def _catalog_lookup():
    return {i["id"]: i for i in _catalog_list()}

def _brands(items): return ["All"] + sorted(list({i["brand"] for i in items}))
def _cats(items):   return ["All"] + sorted(list({i["category"] for i in items}))

def _filter_sort(items, q, brand, category, sort_key):
    q = (q or "").lower().strip()
    brand = brand or "All"
    category = category or "All"
    out = []
    for it in items:
        if brand != "All" and it["brand"] != brand: 
            continue
        if category != "All" and it["category"] != category: 
            continue
        blob = f'{it.get("name","")} {it["brand"]} {it["model"]} {it["category"]}'.lower()
        if q and q not in blob: 
            continue
        out.append(it)
    keymap = {
        "Price ↓": lambda x: -x["list_price"],
        "Price ↑": lambda x: x["list_price"],
        "HP ↓":    lambda x: -(x.get("rated_hp") or 0),
        "HP ↑":    lambda x: (x.get("rated_hp") or 0),
        "Brand A→Z": lambda x: (x["brand"], x["model"]),
    }
    return sorted(out, key=keymap.get(sort_key, keymap["Price ↓"]))

def _grid_md(items):
    cards = []
    for it in items:
        price = f'${it["list_price"]:,.0f}'
        hp = f'{it["rated_hp"]} hp' if it.get("rated_hp") else ""
        life = f'{it["expected_life_years"]} yrs'
        salvage = f'{int(100*it["salvage_value_pct"])}%'
        cards.append(
f"""<div class="card">
  <div class="title">{it['brand']} {it['model']}</div>
  <div class="subtitle">{it.get('name','')}</div>
  <div class="meta">{it['category'].title()} Β· {hp}</div>
  <div class="price">{price}</div>
  <div class="attrs">Life: {life} Β· Salvage: {salvage}</div>
  <div class="row">
    <span class="small">ASAE dep: {it['asae_dep1']}/{it['asae_dep2']}</span>
    <span class="small">RFs: {it['repair_rf1']}/{it['repair_rf2']}</span>
  </div>
</div>"""
        )
    return (
"""<style>
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}
.card{border:1px solid #e5e7eb;border-radius:12px;padding:12px;background:white}
.title{font-weight:700}.subtitle{color:#6b7280;margin-top:2px}
.meta{color:#374151;margin:6px 0}.price{font-size:1.1rem;font-weight:700;margin:6px 0}
.attrs{color:#374151;margin-bottom:6px}.small{font-size:12px;color:#6b7280}
.row{display:flex;justify-content:space-between}
</style>
<div class="grid">
""" + "\n".join(cards) + "\n</div>"
    )

def list_for_select(items):
    return [ (f'{i["brand"]} {i["model"]} β€” ${i["list_price"]:,.0f}', i["id"]) for i in items ]

# ------------ Persistence ------------
def log_purchases_to_dataset(student_id: str, cart_items: list, catalog_lookup: dict) -> str:
    """
    Append one JSON line per cart item to dataset.
    We save a timestamped file per checkout to avoid overwriting history.
    """
    sid = (student_id or "").strip()
    if not sid:
        return "⚠️ Missing Student ID β€” NOT saved to dataset."

    ts = int(time.time())
    lines = []
    for it in cart_items:
        spec = catalog_lookup.get(it["id"], {})
        lines.append(json.dumps({
            "student_id": sid,
            "ts": ts,
            "catalog_id": it["id"],
            "label": it["label"],
            "price": float(it["price"]),
            "snapshot": {
                "brand": spec.get("brand"),
                "model": spec.get("model"),
                "category": spec.get("category"),
                "expected_life_years": spec.get("expected_life_years"),
                "salvage_value_pct": spec.get("salvage_value_pct"),
                "dep_method_default": spec.get("dep_method_default"),
                "asae_dep1": spec.get("asae_dep1"),
                "asae_dep2": spec.get("asae_dep2"),
                "repair_rf1": spec.get("repair_rf1"),
                "repair_rf2": spec.get("repair_rf2"),
                "eul_hours": spec.get("eul_hours"),
                "rated_hp": spec.get("rated_hp"),
                "transport_speed_mph": spec.get("transport_speed_mph"),
                "fuel_type": spec.get("fuel_type"),
            }
        }))

    tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl")
    with open(tmp.name, "w") as f:
        f.write("\n".join(lines) + "\n")

    api = HfApi(token=_hf_token())
    # Use timestamped file for durable history
    api.upload_file(
        path_or_fileobj=tmp.name,
        path_in_repo=f"purchases/{sid}-{ts}.jsonl",
        repo_id=DATA_REPO,
        repo_type=DATA_REPO_TYPE,
    )
    return f"πŸ’Ύ Saved {len(cart_items)} purchase(s) to dataset for {sid}."

# ------------ Cart & UI logic ------------
def refresh(q, brand, category, sort_key, state):
    items = _catalog_list()
    filtered = _filter_sort(items, q, brand, category, sort_key)
    grid = _grid_md(filtered)
    options = list_for_select(filtered)
    cart = state or []
    if not cart:
        cart_txt = "Cart is empty."
    else:
        total = sum(x["price"] for x in cart)
        lines = [f'β€’ {c["label"]} β€” ${c["price"]:,.0f}' for c in cart]
        cart_txt = "\n".join(lines) + f"\n\n**Subtotal:** ${total:,.0f}"
    return grid, gr.update(choices=options), cart_txt

def add_to_cart(sel_id, custom_price, state):
    items = {i["id"]: i for i in _catalog_list()}
    if sel_id not in items:
        return state, "Select a product first."
    it = items[sel_id]
    try:
        price = float(custom_price) if custom_price else float(it["list_price"])
    except:
        price = float(it["list_price"])
    cart = state or []
    cart.append({"id": sel_id, "label": f'{it["brand"]} {it["model"]}', "price": price})
    return cart, f"βœ… Added {it['brand']} {it['model']} (${price:,.0f}) to cart."

def clear_cart(_state): 
    return [], "🧹 Cart cleared."

def checkout(state, student_id):
    if not state:
        return "Cart is empty.", []
    note = log_purchases_to_dataset(student_id, state, _catalog_lookup())
    receipts = [f'Purchased {c["label"]} at ${c["price"]:,.0f}.' for c in state]
    return "βœ… Checkout complete!\n" + "\n".join(receipts) + f"\n\n{note}", []

# ------------ Gradio app ------------
with gr.Blocks(theme=gr.themes.Soft()) as app:
    gr.Markdown("## πŸ›’ Farm Equipment Store β€” Haymazon")
    cart_state = gr.State([])

    items = _catalog_list()

    with gr.Row():
        q = gr.Textbox(label="Search", placeholder="tractor, brand, model…")
        brand = gr.Dropdown(choices=_brands(items), value="All", label="Brand")
        category = gr.Dropdown(choices=_cats(items), value="All", label="Category")
        sort_key = gr.Dropdown(choices=["Price ↓","Price ↑","HP ↓","HP ↑","Brand Aβ†’Z"], value="Price ↓", label="Sort by")

    student_id = gr.Textbox(label="Student ID (required to save to class ledger)", placeholder="e.g., netid or student number")

    grid_md = gr.HTML()
    sel = gr.Dropdown(choices=list_for_select(items), label="Select item to add")
    custom_price = gr.Textbox(label="Custom price (optional)")
    with gr.Row():
        add_btn = gr.Button("Add to Cart")
        clear_btn = gr.Button("Clear Cart")
        buy_btn = gr.Button("Checkout")
    cart_box = gr.Markdown("Cart is empty.")
    status = gr.Markdown()

    # Events
    for w in (q, brand, category, sort_key):
        w.change(refresh, inputs=[q, brand, category, sort_key, cart_state],
                 outputs=[grid_md, sel, cart_box])

    add_btn.click(add_to_cart, inputs=[sel, custom_price, cart_state], outputs=[cart_state, status]) \
           .then(refresh, inputs=[q, brand, category, sort_key, cart_state],
                 outputs=[grid_md, sel, cart_box])

    clear_btn.click(clear_cart, inputs=[cart_state], outputs=[cart_state, status]) \
            .then(refresh, inputs=[q, brand, category, sort_key, cart_state],
                  outputs=[grid_md, sel, cart_box])

    # βœ… Checkout wired ONCE with student_id
    buy_btn.click(
        checkout,
        inputs=[cart_state, student_id],
        outputs=[status, cart_state]
    ).then(
        refresh,
        inputs=[q, brand, category, sort_key, cart_state],
        outputs=[grid_md, sel, cart_box]
    )

    # Initial render
    app.load(refresh, inputs=[q, brand, category, sort_key, cart_state],
             outputs=[grid_md, sel, cart_box])

if __name__ == "__main__":
    app.launch()