basic db editor admin page
This commit is contained in:
parent
3c3e3b7ae4
commit
3b67f50518
17
main.go
17
main.go
|
|
@ -29,7 +29,9 @@ func main() {
|
|||
r := gin.Default()
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.Status(200)
|
||||
uiTemplate.Execute(c.Writer, nil)
|
||||
if err := uiTemplate.Execute(c.Writer, nil); err != nil {
|
||||
logrus.Errorf("Failed to execute ui.html template: %v", err)
|
||||
}
|
||||
})
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.Status(200)
|
||||
|
|
@ -37,5 +39,18 @@ func main() {
|
|||
})
|
||||
r.POST("/chat", chatService.HandleChat)
|
||||
|
||||
if err := loadDBEditTemplate("ui_dbedit.html"); err != nil {
|
||||
logrus.Fatalf("Failed to load ui_dbedit.html: %v", err)
|
||||
}
|
||||
r.GET("/admin", func(c *gin.Context) {
|
||||
c.Status(200)
|
||||
if err := uiDBEditTemplate.Execute(c.Writer, nil); err != nil {
|
||||
logrus.Errorf("Failed to execute ui_dbedit.html template: %v", err)
|
||||
}
|
||||
})
|
||||
r.GET("/db.yaml", func(c *gin.Context) {
|
||||
c.File("db.yaml")
|
||||
})
|
||||
|
||||
r.Run(":8080")
|
||||
}
|
||||
|
|
|
|||
10
ui.go
10
ui.go
|
|
@ -5,6 +5,7 @@ import (
|
|||
)
|
||||
|
||||
var uiTemplate *template.Template
|
||||
var uiDBEditTemplate *template.Template
|
||||
|
||||
func loadUITemplate(path string) error {
|
||||
tmpl, err := template.ParseFiles(path)
|
||||
|
|
@ -14,3 +15,12 @@ func loadUITemplate(path string) error {
|
|||
uiTemplate = tmpl
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadDBEditTemplate(path string) error {
|
||||
tmpl, err := template.ParseFiles(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uiDBEditTemplate = tmpl
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Edit db.yaml</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 2em; }
|
||||
#editor { max-width: 900px; margin: 0 auto; }
|
||||
textarea { width: 100%; height: 300px; margin-bottom: 1em; }
|
||||
button { padding: 0.5em 1em; margin-right: 1em; }
|
||||
#status { color: #007a3d; margin-bottom: 1em; }
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="editor">
|
||||
<h2>Edit db.yaml</h2>
|
||||
<div id="status"></div>
|
||||
<button id="load">Load db.yaml</button>
|
||||
<button id="add">Add Entry</button>
|
||||
<button id="download">Download YAML</button>
|
||||
<textarea id="yamlArea" placeholder="YAML will appear here..."></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');
|
||||
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);
|
||||
};
|
||||
|
||||
function renderForm() {
|
||||
const formArea = document.getElementById('formArea');
|
||||
formArea.innerHTML = '';
|
||||
if (!Array.isArray(data)) return;
|
||||
data.forEach((entry, idx) => {
|
||||
const div = document.createElement('div');
|
||||
div.style.border = '1px solid #ccc';
|
||||
div.style.padding = '1em';
|
||||
div.style.marginBottom = '1em';
|
||||
div.innerHTML = `
|
||||
<b>Entry #${idx+1}</b> <button onclick="removeEntry(${idx})">Remove</button><br>
|
||||
<label>ID: <input type="text" value="${entry.id||''}" onchange="updateEntry(${idx}, 'id', this.value)"></label><br>
|
||||
<label>Visit:<br><textarea rows="4" style="width:100%" onchange="updateEntry(${idx}, 'visit', this.value)">${entry.visit||''}</textarea></label><br>
|
||||
<label>Keywords:</label>
|
||||
<div id="keywords${idx}" style="margin-bottom:0.5em;"></div>
|
||||
<input type="text" id="keywordInput${idx}" placeholder="Add keyword..." style="width:200px;">
|
||||
<button type="button" onclick="addKeyword(${idx})">Add</button><br>
|
||||
<label>Notes:<br><textarea rows="2" style="width:100%;height:48px;resize:vertical;" onchange="updateEntry(${idx}, 'notes', this.value)">${entry.notes||''}</textarea></label><br>
|
||||
<b>Procedures:</b><br>
|
||||
<div id="procedures${idx}"></div>
|
||||
<button onclick="addProcedure(${idx})">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});
|
||||
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.style.border = '1px solid #aaa';
|
||||
span.style.padding = '2px 8px';
|
||||
span.style.marginRight = '4px';
|
||||
span.style.background = '#f0f0f0';
|
||||
span.style.borderRadius = '12px';
|
||||
span.style.display = 'inline-block';
|
||||
span.style.marginBottom = '2px';
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = 'x';
|
||||
btn.style.marginLeft = '4px';
|
||||
btn.style.fontSize = '10px';
|
||||
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.style.marginBottom = '0.5em';
|
||||
pdiv.innerHTML = `
|
||||
<label>Name: <input type="text" value="${proc.name||''}" onchange="updateProcedure(${entryIdx},${procIdx},'name',this.value)"></label>
|
||||
<label>Price: <input type="number" value="${proc.price||0}" onchange="updateProcedure(${entryIdx},${procIdx},'price',this.value)"></label>
|
||||
<label>Duration (min): <input type="number" value="${proc.duration_minutes||0}" onchange="updateProcedure(${entryIdx},${procIdx},'duration_minutes',this.value)"></label>
|
||||
<button onclick="removeProcedure(${entryIdx},${procIdx})">Remove</button>
|
||||
`;
|
||||
procDiv.appendChild(pdiv);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
Loading…
Reference in New Issue