dummybot/resources/views/pages/settings.blade.php

261 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@extends('layouts.base')
@section('content')
<div x-data="settingsApp()" x-init="init()" class="max-w-4xl mx-auto px-4 sm:px-6 py-8">
<a href="{{ route('app') }}" class="text-sm text-zinc-500 hover:text-zinc-300"> Volver</a>
<h1 class="text-3xl font-bold mt-2 mb-6">Ajustes</h1>
{{-- TABS --}}
<div class="flex gap-1 border-b border-white/5 mb-6">
<button @click="tab = 'devices'" :class="tab==='devices' ? 'border-indigo-400 text-white' : 'border-transparent text-zinc-500'" class="px-4 py-2 border-b-2">Dispositivos</button>
<button @click="tab = 'projects'" :class="tab==='projects' ? 'border-indigo-400 text-white' : 'border-transparent text-zinc-500'" class="px-4 py-2 border-b-2">Proyectos</button>
<button @click="tab = 'tags'" :class="tab==='tags' ? 'border-indigo-400 text-white' : 'border-transparent text-zinc-500'" class="px-4 py-2 border-b-2">Tags</button>
<button @click="tab = 'api'" :class="tab==='api' ? 'border-indigo-400 text-white' : 'border-transparent text-zinc-500'" class="px-4 py-2">API</button>
</div>
{{-- DEVICES --}}
<div x-show="tab === 'devices'" class="space-y-3">
<div class="flex items-center justify-between">
<p class="text-zinc-400 text-sm">Tus DummyBots físicos. Cada uno tiene su propio token.</p>
<button @click="newDeviceOpen = true" class="btn btn-primary text-sm">+ Nuevo dispositivo</button>
</div>
<template x-for="d in devices" :key="d.id">
<div class="task-card">
<div class="flex items-center gap-3">
<div class="text-2xl">🤖</div>
<div class="flex-1 min-w-0">
<div class="font-semibold flex items-center gap-2">
<span x-text="d.name"></span>
<span x-show="!d.last_seen_at" class="chip prio-low text-xs">nunca visto</span>
<span x-show="d.last_seen_at" class="chip prio-low text-xs">visto <span x-text="ago(d.last_seen_at)"></span></span>
</div>
<div class="text-xs text-zinc-500 font-mono mt-1 truncate" x-text="d.token.substring(0, 16) + '…'"></div>
</div>
<button @click="rotate(d)" class="btn btn-ghost text-xs">🔄 Rotar token</button>
<button @click="delDevice(d)" class="btn btn-danger text-xs">🗑</button>
</div>
<template x-if="d.tokenShown">
<div class="mt-3 p-3 rounded bg-yellow-500/10 border border-yellow-500/30 text-sm">
<strong>⚠️ Guarda este token, no se mostrará otra vez:</strong>
<div class="font-mono text-xs mt-1 break-all" x-text="d.tokenShown"></div>
<div class="mt-2 text-xs text-zinc-400">Pégalo en el firmware del ESP32 en <code>BACKEND_URL</code> + header <code>Authorization: Bearer </code></div>
</div>
</template>
</div>
</template>
</div>
{{-- PROJECTS --}}
<div x-show="tab === 'projects'" class="space-y-3">
<div class="flex items-center justify-between">
<p class="text-zinc-400 text-sm">Agrupa tareas por proyecto (cliente, contexto, etc.)</p>
<button @click="newProjectOpen = true" class="btn btn-primary text-sm">+ Nuevo proyecto</button>
</div>
<div class="grid sm:grid-cols-2 gap-2">
<template x-for="p in projects" :key="p.id">
<div class="task-card flex items-center gap-3">
<div class="w-8 h-8 rounded" :style="'background:' + p.color"></div>
<div class="flex-1 font-semibold" x-text="p.name"></div>
<button @click="delProject(p)" class="btn btn-danger text-xs">🗑</button>
</div>
</template>
</div>
</div>
{{-- TAGS --}}
<div x-show="tab === 'tags'" class="space-y-3">
<div class="flex items-center justify-between">
<p class="text-zinc-400 text-sm">Etiquetas transversales para filtrar</p>
<button @click="newTagOpen = true" class="btn btn-primary text-sm">+ Nuevo tag</button>
</div>
<div class="flex flex-wrap gap-2">
<template x-for="t in tags" :key="t.id">
<div class="chip flex items-center gap-2" :style="'background:' + t.color + '20; color:' + t.color">
<span x-text="'#' + t.name"></span>
<button @click="delTag(t)" class="hover:text-red-400">×</button>
</div>
</template>
</div>
</div>
{{-- API DOC --}}
<div x-show="tab === 'api'" class="space-y-4 text-sm">
<p class="text-zinc-400">API REST completa. Todas las rutas devuelven JSON.</p>
<pre class="glass rounded-lg p-4 overflow-x-auto text-xs leading-relaxed"><code>GET /api/tasks Listar tareas
POST /api/tasks Crear tarea
PUT /api/tasks/{id} Editar
DELETE /api/tasks/{id} Borrar
POST /api/tasks/{id}/done Marcar como hecha
POST /api/tasks/{id}/snooze Posponer {minutes}
POST /api/tasks/{id}/restore Reabrir
GET /api/projects Listar proyectos
POST /api/projects Crear {name, color}
...
GET /api/tags Listar tags
POST /api/tags Crear {name, color}
GET /api/devices Tus dispositivos
POST /api/devices Crear dispositivo (devuelve token)
POST /api/devices/{id}/rotate Rotar token
# Para el DummyBot físico:
GET /api/device/tasks/pending (auth.device)
POST /api/device/tasks (auth.device)
POST /api/device/heartbeat (auth.device) ping
Auth: Authorization: Bearer &lt;token&gt;</code></pre>
</div>
{{-- MODAL NUEVO DISPOSITIVO --}}
<div x-show="newDeviceOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4"
@keydown.escape.window="newDeviceOpen = false">
<div class="glass rounded-2xl p-6 w-full max-w-md" @click.outside="newDeviceOpen = false">
<h2 class="text-xl font-bold mb-4">Nuevo DummyBot</h2>
<form @submit.prevent="createDevice()" class="space-y-3">
<input x-model="newDevice.name" type="text" placeholder="Nombre (ej: ESP32 del curro)" required class="input">
<input x-model="newDevice.firmware_version" type="text" placeholder="Versión firmware (opcional)" class="input">
<div class="flex gap-2 pt-2">
<button type="submit" class="btn btn-primary flex-1 justify-center">Crear</button>
<button type="button" @click="newDeviceOpen = false" class="btn btn-ghost">Cancelar</button>
</div>
</form>
</div>
</div>
{{-- MODALES PROYECTOS Y TAGS --}}
<div x-show="newProjectOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4"
@keydown.escape.window="newProjectOpen = false">
<div class="glass rounded-2xl p-6 w-full max-w-md" @click.outside="newProjectOpen = false">
<h2 class="text-xl font-bold mb-4">Nuevo proyecto</h2>
<form @submit.prevent="createProject()" class="space-y-3">
<input x-model="newProject.name" type="text" placeholder="Nombre" required class="input">
<input x-model="newProject.color" type="color" class="input h-12">
<div class="flex gap-2 pt-2">
<button type="submit" class="btn btn-primary flex-1 justify-center">Crear</button>
<button type="button" @click="newProjectOpen = false" class="btn btn-ghost">Cancelar</button>
</div>
</form>
</div>
</div>
<div x-show="newTagOpen" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4"
@keydown.escape.window="newTagOpen = false">
<div class="glass rounded-2xl p-6 w-full max-w-md" @click.outside="newTagOpen = false">
<h2 class="text-xl font-bold mb-4">Nuevo tag</h2>
<form @submit.prevent="createTag()" class="space-y-3">
<input x-model="newTag.name" type="text" placeholder="Nombre" required class="input">
<input x-model="newTag.color" type="color" class="input h-12">
<div class="flex gap-2 pt-2">
<button type="submit" class="btn btn-primary flex-1 justify-center">Crear</button>
<button type="button" @click="newTagOpen = false" class="btn btn-ghost">Cancelar</button>
</div>
</form>
</div>
</div>
<div x-show="toast" x-cloak class="toast" x-text="toast"></div>
</div>
<style>[x-cloak]{display:none!important}</style>
<script>
function settingsApp() {
return {
tab: 'devices',
devices: [], projects: [], tags: [],
newDeviceOpen: false, newProjectOpen: false, newTagOpen: false,
newDevice: {name: '', firmware_version: ''},
newProject: {name: '', color: '#6366f1'},
newTag: {name: '', color: '#a3a3a3'},
csrf: document.querySelector('meta[name=csrf-token]').content,
toast: '',
async init() { await this.loadAll(); },
async loadAll() {
this.devices = await this.api('/api/devices');
this.projects = await this.api('/api/projects');
this.tags = await this.api('/api/tags');
},
async createDevice() {
const d = await this.api('/api/devices', 'POST', this.newDevice);
d.tokenShown = d.token;
this.devices.push(d);
this.newDeviceOpen = false;
this.newDevice = {name: '', firmware_version: ''};
this.flash('✓ Dispositivo creado, guarda el token');
},
async rotate(d) {
if (!confirm('Rotar token invalidará el actual. ¿Continuar?')) return;
const upd = await this.api(`/api/devices/${d.id}/rotate`, 'POST');
upd.tokenShown = upd.token;
Object.assign(d, upd);
this.flash('🔄 Token rotado, guárdalo');
},
async delDevice(d) {
if (!confirm('¿Borrar dispositivo?')) return;
await this.api(`/api/devices/${d.id}`, 'DELETE');
this.devices = this.devices.filter(x => x.id !== d.id);
},
async createProject() {
const p = await this.api('/api/projects', 'POST', this.newProject);
this.projects.push(p);
this.newProjectOpen = false;
this.newProject = {name: '', color: '#6366f1'};
},
async delProject(p) {
if (!confirm('¿Borrar proyecto?')) return;
await this.api(`/api/projects/${p.id}`, 'DELETE');
this.projects = this.projects.filter(x => x.id !== p.id);
},
async createTag() {
const t = await this.api('/api/tags', 'POST', this.newTag);
this.tags.push(t);
this.newTagOpen = false;
this.newTag = {name: '', color: '#a3a3a3'};
},
async delTag(t) {
await this.api(`/api/tags/${t.id}`, 'DELETE');
this.tags = this.tags.filter(x => x.id !== t.id);
},
ago(s) {
if (!s) return 'nunca';
const d = (Date.now() - new Date(s).getTime()) / 1000;
if (d < 60) return 'hace un momento';
if (d < 3600) return `hace ${Math.floor(d/60)} min`;
if (d < 86400) return `hace ${Math.floor(d/3600)} h`;
return `hace ${Math.floor(d/86400)} d`;
},
async api(url, method = 'GET', body = null) {
const opts = {
method,
headers: {
'Accept': 'application/json',
'X-CSRF-TOKEN': this.csrf,
'X-Requested-With': 'XMLHttpRequest',
},
credentials: 'same-origin',
};
if (body) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(body);
}
const r = await fetch(url, opts);
if (!r.ok) throw new Error(await r.text());
return r.json();
},
flash(msg) { this.toast = msg; setTimeout(() => this.toast = '', 2500); },
};
}
</script>
@endsection