fastapi-sqlite-ui 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1131 @@
1
+ def get_html_template(base_path: str) -> str:
2
+ cleaned_base_path = base_path[:-1] if base_path.endswith('/') else base_path
3
+
4
+ html = r"""<!DOCTYPE html>
5
+ <html lang="en" class="h-full">
6
+ <head>
7
+ <meta charset="UTF-8">
8
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
9
+ <title>SQLite Admin Dashboard</title>
10
+ <!-- Google Fonts: Inter -->
11
+ <link rel="preconnect" href="https://fonts.googleapis.com">
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
14
+
15
+ <!-- Tailwind CSS CDN -->
16
+ <script src="https://cdn.tailwindcss.com"></script>
17
+ <script>
18
+ tailwind.config = {
19
+ darkMode: 'class',
20
+ theme: {
21
+ extend: {
22
+ fontFamily: {
23
+ sans: ['Inter', 'sans-serif'],
24
+ mono: ['Fira Code', 'monospace'],
25
+ },
26
+ colors: {
27
+ brand: {
28
+ 50: '#f4f5fa',
29
+ 100: '#eae9f5',
30
+ 200: '#d7d4ee',
31
+ 300: '#bab2e2',
32
+ 400: '#9b8bd2',
33
+ 500: '#7c65be',
34
+ 600: '#684da7',
35
+ 700: '#563e8a',
36
+ 800: '#483473',
37
+ 900: '#3d2d60',
38
+ 950: '#251a3d',
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ </script>
45
+
46
+ <!-- Alpine.js CDN -->
47
+ <script defer src="https://unpkg.com/alpinejs@3.13.5/dist/cdn.min.js"></script>
48
+
49
+ <!-- Lucide Icons -->
50
+ <script src="https://unpkg.com/lucide@latest"></script>
51
+
52
+ <style>
53
+ body {
54
+ background-color: #0b0f19;
55
+ color: #f3f4f6;
56
+ }
57
+ /* Custom Scrollbar */
58
+ ::-webkit-scrollbar {
59
+ width: 6px;
60
+ height: 6px;
61
+ }
62
+ ::-webkit-scrollbar-track {
63
+ background: #0f172a;
64
+ }
65
+ ::-webkit-scrollbar-thumb {
66
+ background: #334155;
67
+ border-radius: 3px;
68
+ }
69
+ ::-webkit-scrollbar-thumb:hover {
70
+ background: #475569;
71
+ }
72
+ /* Glassmorphism Classes */
73
+ .glass-card {
74
+ background: rgba(17, 24, 39, 0.7);
75
+ backdrop-filter: blur(12px);
76
+ -webkit-backdrop-filter: blur(12px);
77
+ border: 1px solid rgba(255, 255, 255, 0.05);
78
+ }
79
+ .glass-input {
80
+ background: rgba(31, 41, 55, 0.5);
81
+ border: 1px solid rgba(255, 255, 255, 0.08);
82
+ }
83
+ .glass-input:focus {
84
+ border-color: #9b8bd2;
85
+ background: rgba(31, 41, 55, 0.8);
86
+ box-shadow: 0 0 0 2px rgba(155, 139, 210, 0.2);
87
+ }
88
+ </style>
89
+ </head>
90
+ <body class="h-full flex overflow-hidden antialiased font-sans" x-data="appState()">
91
+
92
+ <!-- Toast Notification System -->
93
+ <div class="fixed top-4 right-4 z-50 flex flex-col gap-2 pointer-events-none">
94
+ <template x-for="toast in toasts" :key="toast.id">
95
+ <div
96
+ x-transition:enter="transition ease-out duration-300 transform translate-y-[-10px] opacity-0"
97
+ x-transition:enter-start="opacity-0 translate-y-[-10px]"
98
+ x-transition:enter-end="opacity-100 translate-y-0"
99
+ x-transition:leave="transition ease-in duration-200 opacity-0 transform translate-x-[10px]"
100
+ class="pointer-events-auto p-4 rounded-xl flex items-start gap-3 shadow-lg w-80 max-w-full border"
101
+ :class="toast.type === 'success' ? 'bg-emerald-950/90 border-emerald-500/30 text-emerald-200' : 'bg-rose-950/90 border-rose-500/30 text-rose-200'"
102
+ >
103
+ <span class="mt-0.5">
104
+ <template x-if="toast.type === 'success'">
105
+ <svg class="w-5 h-5 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
106
+ </template>
107
+ <template x-if="toast.type === 'error'">
108
+ <svg class="w-5 h-5 text-rose-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
109
+ </template>
110
+ </span>
111
+ <div class="flex-1">
112
+ <p class="text-sm font-semibold" x-text="toast.title"></p>
113
+ <p class="text-xs mt-0.5 opacity-80" x-text="toast.message"></p>
114
+ </div>
115
+ <button @click="removeToast(toast.id)" class="text-gray-400 hover:text-white transition">
116
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
117
+ </button>
118
+ </div>
119
+ </template>
120
+ </div>
121
+
122
+ <!-- Sidebar -->
123
+ <aside class="w-64 flex-shrink-0 flex flex-col border-r border-slate-800 bg-[#0f1524]">
124
+ <!-- Brand / Title -->
125
+ <div class="p-5 border-b border-slate-800 flex items-center gap-3">
126
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-tr from-brand-600 to-indigo-500 flex items-center justify-center shadow-lg shadow-brand-500/20">
127
+ <i data-lucide="database" class="w-4.5 h-4.5 text-white"></i>
128
+ </div>
129
+ <div>
130
+ <h1 class="font-bold text-sm tracking-wide text-white">HONOLITE</h1>
131
+ <p class="text-[10px] text-slate-400 font-medium tracking-wider uppercase">SQLite Admin Panel</p>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Search Tables -->
136
+ <div class="p-3">
137
+ <div class="relative">
138
+ <span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
139
+ <i data-lucide="search" class="h-4 w-4 text-slate-500"></i>
140
+ </span>
141
+ <input
142
+ type="text"
143
+ placeholder="Tìm kiếm bảng..."
144
+ x-model="tableSearchQuery"
145
+ class="w-full pl-9 pr-3 py-1.5 text-xs rounded-lg glass-input focus:outline-none text-slate-200 placeholder-slate-500"
146
+ />
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Tables List -->
151
+ <div class="flex-1 overflow-y-auto px-2 pb-4">
152
+ <nav class="space-y-0.5">
153
+ <!-- SQL Editor Tab Button -->
154
+ <button
155
+ @click="selectSQLEditor()"
156
+ class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-xs font-medium transition duration-200 group"
157
+ :class="isSQLEditorActive ? 'bg-brand-950/80 border border-brand-500/30 text-brand-300' : 'text-slate-400 hover:bg-slate-800/50 hover:text-slate-200'"
158
+ >
159
+ <i data-lucide="terminal" class="w-4 h-4 flex-shrink-0" :class="isSQLEditorActive ? 'text-brand-400' : 'text-slate-500 group-hover:text-slate-400'"></i>
160
+ <span class="flex-1 text-left">Trình chạy SQL Console</span>
161
+ </button>
162
+
163
+ <div class="h-px bg-slate-800 my-2"></div>
164
+
165
+ <p class="px-3 py-1 text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Danh sách bảng</p>
166
+
167
+ <template x-for="table in filteredTables" :key="table">
168
+ <button
169
+ @click="selectTable(table)"
170
+ class="w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition duration-200 group"
171
+ :class="activeTable === table && !isSQLEditorActive ? 'bg-slate-800 text-white' : 'text-slate-400 hover:bg-slate-900 hover:text-slate-200'"
172
+ >
173
+ <div class="flex items-center gap-3 overflow-hidden">
174
+ <i data-lucide="table-2" class="w-4 h-4 flex-shrink-0" :class="activeTable === table && !isSQLEditorActive ? 'text-brand-400' : 'text-slate-500 group-hover:text-slate-400'"></i>
175
+ <span class="truncate" x-text="table"></span>
176
+ </div>
177
+ <i data-lucide="chevron-right" class="w-3.5 h-3.5 text-slate-600 opacity-0 group-hover:opacity-100 transition-opacity"></i>
178
+ </button>
179
+ </template>
180
+ </nav>
181
+ </div>
182
+ </aside>
183
+
184
+ <!-- Main View -->
185
+ <main class="flex-1 flex flex-col overflow-hidden bg-[#070a13]">
186
+ <!-- Topbar / Header -->
187
+ <header class="h-16 flex items-center justify-between px-8 border-b border-slate-800/80 bg-[#0c101d]">
188
+ <div class="flex items-center gap-4">
189
+ <template x-if="isSQLEditorActive">
190
+ <div class="flex items-center gap-2">
191
+ <span class="p-1.5 rounded-lg bg-brand-950 border border-brand-500/20 text-brand-400">
192
+ <i data-lucide="terminal" class="w-4 h-4"></i>
193
+ </span>
194
+ <span class="text-sm font-semibold text-white">SQL Raw Console</span>
195
+ </div>
196
+ </template>
197
+ <template x-if="!isSQLEditorActive && activeTable">
198
+ <div class="flex items-center gap-2">
199
+ <span class="p-1.5 rounded-lg bg-slate-800 border border-slate-700 text-slate-300">
200
+ <i data-lucide="table-2" class="w-4 h-4"></i>
201
+ </span>
202
+ <div>
203
+ <span class="text-sm font-semibold text-white" x-text="activeTable"></span>
204
+ <span class="text-xs text-slate-400 ml-2" x-text="'(' + totalRows + ' dòng)'"></span>
205
+ </div>
206
+ </div>
207
+ </template>
208
+ </div>
209
+
210
+ <div class="flex items-center gap-3">
211
+ <!-- Read-Only Badge -->
212
+ <template x-if="isReadOnly">
213
+ <span class="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-semibold bg-amber-950/60 text-amber-300 border border-amber-500/20">
214
+ <i data-lucide="eye" class="w-3 h-3"></i> Read-Only
215
+ </span>
216
+ </template>
217
+
218
+ <!-- Refresh Button -->
219
+ <button
220
+ @click="refreshData()"
221
+ class="flex items-center justify-center p-2 rounded-lg border border-slate-800 text-slate-400 hover:text-white hover:bg-slate-800/60 transition"
222
+ title="Tải lại dữ liệu"
223
+ >
224
+ <i data-lucide="refresh-cw" class="w-4 h-4" :class="loading ? 'animate-spin' : ''"></i>
225
+ </button>
226
+ </div>
227
+ </header>
228
+
229
+ <!-- Content Workspace -->
230
+ <div class="flex-1 overflow-y-auto p-8 relative">
231
+ <!-- Loading Overlay -->
232
+ <template x-if="loading && !rows.length && !queryResult.rows">
233
+ <div class="absolute inset-0 bg-slate-950/40 backdrop-blur-xs flex items-center justify-center z-10">
234
+ <div class="flex flex-col items-center gap-3">
235
+ <div class="w-10 h-10 border-4 border-brand-500 border-t-transparent rounded-full animate-spin"></div>
236
+ <p class="text-xs text-slate-400 font-medium">Đang tải dữ liệu...</p>
237
+ </div>
238
+ </div>
239
+ </template>
240
+
241
+ <!-- 1. SQL Editor View -->
242
+ <template x-if="isSQLEditorActive">
243
+ <div class="space-y-6">
244
+ <div class="glass-card rounded-xl overflow-hidden">
245
+ <div class="px-5 py-3 border-b border-slate-800 bg-slate-900/50 flex items-center justify-between">
246
+ <span class="text-xs font-semibold text-slate-300">Nhập truy vấn SQL của bạn</span>
247
+ <span class="text-[10px] text-slate-500 font-mono">Hỗ trợ các câu lệnh SQLite tiêu chuẩn</span>
248
+ </div>
249
+ <div class="p-5 space-y-4">
250
+ <textarea
251
+ x-model="sqlQuery"
252
+ class="w-full h-40 p-4 font-mono text-sm rounded-xl glass-input text-slate-100 focus:outline-none"
253
+ placeholder="SELECT * FROM users LIMIT 10;"
254
+ ></textarea>
255
+ <div class="flex items-center justify-between">
256
+ <button
257
+ @click="clearSqlQuery()"
258
+ class="px-4 py-2 text-xs font-semibold text-slate-400 hover:text-white transition"
259
+ >
260
+ Xóa câu lệnh
261
+ </button>
262
+ <button
263
+ @click="runSqlQuery()"
264
+ :disabled="loading || !sqlQuery.trim()"
265
+ class="px-5 py-2.5 rounded-xl bg-brand-600 hover:bg-brand-500 disabled:opacity-50 disabled:cursor-not-allowed font-semibold text-xs tracking-wide text-white transition flex items-center gap-2 shadow-lg shadow-brand-500/20"
266
+ >
267
+ <i data-lucide="play" class="w-3.5 h-3.5 fill-current"></i> Chạy câu lệnh (Ctrl+Enter)
268
+ </button>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- Query results -->
274
+ <template x-if="queryResult.rows || queryResult.error || queryResult.affectedRows !== undefined">
275
+ <div class="glass-card rounded-xl overflow-hidden">
276
+ <div class="px-5 py-3.5 border-b border-slate-800 bg-slate-900/30 flex items-center justify-between">
277
+ <span class="text-xs font-semibold text-slate-300">Kết quả truy vấn</span>
278
+ <span
279
+ class="text-xs text-slate-400"
280
+ x-text="queryResult.timeMs !== undefined ? 'Thời gian chạy: ' + queryResult.timeMs + 'ms' : ''"
281
+ ></span>
282
+ </div>
283
+ <div class="p-5">
284
+ <!-- Success Write Alert -->
285
+ <template x-if="queryResult.affectedRows !== undefined && !queryResult.error">
286
+ <div class="p-4 rounded-xl bg-indigo-950/40 border border-indigo-500/20 text-indigo-300 text-sm flex items-center gap-3">
287
+ <i data-lucide="info" class="w-5 h-5 text-indigo-400"></i>
288
+ <span>Thành công! Số dòng bị ảnh hưởng: <strong class="text-white font-semibold" x-text="queryResult.affectedRows"></strong> dòng.</span>
289
+ </div>
290
+ </template>
291
+
292
+ <!-- Error Alert -->
293
+ <template x-if="queryResult.error">
294
+ <div class="p-4 rounded-xl bg-rose-950/40 border border-rose-500/20 text-rose-300 text-sm flex items-start gap-3">
295
+ <i data-lucide="alert-triangle" class="w-5 h-5 text-rose-400 mt-0.5 flex-shrink-0"></i>
296
+ <div>
297
+ <h4 class="font-bold text-white mb-0.5">Lỗi SQL:</h4>
298
+ <code class="text-xs font-mono break-all text-rose-200" x-text="queryResult.error"></code>
299
+ </div>
300
+ </div>
301
+ </template>
302
+
303
+ <!-- Result Table -->
304
+ <template x-if="queryResult.rows && queryResult.rows.length > 0">
305
+ <div class="overflow-x-auto border border-slate-800 rounded-lg">
306
+ <table class="w-full text-left border-collapse text-xs">
307
+ <thead>
308
+ <tr class="bg-slate-900 border-b border-slate-800 text-slate-300 uppercase tracking-wider font-semibold">
309
+ <template x-for="col in queryResult.columns" :key="col">
310
+ <th class="px-4 py-3" x-text="col"></th>
311
+ </template>
312
+ </tr>
313
+ </thead>
314
+ <tbody class="divide-y divide-slate-800/60">
315
+ <template x-for="(row, idx) in queryResult.rows" :key="idx">
316
+ <tr class="hover:bg-slate-900/30 transition">
317
+ <template x-for="col in queryResult.columns" :key="col">
318
+ <td class="px-4 py-2.5 font-mono text-slate-300 break-words max-w-xs" x-text="formatCellValue(row[col])"></td>
319
+ </template>
320
+ </tr>
321
+ </template>
322
+ </tbody>
323
+ </table>
324
+ </div>
325
+ </template>
326
+
327
+ <!-- Empty Results -->
328
+ <template x-if="queryResult.rows && queryResult.rows.length === 0 && queryResult.affectedRows === undefined && !queryResult.error">
329
+ <div class="py-8 text-center text-slate-500 text-xs">
330
+ Truy vấn không trả về dòng dữ liệu nào.
331
+ </div>
332
+ </template>
333
+ </div>
334
+ </div>
335
+ </template>
336
+ </div>
337
+ </template>
338
+
339
+ <!-- 2. Table Detail View -->
340
+ <template x-if="!isSQLEditorActive && activeTable">
341
+ <div class="space-y-6" x-init="$nextTick(() => { lucide.createIcons(); })">
342
+ <!-- View Tab Selector -->
343
+ <div class="flex border-b border-slate-800/80 gap-6">
344
+ <button
345
+ @click="activeTab = 'browse'"
346
+ class="pb-3 text-xs font-semibold tracking-wide border-b-2 transition duration-200 flex items-center gap-2"
347
+ :class="activeTab === 'browse' ? 'border-brand-500 text-brand-400' : 'border-transparent text-slate-400 hover:text-slate-200'"
348
+ >
349
+ <i data-lucide="eye" class="w-4 h-4"></i> Duyệt Dữ Liệu
350
+ </button>
351
+ <button
352
+ @click="activeTab = 'schema'"
353
+ class="pb-3 text-xs font-semibold tracking-wide border-b-2 transition duration-200 flex items-center gap-2"
354
+ :class="activeTab === 'schema' ? 'border-brand-500 text-brand-400' : 'border-transparent text-slate-400 hover:text-slate-200'"
355
+ >
356
+ <i data-lucide="info" class="w-4 h-4"></i> Cấu Trúc Bảng
357
+ </button>
358
+ </div>
359
+
360
+ <!-- Browse Tab -->
361
+ <div x-show="activeTab === 'browse'" class="space-y-4">
362
+ <!-- Search & Actions -->
363
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
364
+ <!-- Search inside table -->
365
+ <div class="relative w-full sm:w-72">
366
+ <span class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
367
+ <i data-lucide="search" class="h-4 w-4 text-slate-500"></i>
368
+ </span>
369
+ <input
370
+ type="text"
371
+ placeholder="Tìm trong các cột Text..."
372
+ x-model.debounce.500ms="searchQuery"
373
+ class="w-full pl-9 pr-3 py-2 text-xs rounded-xl glass-input focus:outline-none text-slate-200 placeholder-slate-500"
374
+ />
375
+ </div>
376
+
377
+ <!-- Create record button -->
378
+ <template x-if="!isReadOnly">
379
+ <button
380
+ @click="openAddModal()"
381
+ class="px-4 py-2 bg-brand-600 hover:bg-brand-500 rounded-xl text-xs font-semibold text-white tracking-wide transition shadow-lg shadow-brand-500/10 flex items-center gap-2"
382
+ >
383
+ <i data-lucide="plus" class="w-3.5 h-3.5"></i> Thêm Dòng Mới
384
+ </button>
385
+ </template>
386
+ </div>
387
+
388
+ <!-- Data Table Grid -->
389
+ <div class="glass-card rounded-xl overflow-hidden">
390
+ <div class="overflow-x-auto">
391
+ <table class="w-full text-left border-collapse text-xs">
392
+ <thead>
393
+ <tr class="bg-slate-900 border-b border-slate-800 text-slate-400 font-semibold uppercase tracking-wider">
394
+ <!-- Operations column header -->
395
+ <template x-if="!isReadOnly">
396
+ <th class="px-5 py-3.5 w-24 text-center">Thao tác</th>
397
+ </template>
398
+ <template x-for="col in schema.columns" :key="col.name">
399
+ <th class="px-5 py-3.5 font-semibold">
400
+ <div class="flex items-center gap-1">
401
+ <span class="text-slate-200" x-text="col.name"></span>
402
+ <span class="text-[9px] text-slate-500 normal-case" x-text="col.type"></span>
403
+ <template x-if="col.primaryKey">
404
+ <span class="px-1 text-[8px] bg-brand-950 text-brand-300 border border-brand-500/20 rounded font-bold">PK</span>
405
+ </template>
406
+ </div>
407
+ </th>
408
+ </template>
409
+ </tr>
410
+ </thead>
411
+ <tbody class="divide-y divide-slate-800/50">
412
+ <template x-for="(row, idx) in rows" :key="idx">
413
+ <tr class="hover:bg-slate-900/30 transition">
414
+ <!-- Operations column cells -->
415
+ <template x-if="!isReadOnly">
416
+ <td class="px-5 py-3 w-24 text-center">
417
+ <div class="flex items-center justify-center gap-2.5">
418
+ <button @click="openEditModal(row)" class="text-slate-400 hover:text-brand-400 transition" title="Sửa">
419
+ <i data-lucide="edit-3" class="w-4 h-4"></i>
420
+ </button>
421
+ <button @click="openDeleteModal(row)" class="text-slate-400 hover:text-rose-400 transition" title="Xóa">
422
+ <i data-lucide="trash-2" class="w-4 h-4"></i>
423
+ </button>
424
+ </div>
425
+ </td>
426
+ </template>
427
+ <template x-for="col in schema.columns" :key="col.name">
428
+ <td class="px-5 py-3 font-mono text-slate-300 break-words max-w-xs" x-text="formatCellValue(row[col.name])"></td>
429
+ </template>
430
+ </tr>
431
+ </template>
432
+
433
+ <!-- Empty Data Row -->
434
+ <template x-if="rows.length === 0">
435
+ <tr>
436
+ <td :colspan="schema.columns.length + (isReadOnly ? 0 : 1)" class="py-12 text-center text-slate-500">
437
+ Bảng hiện không có dữ liệu nào phù hợp.
438
+ </td>
439
+ </tr>
440
+ </template>
441
+ </tbody>
442
+ </table>
443
+ </div>
444
+
445
+ <!-- Pagination Info and Actions -->
446
+ <div class="px-5 py-4 border-t border-slate-800/80 bg-slate-900/10 flex items-center justify-between flex-wrap gap-4 text-xs text-slate-400">
447
+ <div>
448
+ Hiển thị từ <strong class="text-slate-200" x-text="((page - 1) * limit) + 1"></strong>
449
+ đến <strong class="text-slate-200" x-text="Math.min(page * limit, totalRows)"></strong>
450
+ trong tổng số <strong class="text-slate-200" x-text="totalRows"></strong> dòng.
451
+ </div>
452
+
453
+ <div class="flex items-center gap-4">
454
+ <!-- Rows per page selector -->
455
+ <div class="flex items-center gap-2">
456
+ <span>Số dòng:</span>
457
+ <select x-model="limit" class="px-2 py-1 rounded-md glass-input focus:outline-none text-slate-300">
458
+ <option value="10">10</option>
459
+ <option value="25">25</option>
460
+ <option value="50">50</option>
461
+ <option value="100">100</option>
462
+ </select>
463
+ </div>
464
+
465
+ <!-- Pagination Controls -->
466
+ <div class="flex items-center gap-1.5">
467
+ <button
468
+ @click="prevPage()"
469
+ :disabled="page <= 1"
470
+ class="px-2.5 py-1.5 rounded-lg border border-slate-800 hover:bg-slate-800 disabled:opacity-40 disabled:hover:bg-transparent transition text-slate-300"
471
+ >
472
+ <i data-lucide="chevron-left" class="w-3.5 h-3.5"></i>
473
+ </button>
474
+ <span class="px-2 font-medium" x-text="'Trang ' + page + ' / ' + Math.ceil(totalRows / limit)"></span>
475
+ <button
476
+ @click="nextPage()"
477
+ :disabled="page >= Math.ceil(totalRows / limit)"
478
+ class="px-2.5 py-1.5 rounded-lg border border-slate-800 hover:bg-slate-800 disabled:opacity-40 disabled:hover:bg-transparent transition text-slate-300"
479
+ >
480
+ <i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
481
+ </button>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ </div>
486
+ </div>
487
+
488
+ <!-- Schema Tab -->
489
+ <div x-show="activeTab === 'schema'" class="space-y-6">
490
+ <div class="glass-card rounded-xl overflow-hidden">
491
+ <div class="px-5 py-3 border-b border-slate-800 bg-slate-900/30">
492
+ <h3 class="text-xs font-semibold text-slate-300 uppercase tracking-wide">Cấu trúc các cột</h3>
493
+ </div>
494
+ <div class="overflow-x-auto">
495
+ <table class="w-full text-left border-collapse text-xs">
496
+ <thead>
497
+ <tr class="bg-slate-900 border-b border-slate-800 text-slate-400 font-semibold uppercase tracking-wider">
498
+ <th class="px-5 py-3.5">Cột</th>
499
+ <th class="px-5 py-3.5">Kiểu dữ liệu</th>
500
+ <th class="px-5 py-3.5">Thuộc tính</th>
501
+ <th class="px-5 py-3.5">Giá trị mặc định</th>
502
+ </tr>
503
+ </thead>
504
+ <tbody class="divide-y divide-slate-800/50">
505
+ <template x-for="col in schema.columns" :key="col.name">
506
+ <tr class="hover:bg-slate-900/30 transition">
507
+ <td class="px-5 py-3 font-semibold text-slate-200">
508
+ <div class="flex items-center gap-2">
509
+ <span x-text="col.name"></span>
510
+ <template x-if="col.primaryKey">
511
+ <span class="px-1.5 py-0.5 text-[8px] bg-brand-950 text-brand-300 border border-brand-500/20 rounded font-bold uppercase">Primary Key</span>
512
+ </template>
513
+ </div>
514
+ </td>
515
+ <td class="px-5 py-3 font-mono text-xs text-brand-300" x-text="col.type || 'INTEGER/TEXT/BLOB'"></td>
516
+ <td class="px-5 py-3 text-slate-400">
517
+ <span class="px-1.5 py-0.5 rounded text-[10px] border border-slate-800" :class="col.notNull ? 'bg-rose-950/20 text-rose-400 border-rose-500/10' : 'bg-emerald-950/20 text-emerald-400 border-emerald-500/10'" x-text="col.notNull ? 'NOT NULL' : 'NULLABLE'"></span>
518
+ </td>
519
+ <td class="px-5 py-3 font-mono text-slate-400" x-text="col.defaultValue !== null ? col.defaultValue : 'NULL'"></td>
520
+ </tr>
521
+ </template>
522
+ </tbody>
523
+ </table>
524
+ </div>
525
+ </div>
526
+
527
+ <!-- Foreign Keys Section -->
528
+ <template x-if="schema.foreignKeys && schema.foreignKeys.length > 0">
529
+ <div class="glass-card rounded-xl overflow-hidden">
530
+ <div class="px-5 py-3 border-b border-slate-800 bg-slate-900/30">
531
+ <h3 class="text-xs font-semibold text-slate-300 uppercase tracking-wide">Khóa ngoại (Foreign Keys)</h3>
532
+ </div>
533
+ <div class="overflow-x-auto">
534
+ <table class="w-full text-left border-collapse text-xs">
535
+ <thead>
536
+ <tr class="bg-slate-900 border-b border-slate-800 text-slate-400 font-semibold uppercase tracking-wider">
537
+ <th class="px-5 py-3.5">Cột tham chiếu</th>
538
+ <th class="px-5 py-3.5">Bảng đích</th>
539
+ <th class="px-5 py-3.5">Cột đích</th>
540
+ <th class="px-5 py-3.5">On Update / Delete</th>
541
+ </tr>
542
+ </thead>
543
+ <tbody class="divide-y divide-slate-800/50">
544
+ <template x-for="fk in schema.foreignKeys" :key="fk.id + '-' + fk.seq">
545
+ <tr class="hover:bg-slate-900/30 transition text-slate-300">
546
+ <td class="px-5 py-3 font-mono font-semibold" x-text="fk.from"></td>
547
+ <td class="px-5 py-3 text-slate-200" x-text="fk.table"></td>
548
+ <td class="px-5 py-3 font-mono" x-text="fk.to"></td>
549
+ <td class="px-5 py-3 text-slate-400 font-mono text-[10px]" x-text="'ON UPDATE: ' + fk.onUpdate + ' | ON DELETE: ' + fk.onDelete"></td>
550
+ </tr>
551
+ </template>
552
+ </tbody>
553
+ </table>
554
+ </div>
555
+ </div>
556
+ </template>
557
+ </div>
558
+ </div>
559
+ </template>
560
+
561
+ <!-- 3. Welcome / Empty Screen -->
562
+ <template x-if="!isSQLEditorActive && !activeTable">
563
+ <div class="h-96 flex flex-col items-center justify-center text-center">
564
+ <div class="w-16 h-16 rounded-2xl bg-gradient-to-tr from-brand-600/20 to-indigo-500/20 border border-brand-500/30 flex items-center justify-center shadow-2xl mb-5 text-brand-400 animate-pulse">
565
+ <i data-lucide="database" class="w-8 h-8"></i>
566
+ </div>
567
+ <h2 class="text-lg font-bold text-white">Chào mừng tới trang quản trị SQLite</h2>
568
+ <p class="text-sm text-slate-400 mt-2 max-w-sm">Chọn một bảng từ thanh bên để xem cấu trúc và dữ liệu, hoặc sử dụng SQL Console để truy vấn.</p>
569
+ </div>
570
+ </template>
571
+ </div>
572
+ </main>
573
+
574
+ <!-- CRUD Modals -->
575
+
576
+ <!-- Add Record Modal -->
577
+ <div
578
+ x-show="addModalOpen"
579
+ class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm"
580
+ x-transition
581
+ style="display: none;"
582
+ >
583
+ <div @click.outside="closeAddModal()" class="w-full max-w-lg glass-card rounded-2xl overflow-hidden shadow-2xl">
584
+ <div class="px-6 py-4 border-b border-slate-800 bg-slate-900/50 flex items-center justify-between">
585
+ <h3 class="text-sm font-bold text-white flex items-center gap-2">
586
+ <i data-lucide="plus-circle" class="w-4 h-4 text-brand-400"></i> Thêm dòng dữ liệu mới
587
+ </h3>
588
+ <button @click="closeAddModal()" class="text-slate-400 hover:text-white transition">
589
+ <i data-lucide="x" class="w-5 h-5"></i>
590
+ </button>
591
+ </div>
592
+ <form @submit.prevent="submitAddForm()" class="p-6 space-y-4">
593
+ <div class="max-h-96 overflow-y-auto space-y-4 pr-1">
594
+ <template x-for="col in schema.columns" :key="col.name">
595
+ <div class="space-y-1.5">
596
+ <label class="block text-xs font-semibold text-slate-300">
597
+ <span x-text="col.name"></span>
598
+ <span class="text-[10px] text-slate-500 ml-1 font-mono" x-text="'(' + (col.type || 'TEXT') + ')'"></span>
599
+ <template x-if="col.primaryKey">
600
+ <span class="text-[9px] text-brand-400 font-bold ml-1">PK</span>
601
+ </template>
602
+ <template x-if="col.notNull">
603
+ <span class="text-rose-500 ml-0.5">*</span>
604
+ </template>
605
+ </label>
606
+
607
+ <!-- Inputs dynamically generated -->
608
+ <input
609
+ type="text"
610
+ x-model="formData[col.name]"
611
+ :placeholder="col.primaryKey ? 'Tự động tạo (nếu tự tăng)' : 'Nhập giá trị...'"
612
+ :disabled="col.primaryKey"
613
+ class="w-full px-3 py-2 text-xs rounded-xl glass-input text-slate-200 focus:outline-none disabled:opacity-40"
614
+ />
615
+ </div>
616
+ </template>
617
+ </div>
618
+ <div class="pt-4 border-t border-slate-800/80 flex items-center justify-end gap-3">
619
+ <button
620
+ type="button"
621
+ @click="closeAddModal()"
622
+ class="px-4 py-2 text-xs font-semibold text-slate-400 hover:text-white transition"
623
+ >
624
+ Hủy
625
+ </button>
626
+ <button
627
+ type="submit"
628
+ class="px-5 py-2.5 rounded-xl bg-brand-600 hover:bg-brand-500 font-semibold text-xs text-white transition shadow-lg shadow-brand-500/20"
629
+ >
630
+ Thêm dòng
631
+ </button>
632
+ </div>
633
+ </form>
634
+ </div>
635
+ </div>
636
+
637
+ <!-- Edit Record Modal -->
638
+ <div
639
+ x-show="editModalOpen"
640
+ class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm"
641
+ x-transition
642
+ style="display: none;"
643
+ >
644
+ <div @click.outside="closeEditModal()" class="w-full max-w-lg glass-card rounded-2xl overflow-hidden shadow-2xl">
645
+ <div class="px-6 py-4 border-b border-slate-800 bg-slate-900/50 flex items-center justify-between">
646
+ <h3 class="text-sm font-bold text-white flex items-center gap-2">
647
+ <i data-lucide="edit-3" class="w-4 h-4 text-brand-400"></i> Cập nhật thông tin dòng
648
+ </h3>
649
+ <button @click="closeEditModal()" class="text-slate-400 hover:text-white transition">
650
+ <i data-lucide="x" class="w-5 h-5"></i>
651
+ </button>
652
+ </div>
653
+ <form @submit.prevent="submitEditForm()" class="p-6 space-y-4">
654
+ <div class="max-h-96 overflow-y-auto space-y-4 pr-1">
655
+ <template x-for="col in schema.columns" :key="col.name">
656
+ <div class="space-y-1.5">
657
+ <label class="block text-xs font-semibold text-slate-300">
658
+ <span x-text="col.name"></span>
659
+ <span class="text-[10px] text-slate-500 ml-1 font-mono" x-text="'(' + (col.type || 'TEXT') + ')'"></span>
660
+ <template x-if="col.primaryKey">
661
+ <span class="text-[9px] text-brand-400 font-bold ml-1">PK (Khóa)</span>
662
+ </template>
663
+ <template x-if="col.notNull">
664
+ <span class="text-rose-500 ml-0.5">*</span>
665
+ </template>
666
+ </label>
667
+
668
+ <!-- Inputs -->
669
+ <input
670
+ type="text"
671
+ x-model="formData[col.name]"
672
+ :disabled="col.primaryKey"
673
+ class="w-full px-3 py-2 text-xs rounded-xl glass-input text-slate-200 focus:outline-none disabled:opacity-40"
674
+ />
675
+ </div>
676
+ </template>
677
+ </div>
678
+ <div class="pt-4 border-t border-slate-800/80 flex items-center justify-end gap-3">
679
+ <button
680
+ type="button"
681
+ @click="closeEditModal()"
682
+ class="px-4 py-2 text-xs font-semibold text-slate-400 hover:text-white transition"
683
+ >
684
+ Hủy
685
+ </button>
686
+ <button
687
+ type="submit"
688
+ class="px-5 py-2.5 rounded-xl bg-brand-600 hover:bg-brand-500 font-semibold text-xs text-white transition shadow-lg shadow-brand-500/20"
689
+ >
690
+ Lưu thay đổi
691
+ </button>
692
+ </div>
693
+ </form>
694
+ </div>
695
+ </div>
696
+
697
+ <!-- Delete Record Confirmation Modal -->
698
+ <div
699
+ x-show="deleteModalOpen"
700
+ class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/80 backdrop-blur-sm"
701
+ x-transition
702
+ style="display: none;"
703
+ >
704
+ <div @click.outside="closeDeleteModal()" class="w-full max-w-sm glass-card rounded-2xl overflow-hidden shadow-2xl border-rose-500/20">
705
+ <div class="p-6 text-center space-y-4">
706
+ <div class="w-12 h-12 rounded-full bg-rose-950/60 border border-rose-500/30 text-rose-400 flex items-center justify-center mx-auto mb-2">
707
+ <i data-lucide="alert-triangle" class="w-6 h-6"></i>
708
+ </div>
709
+ <h3 class="text-sm font-bold text-white">Xác nhận xóa dữ liệu</h3>
710
+ <p class="text-xs text-slate-400 leading-relaxed">Hành động này không thể hoàn tác. Bạn có chắc chắn muốn xóa dòng dữ liệu đang chọn khỏi cơ sở dữ liệu?</p>
711
+
712
+ <div class="flex items-center justify-center gap-3 pt-2">
713
+ <button
714
+ type="button"
715
+ @click="closeDeleteModal()"
716
+ class="px-4 py-2.5 text-xs font-semibold text-slate-400 hover:text-white transition"
717
+ >
718
+ Hủy bỏ
719
+ </button>
720
+ <button
721
+ type="button"
722
+ @click="submitDelete()"
723
+ class="px-5 py-2.5 rounded-xl bg-rose-600 hover:bg-rose-500 font-semibold text-xs text-white transition"
724
+ >
725
+ Chắc chắn xóa
726
+ </button>
727
+ </div>
728
+ </div>
729
+ </div>
730
+ </div>
731
+
732
+ <!-- Script logics -->
733
+ <script>
734
+ function appState() {
735
+ return {
736
+ basePath: '${cleanedBasePath}',
737
+ tables: [],
738
+ tableSearchQuery: '',
739
+ activeTable: null,
740
+ isSQLEditorActive: false,
741
+ activeTab: 'browse',
742
+
743
+ // Data states
744
+ schema: { columns: [], foreignKeys: [] },
745
+ rows: [],
746
+ totalRows: 0,
747
+ page: 1,
748
+ limit: 10,
749
+ searchQuery: '',
750
+ loading: false,
751
+ isReadOnly: false,
752
+
753
+ // SQL Editor state
754
+ sqlQuery: '',
755
+ queryResult: { rows: null, columns: [], error: null },
756
+
757
+ // CRUD Modals and form state
758
+ addModalOpen: false,
759
+ editModalOpen: false,
760
+ deleteModalOpen: false,
761
+ formData: {},
762
+ selectedRow: null, // Used for edit/delete reference
763
+
764
+ // Toasts
765
+ toasts: [],
766
+
767
+ init() {
768
+ this.fetchTables();
769
+ // Event listener for Ctrl+Enter in SQL Editor
770
+ window.addEventListener('keydown', (e) => {
771
+ if (this.isSQLEditorActive && (e.ctrlKey || e.metaKey) && e.key === 'Enter') {
772
+ this.runSqlQuery();
773
+ }
774
+ });
775
+
776
+ this.$watch('searchQuery', () => {
777
+ if (this.activeTable) {
778
+ this.page = 1;
779
+ this.fetchTableData();
780
+ }
781
+ });
782
+
783
+ this.$watch('limit', () => {
784
+ if (this.activeTable) {
785
+ this.page = 1;
786
+ this.fetchTableData();
787
+ }
788
+ });
789
+
790
+ this.$nextTick(() => {
791
+ lucide.createIcons();
792
+ });
793
+ },
794
+
795
+ get filteredTables() {
796
+ if (!this.tableSearchQuery.trim()) return this.tables;
797
+ const query = this.tableSearchQuery.toLowerCase();
798
+ return this.tables.filter(t => t.toLowerCase().includes(query));
799
+ },
800
+
801
+ // Toast notifications
802
+ showToast(title, message, type = 'success') {
803
+ const id = Date.now();
804
+ this.toasts.push({ id, title, message, type });
805
+ setTimeout(() => {
806
+ this.removeToast(id);
807
+ }, 4000);
808
+ },
809
+
810
+ removeToast(id) {
811
+ this.toasts = this.toasts.filter(t => t.id !== id);
812
+ },
813
+
814
+ // API calls
815
+ async fetchTables() {
816
+ this.loading = true;
817
+ try {
818
+ const res = await fetch(`${this.basePath}/api/tables`);
819
+ const data = await res.json();
820
+ if (data.success) {
821
+ this.tables = data.tables;
822
+ this.isReadOnly = data.readOnly;
823
+ } else {
824
+ this.showToast('Lỗi tải bảng', data.error || 'Không rõ lý do', 'error');
825
+ }
826
+ } catch (err) {
827
+ this.showToast('Lỗi mạng', err.message, 'error');
828
+ } finally {
829
+ this.loading = false;
830
+ this.$nextTick(() => { lucide.createIcons(); });
831
+ }
832
+ },
833
+
834
+ async fetchTableData() {
835
+ if (!this.activeTable) return;
836
+ this.loading = true;
837
+ try {
838
+ const url = `${this.basePath}/api/tables/${encodeURIComponent(this.activeTable)}?page=${this.page}&limit=${this.limit}&search=${encodeURIComponent(this.searchQuery)}`;
839
+ const res = await fetch(url);
840
+ const data = await res.json();
841
+ if (data.success) {
842
+ this.schema = data.info;
843
+ this.rows = data.rows;
844
+ this.totalRows = data.total;
845
+ } else {
846
+ this.showToast('Lỗi dữ liệu', data.error, 'error');
847
+ }
848
+ } catch (err) {
849
+ this.showToast('Lỗi mạng', err.message, 'error');
850
+ } finally {
851
+ this.loading = false;
852
+ this.$nextTick(() => { lucide.createIcons(); });
853
+ }
854
+ },
855
+
856
+ selectTable(table) {
857
+ this.isSQLEditorActive = false;
858
+ this.activeTable = table;
859
+ this.page = 1;
860
+ this.searchQuery = '';
861
+ this.activeTab = 'browse';
862
+ this.queryResult = { rows: null, columns: [], error: null };
863
+ this.fetchTableData();
864
+ },
865
+
866
+ selectSQLEditor() {
867
+ this.isSQLEditorActive = true;
868
+ this.activeTable = null;
869
+ this.$nextTick(() => { lucide.createIcons(); });
870
+ },
871
+
872
+ refreshData() {
873
+ if (this.isSQLEditorActive) {
874
+ if (this.sqlQuery.trim()) this.runSqlQuery();
875
+ } else if (this.activeTable) {
876
+ this.fetchTableData();
877
+ } else {
878
+ this.fetchTables();
879
+ }
880
+ },
881
+
882
+ prevPage() {
883
+ if (this.page > 1) {
884
+ this.page--;
885
+ this.fetchTableData();
886
+ }
887
+ },
888
+
889
+ nextPage() {
890
+ if (this.page < Math.ceil(this.totalRows / this.limit)) {
891
+ this.page++;
892
+ this.fetchTableData();
893
+ }
894
+ },
895
+
896
+ formatCellValue(val) {
897
+ if (val === null || val === undefined) return 'NULL';
898
+ if (typeof val === 'object') return JSON.stringify(val);
899
+ return val.toString();
900
+ },
901
+
902
+ // SQL Query methods
903
+ async runSqlQuery() {
904
+ if (!this.sqlQuery.trim()) return;
905
+ this.loading = true;
906
+ const startTime = performance.now();
907
+ try {
908
+ const res = await fetch(`${this.basePath}/api/query`, {
909
+ method: 'POST',
910
+ headers: { 'Content-Type': 'application/json' },
911
+ body: JSON.stringify({ sql: this.sqlQuery })
912
+ });
913
+ const data = await res.json();
914
+ const timeMs = Math.round(performance.now() - startTime);
915
+
916
+ if (data.success) {
917
+ this.queryResult = {
918
+ rows: data.rows || null,
919
+ columns: data.columns || [],
920
+ affectedRows: data.affectedRows,
921
+ error: null,
922
+ timeMs
923
+ };
924
+ this.showToast('Truy vấn hoàn thành', `Thành công sau ${timeMs}ms`);
925
+ // Reload sidebar if a write happened
926
+ if (data.affectedRows !== undefined) {
927
+ this.fetchTables();
928
+ }
929
+ } else {
930
+ this.queryResult = {
931
+ rows: null,
932
+ columns: [],
933
+ error: data.error,
934
+ timeMs
935
+ };
936
+ this.showToast('Lỗi truy vấn', 'Vui lòng kiểm tra lại câu lệnh', 'error');
937
+ }
938
+ } catch (err) {
939
+ this.queryResult = { rows: null, columns: [], error: err.message, timeMs: 0 };
940
+ this.showToast('Lỗi kết nối', err.message, 'error');
941
+ } finally {
942
+ this.loading = false;
943
+ this.$nextTick(() => { lucide.createIcons(); });
944
+ }
945
+ },
946
+
947
+ clearSqlQuery() {
948
+ this.sqlQuery = '';
949
+ this.queryResult = { rows: null, columns: [], error: null };
950
+ },
951
+
952
+ // CRUD UI Actions
953
+ openAddModal() {
954
+ if (this.isReadOnly) return;
955
+ this.formData = {};
956
+ // Initialize empty values matching schema
957
+ this.schema.columns.forEach(col => {
958
+ this.formData[col.name] = '';
959
+ });
960
+ this.addModalOpen = true;
961
+ this.$nextTick(() => { lucide.createIcons(); });
962
+ },
963
+
964
+ closeAddModal() {
965
+ this.addModalOpen = false;
966
+ },
967
+
968
+ async submitAddForm() {
969
+ this.loading = true;
970
+ // Filter primary key column from payload if empty (to let SQLite autoincrement work)
971
+ const payload = {};
972
+ this.schema.columns.forEach(col => {
973
+ const val = this.formData[col.name];
974
+ if (col.primaryKey && (val === '' || val === null || val === undefined)) {
975
+ // Skip primary keys to let database autoincrement them
976
+ return;
977
+ }
978
+
979
+ // Type conversion basics
980
+ if (val === '') {
981
+ payload[col.name] = col.notNull ? '' : null;
982
+ } else if (col.type && (col.type.toUpperCase().includes('INT') || col.type.toUpperCase().includes('NUM'))) {
983
+ payload[col.name] = val.includes('.') ? parseFloat(val) : parseInt(val, 10);
984
+ } else {
985
+ payload[col.name] = val;
986
+ }
987
+ });
988
+
989
+ try {
990
+ const res = await fetch(`${this.basePath}/api/tables/${encodeURIComponent(this.activeTable)}`, {
991
+ method: 'POST',
992
+ headers: { 'Content-Type': 'application/json' },
993
+ body: JSON.stringify({ data: payload })
994
+ });
995
+ const data = await res.json();
996
+ if (data.success) {
997
+ this.showToast('Thành công', 'Đã thêm 1 dòng dữ liệu mới');
998
+ this.closeAddModal();
999
+ this.fetchTableData();
1000
+ } else {
1001
+ this.showToast('Lỗi ghi dữ liệu', data.error, 'error');
1002
+ }
1003
+ } catch (err) {
1004
+ this.showToast('Lỗi kết nối', err.message, 'error');
1005
+ } finally {
1006
+ this.loading = false;
1007
+ }
1008
+ },
1009
+
1010
+ openEditModal(row) {
1011
+ if (this.isReadOnly) return;
1012
+ this.selectedRow = row;
1013
+ this.formData = { ...row };
1014
+ this.editModalOpen = true;
1015
+ this.$nextTick(() => { lucide.createIcons(); });
1016
+ },
1017
+
1018
+ closeEditModal() {
1019
+ this.editModalOpen = false;
1020
+ this.selectedRow = null;
1021
+ },
1022
+
1023
+ async submitEditForm() {
1024
+ this.loading = true;
1025
+
1026
+ // Separate PK and update values
1027
+ const pk = {};
1028
+ const updateData = {};
1029
+
1030
+ this.schema.columns.forEach(col => {
1031
+ const currentVal = this.formData[col.name];
1032
+
1033
+ // Parse numerical columns
1034
+ let finalVal = currentVal;
1035
+ if (currentVal !== '' && currentVal !== null && currentVal !== undefined) {
1036
+ if (col.type && (col.type.toUpperCase().includes('INT') || col.type.toUpperCase().includes('NUM'))) {
1037
+ finalVal = currentVal.toString().includes('.') ? parseFloat(currentVal) : parseInt(currentVal, 10);
1038
+ }
1039
+ } else {
1040
+ finalVal = col.notNull ? '' : null;
1041
+ }
1042
+
1043
+ if (col.primaryKey) {
1044
+ // PK criteria remains unchanged from selected row
1045
+ pk[col.name] = this.selectedRow[col.name];
1046
+ } else {
1047
+ updateData[col.name] = finalVal;
1048
+ }
1049
+ });
1050
+
1051
+ // If table has no defined PK, use the entire selected row as identification criteria
1052
+ if (Object.keys(pk).length === 0) {
1053
+ Object.assign(pk, this.selectedRow);
1054
+ }
1055
+
1056
+ try {
1057
+ const res = await fetch(`${this.basePath}/api/tables/${encodeURIComponent(this.activeTable)}`, {
1058
+ method: 'PUT',
1059
+ headers: { 'Content-Type': 'application/json' },
1060
+ body: JSON.stringify({ pk, data: updateData })
1061
+ });
1062
+ const data = await res.json();
1063
+ if (data.success) {
1064
+ this.showToast('Thành công', 'Đã lưu các thay đổi');
1065
+ this.closeEditModal();
1066
+ this.fetchTableData();
1067
+ } else {
1068
+ this.showToast('Lỗi cập nhật', data.error, 'error');
1069
+ }
1070
+ } catch (err) {
1071
+ this.showToast('Lỗi kết nối', err.message, 'error');
1072
+ } finally {
1073
+ this.loading = false;
1074
+ }
1075
+ },
1076
+
1077
+ openDeleteModal(row) {
1078
+ if (this.isReadOnly) return;
1079
+ this.selectedRow = row;
1080
+ this.deleteModalOpen = true;
1081
+ this.$nextTick(() => { lucide.createIcons(); });
1082
+ },
1083
+
1084
+ closeDeleteModal() {
1085
+ this.deleteModalOpen = false;
1086
+ this.selectedRow = null;
1087
+ },
1088
+
1089
+ async submitDelete() {
1090
+ this.loading = true;
1091
+
1092
+ // Formulate primary key identifier
1093
+ const pk = {};
1094
+ this.schema.columns.forEach(col => {
1095
+ if (col.primaryKey) {
1096
+ pk[col.name] = this.selectedRow[col.name];
1097
+ }
1098
+ });
1099
+
1100
+ // If no PK, use whole row
1101
+ if (Object.keys(pk).length === 0) {
1102
+ Object.assign(pk, this.selectedRow);
1103
+ }
1104
+
1105
+ try {
1106
+ const res = await fetch(`${this.basePath}/api/tables/${encodeURIComponent(this.activeTable)}`, {
1107
+ method: 'DELETE',
1108
+ headers: { 'Content-Type': 'application/json' },
1109
+ body: JSON.stringify({ pk })
1110
+ });
1111
+ const data = await res.json();
1112
+ if (data.success) {
1113
+ this.showToast('Đã xóa', 'Dòng dữ liệu đã được gỡ bỏ khỏi bảng');
1114
+ this.closeDeleteModal();
1115
+ this.fetchTableData();
1116
+ } else {
1117
+ this.showToast('Lỗi xóa', data.error, 'error');
1118
+ }
1119
+ } catch (err) {
1120
+ this.showToast('Lỗi kết nối', err.message, 'error');
1121
+ } finally {
1122
+ this.loading = false;
1123
+ }
1124
+ }
1125
+ };
1126
+ }
1127
+ </script>
1128
+ </body>
1129
+ </html>"""
1130
+
1131
+ return html.replace('${cleanedBasePath}', cleaned_base_path)