vetrag/web_templates/ui_dbedit.html

244 lines
11 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Edit db.yaml</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
</head>
<body class="bg-gray-50 min-h-screen flex items-center justify-center">
<div id="editor" class="w-full max-w-2xl mx-auto bg-white rounded-xl shadow-lg p-6">
<h2 class="text-2xl font-bold mb-4 text-center text-green-700">Edit db.yaml</h2>
<div id="status" class="mb-2 text-green-700"></div>
<div class="flex flex-wrap gap-2 mb-4">
<button id="load" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">Load db.yaml</button>
<button id="add" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition">Add Entry</button>
<button id="download" class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition">Download YAML</button>
<button id="saveSnapshot" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition">Save Snapshot</button>
<select id="snapshotSelect" class="px-4 py-2 bg-yellow-100 border border-yellow-400 rounded-lg text-yellow-900">
<option value="">Load from snapshot...</option>
</select>
</div>
<textarea id="yamlArea" placeholder="YAML will appear here..." class="w-full h-40 mb-4 p-2 border border-gray-300 rounded-lg bg-gray-100 text-sm"></textarea>
<div id="formArea"></div>
</div>
<script>
const yamlArea = document.getElementById('yamlArea');
const status = document.getElementById('status');
const loadBtn = document.getElementById('load');
const addBtn = document.getElementById('add');
const downloadBtn = document.getElementById('download');
const saveSnapshotBtn = document.getElementById('saveSnapshot');
const snapshotSelect = document.getElementById('snapshotSelect');
let data = [];
loadBtn.onclick = async function() {
status.textContent = 'Loading...';
const resp = await fetch('/db.yaml');
if (!resp.ok) { status.textContent = 'Failed to load db.yaml'; return; }
const text = await resp.text();
yamlArea.value = text;
try {
data = jsyaml.load(text);
status.textContent = 'Loaded!';
renderForm();
} catch (e) {
status.textContent = 'YAML parse error!';
}
};
addBtn.onclick = function() {
if (!Array.isArray(data)) data = [];
data.push({id: '', visit: '', keywords: [], procedures: [], notes: ''});
renderForm();
yamlArea.value = jsyaml.dump(data);
};
downloadBtn.onclick = function() {
const blob = new Blob([yamlArea.value], {type: 'text/yaml'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'db.yaml';
a.click();
URL.revokeObjectURL(url);
};
saveSnapshotBtn.onclick = async function() {
const text = yamlArea.value;
status.textContent = 'Saving snapshot...';
const resp = await fetch('/admin/chats/snapshot', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({text})
});
const result = await resp.json();
if (result.status === 'ok') {
status.textContent = 'Snapshot saved!';
loadSnapshots(); // Refresh snapshot list
} else {
status.textContent = 'Error: ' + (result.error || 'Unknown error');
}
};
async function loadSnapshots() {
const resp = await fetch('/admin/chats/snapshots');
if (!resp.ok) return;
const result = await resp.json();
snapshotSelect.innerHTML = '<option value="">Load from snapshot...</option>';
(result.items || []).forEach(s => {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = `#${s.id} - ${new Date(s.created_at).toLocaleString()}`;
snapshotSelect.appendChild(opt);
});
}
snapshotSelect.onchange = async function() {
const id = snapshotSelect.value;
if (!id) return;
status.textContent = 'Loading snapshot...';
const resp = await fetch(`/admin/chats/snapshot/${id}`);
if (!resp.ok) { status.textContent = 'Failed to load snapshot'; return; }
const text = await resp.text();
yamlArea.value = text;
try {
data = jsyaml.load(text);
status.textContent = 'Snapshot loaded!';
renderForm();
} catch (e) {
status.textContent = 'YAML parse error!';
}
};
function renderForm() {
const formArea = document.getElementById('formArea');
formArea.innerHTML = '';
if (!Array.isArray(data)) return;
data.forEach((entry, idx) => {
const div = document.createElement('div');
div.className = "border border-gray-300 rounded-lg p-4 mb-6 bg-gray-50";
div.innerHTML = `
<div class="flex items-center justify-between mb-2">
<span class="font-semibold text-lg">Entry #${idx+1}</span>
<button type="button" onclick="removeEntry(${idx})" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600">Remove</button>
</div>
<label class="block mb-2">ID:
<input type="text" value="${entry.id||''}" onchange="updateEntry(${idx}, 'id', this.value)" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-lg" />
</label>
<label class="block mb-2">Visit:<br>
<textarea rows="4" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-lg" onchange="updateEntry(${idx}, 'visit', this.value)">${entry.visit||''}</textarea>
</label>
<label class="block mb-2">Keywords:</label>
<div id="keywords${idx}" class="flex flex-wrap gap-2 mb-2"></div>
<div class="flex gap-2 mb-2">
<input type="text" id="keywordInput${idx}" placeholder="Add keyword..." class="px-3 py-2 border border-gray-300 rounded-lg" style="width:200px;">
<button type="button" onclick="addKeyword(${idx})" class="px-3 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">Add</button>
</div>
<label class="block mb-2">Notes:<br>
<textarea rows="2" style="height:48px;resize:vertical;" class="mt-1 w-full px-3 py-2 border border-gray-300 rounded-lg" onchange="updateEntry(${idx}, 'notes', this.value)">${entry.notes||''}</textarea>
</label>
<b class="block mb-2">Procedures:</b>
<div id="procedures${idx}" class="mb-2"></div>
<button onclick="addProcedure(${idx})" class="px-3 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600">Add Procedure</button>
`;
formArea.appendChild(div);
renderKeywords(idx, entry.keywords||[]);
renderProcedures(idx, entry.procedures||[]);
});
yamlArea.value = jsyaml.dump(data);
}
window.updateEntry = function(idx, field, value) {
if (field === 'keywords') {
data[idx][field] = value.split(',').map(s => s.trim()).filter(Boolean);
} else {
data[idx][field] = value;
}
yamlArea.value = jsyaml.dump(data);
renderForm();
};
window.removeEntry = function(idx) {
data.splice(idx, 1);
renderForm();
yamlArea.value = jsyaml.dump(data);
};
window.addProcedure = function(idx) {
if (!Array.isArray(data[idx].procedures)) data[idx].procedures = [];
data[idx].procedures.push({name: '', price: 0, duration_minutes: 0, note: ''});
renderForm();
yamlArea.value = jsyaml.dump(data);
};
window.updateProcedure = function(entryIdx, procIdx, field, value) {
if (field === 'price' || field === 'duration_minutes') {
data[entryIdx].procedures[procIdx][field] = Number(value);
} else {
data[entryIdx].procedures[procIdx][field] = value;
}
yamlArea.value = jsyaml.dump(data);
renderForm();
};
window.removeProcedure = function(entryIdx, procIdx) {
data[entryIdx].procedures.splice(procIdx, 1);
renderForm();
yamlArea.value = jsyaml.dump(data);
};
window.addKeyword = function(idx) {
const input = document.getElementById('keywordInput'+idx);
const val = input.value.trim();
if (val && !data[idx].keywords.includes(val)) {
data[idx].keywords.push(val);
input.value = '';
yamlArea.value = jsyaml.dump(data);
renderForm();
}
};
window.removeKeyword = function(idx, kwIdx) {
data[idx].keywords.splice(kwIdx, 1);
yamlArea.value = jsyaml.dump(data);
renderForm();
};
function renderKeywords(idx, keywords) {
const kwDiv = document.getElementById('keywords'+idx);
if (!kwDiv) return;
kwDiv.innerHTML = '';
keywords.forEach((kw, kwIdx) => {
const span = document.createElement('span');
span.textContent = kw;
span.className = "inline-flex items-center bg-green-100 text-green-800 border border-green-300 rounded-full px-3 py-1 text-sm";
const btn = document.createElement('button');
btn.textContent = 'x';
btn.className = "ml-2 text-xs bg-red-400 text-white rounded-full px-2 py-0.5 hover:bg-red-500";
btn.onclick = function() { removeKeyword(idx, kwIdx); };
span.appendChild(btn);
kwDiv.appendChild(span);
});
}
function renderProcedures(entryIdx, procedures) {
const procDiv = document.getElementById('procedures'+entryIdx);
if (!procDiv) return;
procDiv.innerHTML = '';
procedures.forEach((proc, procIdx) => {
const pdiv = document.createElement('div');
pdiv.className = "mb-2 p-2 border border-gray-200 rounded-lg bg-white";
pdiv.innerHTML = `
<label class="mr-2">Name: <input type="text" value="${proc.name||''}" onchange="updateProcedure(${entryIdx},${procIdx},'name',this.value)" class="px-2 py-1 border border-gray-300 rounded-lg" /></label>
<label class="mr-2">Price: <input type="number" value="${proc.price||0}" onchange="updateProcedure(${entryIdx},${procIdx},'price',this.value)" class="px-2 py-1 border border-gray-300 rounded-lg w-20" /></label>
<label class="mr-2">Duration (min): <input type="number" value="${proc.duration_minutes||0}" onchange="updateProcedure(${entryIdx},${procIdx},'duration_minutes',this.value)" class="px-2 py-1 border border-gray-300 rounded-lg w-20" /></label>
<label class="block mt-2">Note:<br><input type="text" value="${proc.note||''}" onchange="updateProcedure(${entryIdx},${procIdx},'note',this.value)" class="mt-1 w-full px-2 py-1 border border-gray-300 rounded-lg text-sm" placeholder="Short note..." /></label>
<button onclick="removeProcedure(${entryIdx},${procIdx})" class="px-2 py-1 bg-red-500 text-white rounded hover:bg-red-600 mt-2">Remove</button>
`;
procDiv.appendChild(pdiv);
});
}
// Load snapshot list on page load
loadSnapshots();
</script>
</body>
</html>