342 lines
13 KiB
JavaScript
342 lines
13 KiB
JavaScript
/* FluentGerman.ai — Admin panel logic */
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (!requireAuth() || !requireAdmin()) return;
|
|
|
|
const user = getUser();
|
|
document.getElementById('admin-name').textContent = user?.username || 'Admin';
|
|
|
|
// Tab switching
|
|
const tabs = document.querySelectorAll('.tab');
|
|
const panels = document.querySelectorAll('.tab-panel');
|
|
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('click', () => {
|
|
tabs.forEach(t => t.classList.remove('active'));
|
|
panels.forEach(p => p.classList.add('hidden'));
|
|
tab.classList.add('active');
|
|
document.getElementById(tab.dataset.panel).classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
// ── Users ──────────────────────────────────────────────────────
|
|
const usersBody = document.getElementById('users-body');
|
|
const userModal = document.getElementById('user-modal');
|
|
const userForm = document.getElementById('user-form');
|
|
let editingUserId = null;
|
|
|
|
async function loadUsers() {
|
|
try {
|
|
const users = await apiJSON('/users/');
|
|
usersBody.innerHTML = '';
|
|
|
|
if (users.length === 0) {
|
|
usersBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No clients yet</td></tr>';
|
|
return;
|
|
}
|
|
|
|
users.forEach(u => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${u.username}</td>
|
|
<td class="hide-mobile">${u.email}</td>
|
|
<td><span class="badge ${u.is_active ? 'badge-personal' : 'badge-homework'}">${u.is_active ? 'Active' : 'Inactive'}</span></td>
|
|
<td class="hide-mobile">${new Date(u.created_at).toLocaleDateString()}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-secondary" onclick="editUser(${u.id})">Edit</button>
|
|
<button class="btn btn-sm btn-secondary" onclick="manageInstructions(${u.id}, '${u.username}')">📝</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteUser(${u.id})">✕</button>
|
|
</td>
|
|
`;
|
|
usersBody.appendChild(row);
|
|
});
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
}
|
|
|
|
window.showUserModal = (editing = false) => {
|
|
editingUserId = null;
|
|
document.getElementById('user-modal-title').textContent = 'Add Client';
|
|
userForm.reset();
|
|
document.getElementById('user-password').required = true;
|
|
userModal.classList.remove('hidden');
|
|
};
|
|
|
|
window.closeUserModal = () => {
|
|
userModal.classList.add('hidden');
|
|
editingUserId = null;
|
|
};
|
|
|
|
window.editUser = async (id) => {
|
|
try {
|
|
const users = await apiJSON('/users/');
|
|
const u = users.find(x => x.id === id);
|
|
if (!u) return;
|
|
|
|
editingUserId = id;
|
|
document.getElementById('user-modal-title').textContent = 'Edit Client';
|
|
document.getElementById('user-username').value = u.username;
|
|
document.getElementById('user-email').value = u.email;
|
|
document.getElementById('user-password').value = '';
|
|
document.getElementById('user-password').required = false;
|
|
userModal.classList.remove('hidden');
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
};
|
|
|
|
userForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const data = {
|
|
username: document.getElementById('user-username').value.trim(),
|
|
email: document.getElementById('user-email').value.trim(),
|
|
};
|
|
const password = document.getElementById('user-password').value;
|
|
if (password) data.password = password;
|
|
|
|
try {
|
|
if (editingUserId) {
|
|
await apiJSON(`/users/${editingUserId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
showToast('Client updated');
|
|
} else {
|
|
data.password = password;
|
|
await apiJSON('/users/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
showToast('Client created');
|
|
}
|
|
closeUserModal();
|
|
loadUsers();
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
});
|
|
|
|
window.deleteUser = async (id) => {
|
|
if (!confirm('Delete this client? This will also remove all their instructions.')) return;
|
|
try {
|
|
await api(`/users/${id}`, { method: 'DELETE' });
|
|
showToast('Client deleted');
|
|
loadUsers();
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
};
|
|
|
|
// ── Instructions ───────────────────────────────────────────────
|
|
const instructionsBody = document.getElementById('instructions-body');
|
|
const instrModal = document.getElementById('instruction-modal');
|
|
const instrForm = document.getElementById('instruction-form');
|
|
const instrUserSelect = document.getElementById('instr-user');
|
|
let editingInstrId = null;
|
|
let currentFilterUserId = null;
|
|
|
|
async function loadInstructions(userId = null) {
|
|
try {
|
|
const url = userId ? `/instructions/?user_id=${userId}` : '/instructions/';
|
|
const instructions = await apiJSON(url);
|
|
instructionsBody.innerHTML = '';
|
|
|
|
if (instructions.length === 0) {
|
|
instructionsBody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No instructions yet</td></tr>';
|
|
return;
|
|
}
|
|
|
|
instructions.forEach(inst => {
|
|
const row = document.createElement('tr');
|
|
row.innerHTML = `
|
|
<td>${inst.title}</td>
|
|
<td><span class="badge badge-${inst.type}">${inst.type}</span></td>
|
|
<td class="hide-mobile">${inst.user_id || 'Global'}</td>
|
|
<td class="hide-mobile">${inst.content.substring(0, 60)}${inst.content.length > 60 ? '...' : ''}</td>
|
|
<td>
|
|
<button class="btn btn-sm btn-secondary" onclick="editInstruction(${inst.id})">Edit</button>
|
|
<button class="btn btn-sm btn-danger" onclick="deleteInstruction(${inst.id})">✕</button>
|
|
</td>
|
|
`;
|
|
instructionsBody.appendChild(row);
|
|
});
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function loadUserOptions() {
|
|
try {
|
|
const users = await apiJSON('/users/');
|
|
instrUserSelect.innerHTML = '<option value="">Global (all clients)</option>';
|
|
users.forEach(u => {
|
|
instrUserSelect.innerHTML += `<option value="${u.id}">${u.username}</option>`;
|
|
});
|
|
} catch (e) {
|
|
// silently fail
|
|
}
|
|
}
|
|
|
|
window.manageInstructions = (userId, username) => {
|
|
currentFilterUserId = userId;
|
|
// Switch to instructions tab
|
|
tabs.forEach(t => t.classList.remove('active'));
|
|
panels.forEach(p => p.classList.add('hidden'));
|
|
document.querySelector('[data-panel="instructions-panel"]').classList.add('active');
|
|
document.getElementById('instructions-panel').classList.remove('hidden');
|
|
loadInstructions(userId);
|
|
};
|
|
|
|
window.showInstructionModal = () => {
|
|
editingInstrId = null;
|
|
document.getElementById('instr-modal-title').textContent = 'Add Instruction';
|
|
instrForm.reset();
|
|
loadUserOptions();
|
|
instrModal.classList.remove('hidden');
|
|
};
|
|
|
|
window.closeInstructionModal = () => {
|
|
instrModal.classList.add('hidden');
|
|
editingInstrId = null;
|
|
};
|
|
|
|
window.editInstruction = async (id) => {
|
|
try {
|
|
const instructions = await apiJSON('/instructions/');
|
|
const inst = instructions.find(x => x.id === id);
|
|
if (!inst) return;
|
|
|
|
editingInstrId = id;
|
|
await loadUserOptions();
|
|
document.getElementById('instr-modal-title').textContent = 'Edit Instruction';
|
|
document.getElementById('instr-title').value = inst.title;
|
|
document.getElementById('instr-content').value = inst.content;
|
|
document.getElementById('instr-type').value = inst.type;
|
|
instrUserSelect.value = inst.user_id || '';
|
|
instrModal.classList.remove('hidden');
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
};
|
|
|
|
instrForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const data = {
|
|
title: document.getElementById('instr-title').value.trim(),
|
|
content: document.getElementById('instr-content').value.trim(),
|
|
type: document.getElementById('instr-type').value,
|
|
user_id: instrUserSelect.value ? parseInt(instrUserSelect.value) : null,
|
|
};
|
|
|
|
try {
|
|
if (editingInstrId) {
|
|
await apiJSON(`/instructions/${editingInstrId}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(data),
|
|
});
|
|
showToast('Instruction updated');
|
|
} else {
|
|
await apiJSON('/instructions/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
showToast('Instruction created');
|
|
}
|
|
closeInstructionModal();
|
|
loadInstructions(currentFilterUserId);
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
});
|
|
|
|
window.deleteInstruction = async (id) => {
|
|
if (!confirm('Delete this instruction?')) return;
|
|
try {
|
|
await api(`/instructions/${id}`, { method: 'DELETE' });
|
|
showToast('Instruction deleted');
|
|
loadInstructions(currentFilterUserId);
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
};
|
|
|
|
// ── File upload for instructions ────────────────────────────────
|
|
window.uploadInstructionFile = () => {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = '.txt,.md';
|
|
input.onchange = async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const text = await file.text();
|
|
document.getElementById('instr-title').value = file.name.replace(/\.[^.]+$/, '');
|
|
document.getElementById('instr-content').value = text;
|
|
showInstructionModal();
|
|
};
|
|
input.click();
|
|
};
|
|
|
|
// ── Voice instruction generation ───────────────────────────────
|
|
const voiceGenBtn = document.getElementById('voice-gen-btn');
|
|
const voiceGenOutput = document.getElementById('voice-gen-output');
|
|
const voiceGenText = document.getElementById('voice-gen-text');
|
|
const voice = new VoiceManager();
|
|
|
|
voice.init().then(() => {
|
|
voice.onResult = (text) => {
|
|
document.getElementById('voice-raw-text').value = text;
|
|
};
|
|
|
|
voice.onStateChange = (recording) => {
|
|
const btn = document.getElementById('voice-record-btn');
|
|
btn.classList.toggle('recording', recording);
|
|
btn.textContent = recording ? '⏹ Stop Recording' : '🎤 Start Recording';
|
|
};
|
|
});
|
|
|
|
window.toggleVoiceRecord = () => voice.toggleRecording();
|
|
|
|
window.generateInstruction = async () => {
|
|
const rawText = document.getElementById('voice-raw-text').value.trim();
|
|
if (!rawText) {
|
|
showToast('Please record or type some text first', 'error');
|
|
return;
|
|
}
|
|
|
|
voiceGenBtn.disabled = true;
|
|
voiceGenBtn.textContent = 'Generating...';
|
|
|
|
try {
|
|
const result = await apiJSON('/voice/generate-instruction', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ raw_text: rawText }),
|
|
});
|
|
|
|
voiceGenText.value = result.instruction;
|
|
voiceGenOutput.classList.remove('hidden');
|
|
} catch (e) {
|
|
showToast(e.message, 'error');
|
|
}
|
|
|
|
voiceGenBtn.disabled = false;
|
|
voiceGenBtn.textContent = '✨ Generate Instruction';
|
|
};
|
|
|
|
window.saveGeneratedInstruction = () => {
|
|
const content = voiceGenText.value.trim();
|
|
if (!content) return;
|
|
|
|
loadUserOptions();
|
|
document.getElementById('instr-content').value = content;
|
|
document.getElementById('instr-title').value = 'Voice Generated Instruction';
|
|
showInstructionModal();
|
|
};
|
|
|
|
// ── Init ───────────────────────────────────────────────────────
|
|
loadUsers();
|
|
loadInstructions();
|
|
document.getElementById('logout-btn').addEventListener('click', logout);
|
|
});
|