Files
evento/public/admin.html

459 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Panel</title>
<style>
body {
font-family: system-ui, sans-serif;
padding: 2rem;
max-width: 900px;
margin: 0 auto;
}
h1 {
margin-bottom: 0.5rem;
}
.module {
border: 1px solid #ccc;
padding: 1rem;
border-radius: 10px;
margin-bottom: 1rem;
}
.badge {
display: inline-block;
padding: 0.2rem 0.6rem;
background: #e5e7eb;
border-radius: 9999px;
font-size: 0.8rem;
color: #111827;
}
.field {
display: flex;
flex-direction: column;
margin-bottom: 0.5rem;
}
.field input {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 6px;
}
button {
padding: 0.5rem 0.75rem;
border: 1px solid #ccc;
border-radius: 6px;
background: #f7f7f8;
cursor: pointer;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error {
color: #b91c1c;
margin-top: 0.5rem;
}
.success {
color: #047857;
margin-top: 0.5rem;
}
</style>
</head>
<body>
<h1>Admin Panel</h1>
<p>Interfaz de administración (Vue 3 + JWT).</p>
<div id="app" style="display: flex; gap: 1rem;">
<div v-if="!token" class="module" style="flex: 1;">
<h2>Iniciar sesión</h2>
<div class="field">
<label>Email</label>
<input v-model="login.email" type="email" placeholder="admin@local" />
</div>
<div class="field">
<label>Contraseña</label>
<input v-model="login.password" type="password" placeholder="••••••" />
</div>
<div class="field">
<label>Tenant</label>
<input v-model="login.tenantId" placeholder="default" />
</div>
<button :disabled="loading" @click="loginUser">Iniciar sesión</button>
<div :class="{ error: loginError, success: loginSuccess }">{{ loginMessage }}</div>
<hr />
<h2>Registrar usuario</h2>
<div class="field">
<label>Nombre</label>
<input v-model="register.name" placeholder="Nombre (opcional)" />
</div>
<div class="field">
<label>Email</label>
<input v-model="register.email" type="email" placeholder="admin@local" />
</div>
<div class="field">
<label>Contraseña</label>
<input v-model="register.password" type="password" placeholder="••••••" />
</div>
<div class="field">
<label>Tenant</label>
<input v-model="register.tenantId" placeholder="default" />
</div>
<button :disabled="loading" @click="registerUser">Registrar</button>
<div :class="{ error: registerError, success: registerSuccess }">{{ registerMessage }}</div>
</div>
<div v-else style="flex: 1; display: flex; gap: 1rem;">
<div style="width: 240px;">
<div class="module">
<div style="display:flex; align-items:center; justify-content:space-between;">
<div>
<div><strong>{{ user?.name || user?.email }}</strong></div>
<div style="font-size:0.85rem; color:#555;">Tenant: {{ tenantId }}</div>
</div>
<button @click="logout">Salir</button>
</div>
</div>
<div class="module">
<h3>Módulos</h3>
<div v-for="mod in modules" :key="mod.key" style="margin-bottom:0.5rem;">
<button
:style="{
width: '100%',
textAlign: 'left',
padding: '0.5rem',
border: selectedModule === mod.key ? '1px solid #333' : '1px solid #ccc',
background: selectedModule === mod.key ? '#f0f0f0' : '#fff',
}"
@click="selectModule(mod.key)">
{{ mod.name }}
</button>
</div>
</div>
<div class="module" v-if="selectedModule">
<h3>Acciones</h3>
<div v-for="action in actions" :key="action.key" style="margin-bottom:0.4rem;">
<button style="width: 100%; text-align: left;" @click="action.handler()">{{ action.label }}</button>
</div>
</div>
</div>
<div style="flex: 1;">
<div class="module">
<div style="display:flex; justify-content:space-between; align-items:center;">
<div>
<label>
Tenant ID:
<input v-model="tenantId" style="margin-left:0.5rem;" />
</label>
</div>
<div style="color:#555;">{{ status }}</div>
</div>
</div>
<div class="module">
<h2 v-if="selectedModule">Administrar: {{ selectedModule }}</h2>
<div v-html="moduleHtml"></div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp, reactive, ref } = Vue;
createApp({
setup() {
const apiRoot = '/api/admin';
const authRoot = '/api/auth';
const tokenKey = 'planner_admin_token';
const token = ref(localStorage.getItem(tokenKey) || '');
const user = ref(null);
const tenantId = ref('default');
const status = ref('');
const modules = ref([]);
const selectedModule = ref('');
const moduleHtml = ref('');
const loading = ref(false);
const login = reactive({ email: '', password: '', tenantId: 'default' });
const register = reactive({ name: '', email: '', password: '', tenantId: 'default' });
const loginMessage = ref('');
const loginError = ref(false);
const loginSuccess = ref(false);
const registerMessage = ref('');
const registerError = ref(false);
const registerSuccess = ref(false);
const setStatus = (msg) => (status.value = msg);
const setToken = (t) => {
token.value = t;
if (t) localStorage.setItem(tokenKey, t);
else localStorage.removeItem(tokenKey);
};
const api = async (path, options = {}) => {
setStatus('Consultando ' + path + '...');
const headers = options.headers || {};
if (token.value) headers.Authorization = `Bearer ${token.value}`;
const res = await fetch(`${apiRoot}${path}?tenantId=${encodeURIComponent(tenantId.value)}`, {
...options,
headers,
});
const data = await res.json().catch(() => null);
if (!res.ok) {
const err = data?.message || res.statusText;
setStatus(`Error ${res.status}`);
throw new Error(err);
}
setStatus('OK');
setTimeout(() => setStatus(''), 1500);
return data;
};
const logout = () => {
setToken('');
user.value = null;
selectedModule.value = '';
moduleHtml.value = '';
};
const loginUser = async () => {
loginMessage.value = '';
loginError.value = false;
loginSuccess.value = false;
loading.value = true;
try {
const res = await fetch(`${authRoot}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenantId: login.tenantId || 'default',
email: login.email,
password: login.password,
}),
});
const data = await res.json();
if (!res.ok || !data.token) {
loginMessage.value = data.message || 'Credenciales inválidas';
loginError.value = true;
return;
}
setToken(data.token);
user.value = { email: login.email, name: data.name };
loginMessage.value = 'Sesión iniciada correctamente';
loginSuccess.value = true;
await loadModules();
} catch (err) {
loginMessage.value = err.message || err;
loginError.value = true;
} finally {
loading.value = false;
}
};
const registerUser = async () => {
registerMessage.value = '';
registerError.value = false;
registerSuccess.value = false;
loading.value = true;
try {
const res = await fetch(`${authRoot}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tenantId: register.tenantId || 'default',
name: register.name,
email: register.email,
password: register.password,
}),
});
const data = await res.json();
if (!res.ok) {
registerMessage.value = data.message || 'Error al registrar';
registerError.value = true;
return;
}
registerMessage.value = 'Registrado. Inicia sesión.';
registerSuccess.value = true;
} catch (err) {
registerMessage.value = err.message || err;
registerError.value = true;
} finally {
loading.value = false;
}
};
const formatJson = (value) => `<pre style="white-space: pre-wrap; word-break: break-word;">${JSON.stringify(value, null, 2)}</pre>`;
const showResult = (value) => {
moduleHtml.value = formatJson(value);
};
const confirmPrompt = (message, defaultValue = '') => {
const result = prompt(message, defaultValue);
return result === null ? null : result.trim();
};
const listGuests = async () => {
const guests = await api('/guest');
showResult(guests);
};
const createGuest = async () => {
const name = confirmPrompt('Nombre');
if (!name) return;
const email = confirmPrompt('Email');
const phone = confirmPrompt('Teléfono');
const guest = await api('/guest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, phone }),
});
showResult(guest);
};
const updateGuestRsvp = async () => {
const id = confirmPrompt('ID del invitado');
if (!id) return;
const rsvp = confirmPrompt('RSVP (true/false)', 'true');
const updated = await api(`/guest/${id}/rsvp`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ rsvp: rsvp === 'true' }),
});
showResult(updated);
};
const listTodos = async () => {
const todos = await api('/todo');
showResult(todos);
};
const createTodo = async () => {
const title = confirmPrompt('Título');
if (!title) return;
const description = confirmPrompt('Descripción');
const todo = await api('/todo', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, description }),
});
showResult(todo);
};
const completeTodo = async () => {
const id = confirmPrompt('ID del To-do');
if (!id) return;
const completed = await api(`/todo/${id}/complete`, {
method: 'PATCH',
});
showResult(completed);
};
const listGifts = async () => {
const gifts = await api('/gift');
showResult(gifts);
};
const createGift = async () => {
const name = confirmPrompt('Nombre del regalo');
if (!name) return;
const description = confirmPrompt('Descripción');
const price = confirmPrompt('Precio');
const gift = await api('/gift', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description, price: Number(price) || 0 }),
});
showResult(gift);
};
const contributeGift = async () => {
const id = confirmPrompt('ID del regalo');
if (!id) return;
const name = confirmPrompt('Nombre del contribuyente');
const amount = confirmPrompt('Monto');
const contribution = await api(`/gift/${id}/contribution`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ contributorName: name, amount: Number(amount) || 0 }),
});
showResult(contribution);
};
const actions = computed(() => {
const map = {
guest: [
{ key: 'list', label: 'Listar invitados', handler: listGuests },
{ key: 'create', label: 'Crear invitado', handler: createGuest },
{ key: 'rsvp', label: 'Actualizar RSVP', handler: updateGuestRsvp },
],
todo: [
{ key: 'list', label: 'Listar To-dos', handler: listTodos },
{ key: 'create', label: 'Crear To-do', handler: createTodo },
{ key: 'complete', label: 'Completar To-do', handler: completeTodo },
],
gift: [
{ key: 'list', label: 'Listar regalos', handler: listGifts },
{ key: 'create', label: 'Crear regalo', handler: createGift },
{ key: 'contribute', label: 'Agregar contribución', handler: contributeGift },
],
};
return map[selectedModule.value] || [];
});
const loadModules = async () => {
if (!token.value) return;
try {
modules.value = await api('/modules');
} catch (err) {
console.error(err);
}
};
const selectModule = (key) => {
selectedModule.value = key;
moduleHtml.value = `<p>Seleccionado <strong>${key}</strong>. Usa las acciones en el menú.</p>`;
};
return {
token,
user,
tenantId,
status,
modules,
selectedModule,
moduleHtml,
actions,
login,
register,
loginMessage,
loginError,
loginSuccess,
registerMessage,
registerError,
registerSuccess,
loading,
loginUser,
registerUser,
logout,
loadModules,
selectModule,
};
},
}).mount('#app');
</script>
</body>
</html>
</script>
</body>
</html>