Add sidebar menu with module actions in admin UI
This commit is contained in:
@@ -63,8 +63,8 @@
|
|||||||
<h1>Admin Panel</h1>
|
<h1>Admin Panel</h1>
|
||||||
<p>Interfaz de administración (Vue 3 + JWT).</p>
|
<p>Interfaz de administración (Vue 3 + JWT).</p>
|
||||||
|
|
||||||
<div id="app">
|
<div id="app" style="display: flex; gap: 1rem;">
|
||||||
<div v-if="!token" class="module">
|
<div v-if="!token" class="module" style="flex: 1;">
|
||||||
<h2>Iniciar sesión</h2>
|
<h2>Iniciar sesión</h2>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Email</label>
|
<label>Email</label>
|
||||||
@@ -104,35 +104,63 @@
|
|||||||
<div :class="{ error: registerError, success: registerSuccess }">{{ registerMessage }}</div>
|
<div :class="{ error: registerError, success: registerSuccess }">{{ registerMessage }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="module">
|
<div v-else style="flex: 1; display: flex; gap: 1rem;">
|
||||||
<div style="display:flex; align-items:center; gap:1rem; margin-bottom:1rem;">
|
<div style="width: 240px;">
|
||||||
|
<div class="module">
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||||
<div>
|
<div>
|
||||||
<strong>Autenticado como:</strong> {{ user?.name || user?.email }}
|
<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>
|
||||||
<button @click="logout">Cerrar sesión</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>
|
<label>
|
||||||
Tenant ID:
|
Tenant ID:
|
||||||
<input v-model="tenantId" />
|
<input v-model="tenantId" style="margin-left:0.5rem;" />
|
||||||
</label>
|
</label>
|
||||||
<button @click="loadModules" style="margin-left:0.5rem;">Actualizar módulos</button>
|
</div>
|
||||||
<span style="margin-left:1rem; color:#555;">{{ status }}</span>
|
<div style="color:#555;">{{ status }}</div>
|
||||||
|
|
||||||
<div v-if="modules.length" style="margin-top:1rem;">
|
|
||||||
<div v-for="mod in modules" :key="mod.key" class="module">
|
|
||||||
<h2>{{ mod.name }}</h2>
|
|
||||||
<p><span class="badge">{{ mod.key }}</span> {{ mod.description }}</p>
|
|
||||||
<button @click="selectModule(mod.key)">Administrar</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedModule" class="module">
|
<div class="module">
|
||||||
<h2>Administrar: {{ selectedModule }}</h2>
|
<h2 v-if="selectedModule">Administrar: {{ selectedModule }}</h2>
|
||||||
<div v-html="moduleHtml"></div>
|
<div v-html="moduleHtml"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||||
<script>
|
<script>
|
||||||
@@ -263,6 +291,125 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 () => {
|
const loadModules = async () => {
|
||||||
if (!token.value) return;
|
if (!token.value) return;
|
||||||
try {
|
try {
|
||||||
@@ -274,7 +421,7 @@
|
|||||||
|
|
||||||
const selectModule = (key) => {
|
const selectModule = (key) => {
|
||||||
selectedModule.value = key;
|
selectedModule.value = key;
|
||||||
moduleHtml.value = `<p>Seleccionado <strong>${key}</strong>. Usa los botones del módulo para operar.</p>`;
|
moduleHtml.value = `<p>Seleccionado <strong>${key}</strong>. Usa las acciones en el menú.</p>`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -285,6 +432,7 @@
|
|||||||
modules,
|
modules,
|
||||||
selectedModule,
|
selectedModule,
|
||||||
moduleHtml,
|
moduleHtml,
|
||||||
|
actions,
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
loginMessage,
|
loginMessage,
|
||||||
@@ -304,4 +452,7 @@
|
|||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
</html>
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user