CVNSS commited on
Commit
f7cc486
·
verified ·
1 Parent(s): 0871f23

Upload 2 files

Browse files
Files changed (2) hide show
  1. index.html +620 -17
  2. tinhthanh.lookup.yaml +0 -0
index.html CHANGED
@@ -1,19 +1,622 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
6
+ <title>Tra cứu Phường/Xã sau sáp nhập (YAML)</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0b0f14;
10
+ --card: #111827;
11
+ --muted: #9ca3af;
12
+ --text: #e5e7eb;
13
+ --line: rgba(255,255,255,.08);
14
+ --accent: #22c55e;
15
+ --accent2: #60a5fa;
16
+ --warn: #f59e0b;
17
+ --danger: #ef4444;
18
+ --shadow: 0 10px 30px rgba(0,0,0,.35);
19
+ --radius: 18px;
20
+ --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
21
+ --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
22
+ }
23
+ * { box-sizing: border-box; }
24
+ body {
25
+ margin: 0;
26
+ font-family: var(--sans);
27
+ background: radial-gradient(1200px 600px at 10% 0%, rgba(96,165,250,.15), transparent 55%),
28
+ radial-gradient(1000px 500px at 90% 10%, rgba(34,197,94,.12), transparent 55%),
29
+ var(--bg);
30
+ color: var(--text);
31
+ }
32
+ header {
33
+ padding: 28px 16px 14px;
34
+ max-width: 1100px;
35
+ margin: 0 auto;
36
+ }
37
+ .title {
38
+ display:flex; align-items:flex-end; gap:12px; flex-wrap:wrap;
39
+ }
40
+ h1 {
41
+ margin: 0;
42
+ font-size: 22px;
43
+ letter-spacing: .2px;
44
+ font-weight: 700;
45
+ }
46
+ .pill {
47
+ font-size: 12px;
48
+ color: #d1fae5;
49
+ background: rgba(34,197,94,.12);
50
+ border: 1px solid rgba(34,197,94,.25);
51
+ padding: 6px 10px;
52
+ border-radius: 999px;
53
+ }
54
+ .sub {
55
+ margin-top: 10px;
56
+ color: var(--muted);
57
+ line-height: 1.45;
58
+ font-size: 13px;
59
+ }
60
+ main {
61
+ max-width: 1100px;
62
+ margin: 0 auto;
63
+ padding: 12px 16px 36px;
64
+ display: grid;
65
+ grid-template-columns: 1.2fr .8fr;
66
+ gap: 14px;
67
+ }
68
+ @media (max-width: 900px) {
69
+ main { grid-template-columns: 1fr; }
70
+ }
71
+ .card {
72
+ background: rgba(17,24,39,.7);
73
+ border: 1px solid var(--line);
74
+ border-radius: var(--radius);
75
+ box-shadow: var(--shadow);
76
+ overflow: hidden;
77
+ }
78
+ .card .hd {
79
+ padding: 14px 14px 10px;
80
+ border-bottom: 1px solid var(--line);
81
+ display:flex; align-items:center; justify-content:space-between; gap: 10px; flex-wrap:wrap;
82
+ }
83
+ .card .hd h2 {
84
+ margin: 0;
85
+ font-size: 14px;
86
+ font-weight: 700;
87
+ letter-spacing: .2px;
88
+ }
89
+ .meta {
90
+ font-size: 12px;
91
+ color: var(--muted);
92
+ display:flex; gap:10px; align-items:center; flex-wrap:wrap;
93
+ }
94
+ .badge {
95
+ font-family: var(--mono);
96
+ border: 1px solid var(--line);
97
+ padding: 4px 8px;
98
+ border-radius: 999px;
99
+ background: rgba(255,255,255,.03);
100
+ }
101
+ .body { padding: 14px; }
102
+ .tabs {
103
+ display:flex;
104
+ gap:8px;
105
+ padding: 0 14px 14px;
106
+ border-bottom: 1px solid var(--line);
107
+ }
108
+ .tab {
109
+ cursor:pointer;
110
+ user-select:none;
111
+ font-size: 13px;
112
+ padding: 10px 12px;
113
+ border-radius: 999px;
114
+ border: 1px solid var(--line);
115
+ background: rgba(255,255,255,.02);
116
+ color: var(--text);
117
+ transition: transform .05s ease, background .15s ease;
118
+ }
119
+ .tab.active {
120
+ background: rgba(96,165,250,.14);
121
+ border-color: rgba(96,165,250,.35);
122
+ }
123
+ .tab:active { transform: scale(.99); }
124
+ .grid {
125
+ display:grid;
126
+ grid-template-columns: 1fr 1fr;
127
+ gap: 10px;
128
+ }
129
+ @media (max-width: 650px) {
130
+ .grid { grid-template-columns: 1fr; }
131
+ }
132
+ label {
133
+ display:block;
134
+ font-size: 12px;
135
+ color: var(--muted);
136
+ margin: 0 0 6px;
137
+ }
138
+ input, select {
139
+ width:100%;
140
+ padding: 10px 12px;
141
+ border-radius: 12px;
142
+ border: 1px solid var(--line);
143
+ background: rgba(0,0,0,.18);
144
+ color: var(--text);
145
+ outline: none;
146
+ }
147
+ input::placeholder { color: rgba(156,163,175,.7); }
148
+ .row {
149
+ display:flex;
150
+ gap: 10px;
151
+ align-items: end;
152
+ flex-wrap: wrap;
153
+ margin-top: 10px;
154
+ }
155
+ button {
156
+ cursor:pointer;
157
+ padding: 10px 12px;
158
+ border-radius: 12px;
159
+ border: 1px solid rgba(34,197,94,.35);
160
+ background: rgba(34,197,94,.16);
161
+ color: #dcfce7;
162
+ font-weight: 650;
163
+ }
164
+ button.secondary {
165
+ border-color: rgba(96,165,250,.35);
166
+ background: rgba(96,165,250,.14);
167
+ color: #dbeafe;
168
+ }
169
+ button.ghost {
170
+ border-color: var(--line);
171
+ background: rgba(255,255,255,.03);
172
+ color: var(--text);
173
+ font-weight: 600;
174
+ }
175
+ .hint {
176
+ font-size: 12px;
177
+ color: var(--muted);
178
+ margin-top: 10px;
179
+ line-height: 1.45;
180
+ }
181
+ .out {
182
+ font-family: var(--sans);
183
+ background: rgba(0,0,0,.22);
184
+ border: 1px solid var(--line);
185
+ border-radius: 14px;
186
+ padding: 12px;
187
+ margin-top: 12px;
188
+ }
189
+ .out h3 {
190
+ margin: 0 0 8px;
191
+ font-size: 13px;
192
+ }
193
+ .kv {
194
+ display:grid;
195
+ grid-template-columns: 160px 1fr;
196
+ gap: 8px 10px;
197
+ font-size: 13px;
198
+ }
199
+ @media (max-width: 500px) {
200
+ .kv { grid-template-columns: 1fr; }
201
+ }
202
+ .k {
203
+ color: var(--muted);
204
+ font-family: var(--mono);
205
+ font-size: 12px;
206
+ }
207
+ .v { word-break: break-word; }
208
+ .list {
209
+ margin: 8px 0 0;
210
+ padding-left: 18px;
211
+ color: var(--text);
212
+ }
213
+ .empty {
214
+ color: rgba(245,158,11,.95);
215
+ border-left: 3px solid rgba(245,158,11,.75);
216
+ padding: 8px 10px;
217
+ border-radius: 12px;
218
+ background: rgba(245,158,11,.08);
219
+ margin-top: 12px;
220
+ font-size: 13px;
221
+ }
222
+ .error {
223
+ color: rgba(239,68,68,.95);
224
+ border-left: 3px solid rgba(239,68,68,.75);
225
+ padding: 8px 10px;
226
+ border-radius: 12px;
227
+ background: rgba(239,68,68,.08);
228
+ margin-top: 12px;
229
+ font-size: 13px;
230
+ }
231
+ .right .body { padding-top: 10px; }
232
+ .stats {
233
+ display:grid;
234
+ grid-template-columns: 1fr 1fr;
235
+ gap: 10px;
236
+ }
237
+ .stat {
238
+ border: 1px solid var(--line);
239
+ border-radius: 14px;
240
+ padding: 10px 12px;
241
+ background: rgba(255,255,255,.02);
242
+ }
243
+ .stat .n {
244
+ font-size: 18px;
245
+ font-weight: 800;
246
+ letter-spacing: .2px;
247
+ margin: 0;
248
+ }
249
+ .stat .t {
250
+ font-size: 12px;
251
+ color: var(--muted);
252
+ margin: 6px 0 0;
253
+ }
254
+ footer {
255
+ max-width: 1100px;
256
+ margin: 0 auto;
257
+ padding: 0 16px 26px;
258
+ color: rgba(156,163,175,.85);
259
+ font-size: 12px;
260
+ line-height: 1.5;
261
+ }
262
+ code { font-family: var(--mono); }
263
+ a { color: #93c5fd; }
264
+ </style>
265
+
266
+ <!-- YAML parser (client-side). Uses CDN; OK on Hugging Face Static Space. -->
267
+ <script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
268
+ </head>
269
+
270
+ <body>
271
+ <header>
272
+ <div class="title">
273
+ <h1>Tra cứu Phường/Xã sau sáp nhập</h1>
274
+ <span class="pill">DB: YAML</span>
275
+ <span class="pill">Chạy trực tiếp trên Hugging Face</span>
276
+ </div>
277
+ <div class="sub">
278
+ Tải dữ liệu từ <code>tinhthanh.lookup.yaml</code> trong cùng thư mục. Có 2 kiểu tra cứu:
279
+ (1) <b>Mới → Cũ</b> theo tỉnh mới + đơn vị mới; (2) <b>Cũ → Mới</b> theo tên đơn vị cũ.
280
+ </div>
281
+ </header>
282
+
283
+ <main>
284
+ <section class="card">
285
+ <div class="hd">
286
+ <h2>Tra cứu</h2>
287
+ <div class="meta">
288
+ <span id="dbStatus" class="badge">DB: đang tải…</span>
289
+ <span id="schema" class="badge">schema: -</span>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="tabs">
294
+ <div class="tab active" id="tabNew">Mới → Cũ</div>
295
+ <div class="tab" id="tabOld">Cũ → Mới</div>
296
+ </div>
297
+
298
+ <div class="body">
299
+ <!-- NEW -> OLD -->
300
+ <div id="panelNew">
301
+ <div class="grid">
302
+ <div>
303
+ <label>Tỉnh/TP mới</label>
304
+ <select id="newProvince"></select>
305
+ </div>
306
+ <div>
307
+ <label>Phường/Xã mới</label>
308
+ <select id="newUnit"></select>
309
+ </div>
310
+ </div>
311
+ <div class="row">
312
+ <button id="btnLookupNew">Tra cứu</button>
313
+ <button class="ghost" id="btnCopyKey">Copy key</button>
314
+ <button class="ghost" id="btnResetNew">Reset</button>
315
+ </div>
316
+ <div class="hint">
317
+ Mẹo: dùng menu thả xuống để tránh sai chính tả. "Copy key" sẽ copy dạng
318
+ <code>tinh-slug/donvi-slug</code> đúng theo index.
319
+ </div>
320
+ <div id="outNew"></div>
321
+ </div>
322
+
323
+ <!-- OLD -> NEW -->
324
+ <div id="panelOld" style="display:none">
325
+ <div class="grid">
326
+ <div style="grid-column: 1 / -1;">
327
+ <label>Tên phường/xã (cũ) — nhập tự do</label>
328
+ <input id="oldUnit" placeholder="Ví dụ: Thị trấn Thứ Ba / Xã Đông Yên / Phường 1 ..."/>
329
+ </div>
330
+ </div>
331
+ <div class="row">
332
+ <button class="secondary" id="btnLookupOld">Tra cứu</button>
333
+ <button class="ghost" id="btnResetOld">Reset</button>
334
+ </div>
335
+ <div class="hint">
336
+ Tra cứu theo <b>slug</b> (bỏ dấu, gạch nối). Nếu tên cũ trùng ở nhiều tỉnh, kết quả sẽ trả v�� nhiều dòng.
337
+ </div>
338
+ <div id="outOld"></div>
339
+ </div>
340
+ </div>
341
+ </section>
342
+
343
+ <aside class="card right">
344
+ <div class="hd">
345
+ <h2>Thông tin dữ liệu</h2>
346
+ <div class="meta"><span class="badge">build: 2025-12-25T09:37:55</span></div>
347
+ </div>
348
+ <div class="body">
349
+ <div class="stats">
350
+ <div class="stat">
351
+ <p class="n" id="stRows">-</p>
352
+ <p class="t">Dòng dữ liệu (rows)</p>
353
+ </div>
354
+ <div class="stat">
355
+ <p class="n" id="stProvinces">-</p>
356
+ <p class="t">Tỉnh/TP mới</p>
357
+ </div>
358
+ <div class="stat">
359
+ <p class="n" id="stNewUnits">-</p>
360
+ <p class="t">Đơn vị mới</p>
361
+ </div>
362
+ <div class="stat">
363
+ <p class="n" id="stOldSlugs">-</p>
364
+ <p class="t">Slug đơn vị cũ</p>
365
+ </div>
366
+ </div>
367
+
368
+ <div class="hint" style="margin-top:12px">
369
+ <b>Triển khai trên Hugging Face:</b><br/>
370
+ 1) Tạo Space loại <b>Static</b><br/>
371
+ 2) Upload 2 file: <code>index.html</code> và <code>tinhthanh.lookup.yaml</code><br/>
372
+ 3) Space sẽ tự chạy ngay. Không cần Python/Gradio.
373
+ </div>
374
+
375
+ <div class="hint" style="margin-top:12px">
376
+ <b>Ghi chú:</b> Trang dùng thư viện <code>js-yaml</code> từ CDN để đọc YAML.
377
+ Nếu bạn muốn chạy offline 100%, mình có thể đóng gói luôn bản <code>js-yaml.min.js</code> cục bộ.
378
+ </div>
379
+ </div>
380
+ </aside>
381
+ </main>
382
+
383
+ <footer>
384
+ File dữ liệu: <code>tinhthanh.lookup.yaml</code>. Nếu bạn muốn bổ sung lookup theo <i>mã hành chính</i>,
385
+ hoặc thêm fuzzy-search (không cần gõ đúng tuyệt đối), mình sẽ nâng schema lên v2.
386
+ </footer>
387
+
388
+ <script>
389
+ (function() {
390
+ let DB = null;
391
+
392
+ const $ = (id) => document.getElementById(id);
393
+
394
+ function slugify(s) {
395
+ return (s || "")
396
+ .trim().toLowerCase()
397
+ .normalize("NFKD").replace(/[\u0300-\u036f]/g, "")
398
+ .replace(/đ/g, "d")
399
+ .replace(/[^a-z0-9]+/g, "-")
400
+ .replace(/^-+|-+$/g, "");
401
+ }
402
+
403
+ function setStatus(text, ok=true) {
404
+ const el = $("dbStatus");
405
+ el.textContent = text;
406
+ el.style.borderColor = ok ? "rgba(34,197,94,.35)" : "rgba(239,68,68,.5)";
407
+ el.style.background = ok ? "rgba(34,197,94,.14)" : "rgba(239,68,68,.12)";
408
+ el.style.color = ok ? "#dcfce7" : "#fee2e2";
409
+ }
410
+
411
+ function escapeHtml(s) {
412
+ return (s ?? "").toString()
413
+ .replace(/&/g, "&amp;")
414
+ .replace(/</g, "&lt;")
415
+ .replace(/>/g, "&gt;")
416
+ .replace(/"/g, "&quot;")
417
+ .replace(/'/g, "&#039;");
418
+ }
419
+
420
+ function renderEmpty(targetEl, msg) {
421
+ targetEl.innerHTML = `<div class="empty">${escapeHtml(msg)}</div>`;
422
+ }
423
+ function renderError(targetEl, msg) {
424
+ targetEl.innerHTML = `<div class="error">${escapeHtml(msg)}</div>`;
425
+ }
426
+
427
+ function fillSelect(el, options, value=null) {
428
+ el.innerHTML = "";
429
+ for (const opt of options) {
430
+ const o = document.createElement("option");
431
+ o.value = opt.value;
432
+ o.textContent = opt.label;
433
+ el.appendChild(o);
434
+ }
435
+ if (value !== null) el.value = value;
436
+ }
437
+
438
+ function getProvinceNames() {
439
+ const arr = (DB?.data?.provinces || []).map(p => p.name).filter(Boolean);
440
+ arr.sort((a,b) => a.localeCompare(b, "vi"));
441
+ return arr;
442
+ }
443
+
444
+ function getUnitsForProvince(provinceName) {
445
+ const p = (DB?.data?.provinces || []).find(x => x.name === provinceName);
446
+ if (!p) return [];
447
+ const units = (p.units || []).map(u => u.name).filter(Boolean);
448
+ units.sort((a,b) => a.localeCompare(b, "vi"));
449
+ return units;
450
+ }
451
+
452
+ function lookupByNewUnit(provinceName, unitName) {
453
+ const key = `${slugify(provinceName)}/${slugify(unitName)}`;
454
+ const hit = DB?.index?.by_new_unit?.[key] || null;
455
+ return { key, hit };
456
+ }
457
+
458
+ function lookupByOldUnit(oldUnitName) {
459
+ const k = slugify(oldUnitName);
460
+ const hits = DB?.index?.by_old_unit_slug?.[k] || [];
461
+ return { slug: k, hits };
462
+ }
463
+
464
+ function renderNewResult(container, provinceName, unitName) {
465
+ const { key, hit } = lookupByNewUnit(provinceName, unitName);
466
+ if (!hit) {
467
+ renderEmpty(container, `Không tìm thấy trong index.by_new_unit cho key: ${key}`);
468
+ return;
469
+ }
470
+ const provinces = (hit.merged_from_provinces || []).map(escapeHtml).join(", ");
471
+ const mergedUnits = (hit.merged_from_units || []).map(escapeHtml);
472
+ const mergedUnitsHtml = mergedUnits.length
473
+ ? `<ul class="list">${mergedUnits.map(x => `<li>${x}</li>`).join("")}</ul>`
474
+ : "-";
475
+
476
+ container.innerHTML = `
477
+ <div class="out">
478
+ <h3>Kết quả (Mới → Cũ)</h3>
479
+ <div class="kv">
480
+ <div class="k">key</div><div class="v"><code>${escapeHtml(key)}</code></div>
481
+ <div class="k">tỉnh/TP mới</div><div class="v"><b>${escapeHtml(hit.new_province || provinceName)}</b></div>
482
+ <div class="k">đơn vị mới</div><div class="v"><b>${escapeHtml(hit.new_unit || unitName)}</b> <span class="badge">${escapeHtml(hit.new_unit_type || "-")}</span></div>
483
+ <div class="k">gộp từ tỉnh</div><div class="v">${provinces || "-"}</div>
484
+ <div class="k">gộp từ đơn vị cũ</div>
485
+ <div class="v">${mergedUnitsHtml}</div>
486
+ </div>
487
+ </div>
488
+ `;
489
+ }
490
+
491
+ function renderOldResult(container, oldName) {
492
+ const { slug, hits } = lookupByOldUnit(oldName);
493
+ if (!oldName.trim()) {
494
+ renderEmpty(container, "Hãy nhập tên phường/xã cũ để tra cứu.");
495
+ return;
496
+ }
497
+ if (!hits.length) {
498
+ renderEmpty(container, `Không tìm thấy trong index.by_old_unit_slug cho slug: ${slug}`);
499
+ return;
500
+ }
501
+ const rows = hits.map(h => `
502
+ <div class="out" style="margin-top:10px">
503
+ <h3>Kết quả (Cũ → Mới)</h3>
504
+ <div class="kv">
505
+ <div class="k">slug</div><div class="v"><code>${escapeHtml(slug)}</code></div>
506
+ <div class="k">đơn vị cũ</div><div class="v"><b>${escapeHtml(h.old_unit || oldName)}</b></div>
507
+ <div class="k">tỉnh/TP mới</div><div class="v"><b>${escapeHtml(h.new_province)}</b></div>
508
+ <div class="k">đơn vị mới</div><div class="v"><b>${escapeHtml(h.new_unit)}</b> <span class="badge">${escapeHtml(h.new_unit_type || "-")}</span></div>
509
+ </div>
510
+ </div>
511
+ `).join("");
512
+ container.innerHTML = rows;
513
+ }
514
+
515
+ function wireTabs() {
516
+ const tabNew = $("tabNew");
517
+ const tabOld = $("tabOld");
518
+ const panelNew = $("panelNew");
519
+ const panelOld = $("panelOld");
520
+
521
+ function activate(which) {
522
+ const isNew = which === "new";
523
+ tabNew.classList.toggle("active", isNew);
524
+ tabOld.classList.toggle("active", !isNew);
525
+ panelNew.style.display = isNew ? "" : "none";
526
+ panelOld.style.display = isNew ? "none" : "";
527
+ }
528
+ tabNew.addEventListener("click", () => activate("new"));
529
+ tabOld.addEventListener("click", () => activate("old"));
530
+ }
531
+
532
+ function wireControls() {
533
+ const provinceSel = $("newProvince");
534
+ const unitSel = $("newUnit");
535
+
536
+ provinceSel.addEventListener("change", () => {
537
+ const pName = provinceSel.value;
538
+ const units = getUnitsForProvince(pName);
539
+ fillSelect(unitSel, units.map(u => ({ value: u, label: u })));
540
+ $("outNew").innerHTML = "";
541
+ });
542
+
543
+ $("btnLookupNew").addEventListener("click", () => {
544
+ renderNewResult($("outNew"), provinceSel.value, unitSel.value);
545
+ });
546
+
547
+ $("btnCopyKey").addEventListener("click", async () => {
548
+ try {
549
+ const key = `${slugify(provinceSel.value)}/${slugify(unitSel.value)}`;
550
+ await navigator.clipboard.writeText(key);
551
+ const out = $("outNew");
552
+ out.innerHTML = `<div class="out"><h3>Đã copy</h3><div class="kv"><div class="k">key</div><div class="v"><code>${escapeHtml(key)}</code></div></div></div>` + out.innerHTML;
553
+ } catch (e) {
554
+ renderError($("outNew"), "Không copy được (trình duyệt chặn clipboard). Bạn có thể tự copy key trên màn hình.");
555
+ }
556
+ });
557
+
558
+ $("btnResetNew").addEventListener("click", () => {
559
+ initSelectors();
560
+ $("outNew").innerHTML = "";
561
+ });
562
+
563
+ $("btnLookupOld").addEventListener("click", () => {
564
+ renderOldResult($("outOld"), $("oldUnit").value);
565
+ });
566
+
567
+ $("btnResetOld").addEventListener("click", () => {
568
+ $("oldUnit").value = "";
569
+ $("outOld").innerHTML = "";
570
+ });
571
+
572
+ $("oldUnit").addEventListener("keydown", (e) => {
573
+ if (e.key === "Enter") {
574
+ e.preventDefault();
575
+ renderOldResult($("outOld"), $("oldUnit").value);
576
+ }
577
+ });
578
+ }
579
+
580
+ function initSelectors() {
581
+ const provinces = getProvinceNames();
582
+ const pSel = $("newProvince");
583
+ fillSelect(pSel, provinces.map(p => ({ value: p, label: p })));
584
+
585
+ const first = provinces[0] || "";
586
+ const units = getUnitsForProvince(first);
587
+ fillSelect($("newUnit"), units.map(u => ({ value: u, label: u })));
588
+ }
589
+
590
+ async function loadDB() {
591
+ try {
592
+ setStatus("DB: đang tải…", true);
593
+ const r = await fetch("tinhthanh.lookup.yaml", { cache: "no-store" });
594
+ if (!r.ok) throw new Error("HTTP " + r.status);
595
+ const txt = await r.text();
596
+ DB = jsyaml.load(txt);
597
+
598
+ const schema = DB?.meta?.schema || "-";
599
+ $("schema").textContent = "schema: " + schema;
600
+
601
+ const c = DB?.meta?.counts || {};
602
+ $("stRows").textContent = (c.rows ?? "-");
603
+ $("stProvinces").textContent = (c.new_provinces ?? (DB?.data?.provinces?.length ?? "-"));
604
+ $("stNewUnits").textContent = (c.new_units ?? "-");
605
+ $("stOldSlugs").textContent = (c.old_unit_slugs ?? "-");
606
+
607
+ setStatus("DB: sẵn sàng", true);
608
+ initSelectors();
609
+ } catch (e) {
610
+ setStatus("DB: lỗi tải", false);
611
+ renderError($("outNew"), "Không tải/đọc được tinhthanh.lookup.yaml. Hãy chắc chắn file nằm cùng thư mục với index.html.");
612
+ console.error(e);
613
+ }
614
+ }
615
+
616
+ wireTabs();
617
+ wireControls();
618
+ loadDB();
619
+ })();
620
+ </script>
621
+ </body>
622
  </html>
tinhthanh.lookup.yaml ADDED
The diff for this file is too large to render. See raw diff