edicion de metadata dispositivos, UI/UX

This commit is contained in:
2025-09-26 18:20:36 -06:00
parent e10d8950d9
commit 9848bd46f1
4 changed files with 93 additions and 2 deletions

View File

@@ -72,7 +72,8 @@
<div v-else class="grid"> <div v-else class="grid">
<DispositivoCard v-for="d in devices" :key="d.id" :device="d" :users="usersForDevice(d.id)" :devicesById="devicesById" <DispositivoCard v-for="d in devices" :key="d.id" :device="d" :users="usersForDevice(d.id)" :devicesById="devicesById"
:expanded="!!deviceExpanded[d.id]" :expanded="!!deviceExpanded[d.id]"
@toggle-expand="deviceExpanded[d.id] = !deviceExpanded[d.id]" /> @toggle-expand="deviceExpanded[d.id] = !deviceExpanded[d.id]"
@edit="openDeviceForm" />
</div> </div>
</template> </template>
</div> </div>
@@ -117,6 +118,10 @@
<Modal :open="showVlan" title="Crear VLAN" @close="closeVlan"> <Modal :open="showVlan" title="Crear VLAN" @close="closeVlan">
<VlanForm @success="onVlanCreated" @cancel="closeVlan" /> <VlanForm @success="onVlanCreated" @cancel="closeVlan" />
</Modal> </Modal>
<Modal :open="showDevice" title="Editar dispositivo" @close="closeDevice">
<DeviceForm :model="deviceFormModel" @success="onDeviceSaved" @cancel="closeDevice" />
</Modal>
</template> </template>
<script setup> <script setup>
@@ -128,6 +133,7 @@ import Modal from './components/Modal.vue';
import UserForm from './components/UserForm.vue'; import UserForm from './components/UserForm.vue';
import RawDbViewer from './components/RawDbViewer.vue'; import RawDbViewer from './components/RawDbViewer.vue';
import VlanForm from './components/VlanForm.vue'; import VlanForm from './components/VlanForm.vue';
import DeviceForm from './components/DeviceForm.vue';
const users = ref([]); const users = ref([]);
const requests = ref([]); const requests = ref([]);
@@ -316,6 +322,11 @@ const showVlan = ref(false);
function openVlanForm() { showSettingsMenu.value = false; showVlan.value = true; } function openVlanForm() { showSettingsMenu.value = false; showVlan.value = true; }
function closeVlan() { showVlan.value = false; } function closeVlan() { showVlan.value = false; }
function onVlanCreated() { showVlan.value = false; } function onVlanCreated() { showVlan.value = false; }
const showDevice = ref(false);
const deviceFormModel = ref({});
function openDeviceForm(d) { deviceFormModel.value = d; showDevice.value = true; }
function closeDevice() { showDevice.value = false; }
async function onDeviceSaved() { showDevice.value = false; await fetchDevices(); }
function openSettings() { showSettingsMenu.value = !showSettingsMenu.value; } function openSettings() { showSettingsMenu.value = !showSettingsMenu.value; }
function openEditUser(u) { function openEditUser(u) {
userFormMode.value = 'edit'; userFormMode.value = 'edit';

View File

@@ -0,0 +1,60 @@
<template>
<form @submit.prevent="submit" class="column" style="gap:10px;">
<div class="row">
<label class="toggle" style="flex:0 0 180px;">
<div class="muted" style="font-size:12px;">ID (DB)</div>
<input :value="model.id" readonly style="width:100%; background:transparent; border:none; outline:none; color:inherit;" />
</label>
<label class="toggle" style="flex:1;">
<div class="muted" style="font-size:12px;">MAC</div>
<input :value="model.mac" readonly style="width:100%; background:transparent; border:none; outline:none; color:inherit;" />
</label>
</div>
<div class="row">
<label class="toggle" style="flex:1;">
<div class="muted" style="font-size:12px;">Nombre</div>
<input v-model="state.nombre" placeholder="Nombre del dispositivo" style="width:100%; background:transparent; border:none; outline:none; color:inherit;" />
</label>
</div>
<label class="toggle" style="width:100%;">
<div class="muted" style="font-size:12px;">Descripción</div>
<textarea v-model="state.descripcion" rows="3" placeholder="Descripción opcional" style="width:100%; background:transparent; border:none; outline:none; color:inherit; resize: vertical;"></textarea>
</label>
<div v-if="error" class="muted" style="color:#ff6b6b;">{{ error }}</div>
<div class="modal-footer">
<button type="button" class="icon-btn" @click="$emit('cancel')">Cancelar</button>
<button type="submit" class="icon-btn">Guardar</button>
</div>
</form>
</template>
<script setup>
import { reactive, watch, ref } from 'vue';
const props = defineProps({ model: { type: Object, required: true } });
const emit = defineEmits(['success', 'cancel']);
const state = reactive({ nombre: '', descripcion: '' });
const error = ref('');
watch(() => props.model, (v) => {
state.nombre = v?.nombre || '';
state.descripcion = v?.descripcion || '';
}, { immediate: true, deep: true });
async function submit() {
error.value = '';
try {
const res = await fetch(`/api/devices/${encodeURIComponent(props.model.id)}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nombre: state.nombre, descripcion: state.descripcion })
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data.ok === false) { error.value = data.error || 'Error al guardar'; return; }
emit('success');
} catch (e) {
error.value = String(e?.message || e) || 'Error';
}
}
</script>

View File

@@ -2,11 +2,17 @@
<div class="card"> <div class="card">
<div class="row"> <div class="row">
<b>{{ device.nombre || device.mac }}</b> <b>{{ device.nombre || device.mac }}</b>
<span class="chip">{{ device.mac }}</span> <span class="chip">MAC: {{ device.mac }}</span>
<span class="chip">ID: {{ device.id }}</span>
<span v-if="connectedCount>0 || connected" class="chip" style="background: rgba(255,127,187,.2); border-color: rgba(255,127,187,.5);">Conectado</span> <span v-if="connectedCount>0 || connected" class="chip" style="background: rgba(255,127,187,.2); border-color: rgba(255,127,187,.5);">Conectado</span>
<span class="spacer"></span> <span class="spacer"></span>
<button class="icon-btn" @click="$emit('edit', device)">Editar</button>
<button v-if="!simple" class="icon-btn" @click="$emit('toggleExpand')">{{ expanded ? 'Contraer' : 'Expandir' }}</button> <button v-if="!simple" class="icon-btn" @click="$emit('toggleExpand')">{{ expanded ? 'Contraer' : 'Expandir' }}</button>
</div> </div>
<div class="muted" style="font-size:12px; margin-top:6px;" v-if="device.nombre || device.descripcion">
<div v-if="device.nombre">Nombre: {{ device.nombre }}</div>
<div v-if="device.descripcion">Descripción: {{ device.descripcion }}</div>
</div>
<div v-if="expanded && users && users.length" style="margin-top:8px;"> <div v-if="expanded && users && users.length" style="margin-top:8px;">
<div class="grid"> <div class="grid">
<UserCard v-for="u in users" :key="u.username" :item="u" :devicesById="devicesById" :expandable="false" /> <UserCard v-for="u in users" :key="u.username" :item="u" :devicesById="devicesById" :expandable="false" />

View File

@@ -162,4 +162,18 @@ router.get('/devices', async (_req, res) => {
} }
}); });
router.patch('/devices/:id', async (req, res) => {
try {
const id = parseInt(String(req.params.id), 10);
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ ok: false, error: 'invalid_id' });
const { nombre, descripcion } = req.body || {};
const { rowCount } = await pool.query('UPDATE dispositivos SET nombre = $2, descripcion = $3, last_seen = NOW() WHERE id = $1', [id, nombre != null ? String(nombre) : null, descripcion != null ? String(descripcion) : null]);
if (rowCount === 0) return res.status(404).json({ ok: false, error: 'not_found' });
res.json({ ok: true });
} catch (e) {
console.error('PATCH /api/devices/:id error:', e?.message || e);
res.status(500).json({ ok: false, error: 'db_error' });
}
});
export default router; export default router;