import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Navigate } from 'react-router-dom'; import { Pencil, Save, Plus, Trash2, Power, Copy, Download } from 'lucide-react'; import { useUi } from '../../context/ui-context.jsx'; import { useAuth } from '../../context/auth-context.jsx'; import { useAsync, useAction } from '../../hooks/useAsync.js'; import { listDevices, getDevice, updateDevice, getDevicesDashboard, listSoftware, listSoftwareDevices, getDevicesAccess, listDeviceViewers, addDeviceViewer, removeDeviceViewer, listInventoryTokens, createInventoryToken, setInventoryTokenActive, deleteInventoryToken, getInventoryTokenSecret, } from '../../services/devices.js'; import { listUsers } from '../../services/users.js'; import { listDepartments } from '../../services/departments.js'; import { Button } from '../../components/Button.jsx'; import { Field } from '../../components/Field.jsx'; import { ComboBox } from '../../components/ComboBox.jsx'; import { DetailPopup } from '../../components/DetailPopup.jsx'; import { DraftNotice } from '../../components/DraftNotice.jsx'; import { useDraftForm, useUserDrafts } from '../../hooks/useDraftForm.js'; import { SearchInput } from '../../components/SearchInput.jsx'; import { ListView } from '../../components/ListView.jsx'; import { Tabs } from '../../components/Tabs.jsx'; import { Badge } from '../../components/Badge.jsx'; import { KpiCard } from '../../components/KpiCard.jsx'; import { buildInstallerBat, downloadTextFile, GLPI_AGENT_VERSION } from './agentInstaller.js'; const KINDS = ['desktop', 'laptop', 'server', 'virtual', 'printer', 'network', 'other']; const fmtDate = (v) => (v ? String(v).slice(0, 16).replace('T', ' ') : '—'); const gb = (mb) => (mb ? `${(mb / 1024).toFixed(mb >= 10240 ? 0 : 1)} GB` : '—'); const dash = (v) => (v === null || v === undefined || v === '' ? '—' : v); const daysAgo = (v) => (v ? (Date.now() - new Date(String(v).replace(' ', 'T')).getTime()) / 86400000 : Infinity); function Row({ label, children }) { return (
{label} {children}
); } function SimpleTable({ columns, rows, empty }) { if (!rows?.length) return
{empty}
; return (
{columns.map((c) => )} {rows.map((r, i) => ( {columns.map((c) => )} ))}
{c.label}
{c.render ? c.render(r) : dash(r[c.key])}
); } function DeviceDetail({ id, onClose, onChanged, canEdit, tab = 'general', onTabChange }) { const { t, pushToast } = useUi(); const runAction = useAction(); const { data: device, reload } = useAsync(() => getDevice(id), [id]); const { data: users } = useAsync(listUsers, []); const { data: departments } = useAsync(listDepartments, []); const setTab = onTabChange; // la pestaña vive en la URL (?deviceTab=) const [searches, setSearches] = useState({}); const [showComponents, setShowComponents] = useState(false); const [editing, setEditing] = useState(false); // Borrador de los datos editables del equipo (estado, asignado, departamento, notas). const draft = useDraftForm({ formId: 'device-edit', entityId: id, initial: device ? { status: device.status, assignedUserId: device.assigned_user_id || '', departmentId: device.department_id || '', notes: device.notes || '', editVersion: device.edit_version } : {}, label: `${t('eq_detail')}: ${device?.hostname || id}`, enabled: !!device && canEdit && tab === 'general', onDiscard: () => setEditing(false), }); const form = draft.values; const setForm = draft.setValues; useEffect(() => { if (draft.restored) setEditing(true); }, [draft.restored]); const flat = (list) => (list || []).flatMap((d) => [d, ...(d.children ? flat(d.children) : [])]); const userOptions = (users || []).map((u) => ({ value: u.id, label: u.name })); const deptOptions = flat(departments).map((d) => ({ value: d.id, label: d.name })); function startEdit() { setEditing(true); } function save() { if (!draft.isDirty) { draft.discard(); setEditing(false); return; } draft.guardSave(() => runAction(async () => { await updateDevice(id, { status: form.status, assignedUserId: form.assignedUserId ? Number(form.assignedUserId) : null, departmentId: form.departmentId ? Number(form.departmentId) : null, notes: form.notes, editVersion: form.editVersion, }); draft.clear(); setEditing(false); await reload(); onChanged(); pushToast({ tone: 'success', message: t('common_saved') }); }), { fetchCurrent: async () => { const d = await getDevice(id); return { status: d.status, assignedUserId: d.assigned_user_id || '', departmentId: d.department_id || '', notes: d.notes || '' }; } }); } if (!device) return
{t('common_loading')}
; const tabs = [ { key: 'general', label: t('eq_tab_general'), dirty: editing && draft.isDirty }, { key: 'users', label: t('eq_tab_users'), count: device.accounts.filter((a) => a.kind === 'local_user').length }, { key: 'hardware', label: t('eq_tab_hardware') }, { key: 'network', label: t('eq_tab_network'), count: device.networks.length }, { key: 'software', label: t('eq_tab_software'), count: device.software.filter((r) => showComponents || !r.is_component).length }, { key: 'history', label: t('eq_tab_history'), count: device.changes.length }, ]; // Un buscador por pestaña (su texto se conserva al cambiar de pestaña y volver). const q = (searches[tab] || '').trim().toLowerCase(); const hit = (...values) => !q || values.join(' ').toLowerCase().includes(q); const emptyText = q ? t('common_search_no_results') : t('common_empty'); const only = (rows, fn) => rows.filter((r) => hit(...fn(r))); const showRows = (list) => list.filter(([label, , text]) => hit(label, text)).map(([label, node]) => {node}); const val = (v) => dash(v); const infoRows = [ [t('eq_hostname'), val(device.hostname), device.hostname], [t('eq_kind'), t(`eq_kind_${device.kind}`), t(`eq_kind_${device.kind}`)], [t('eq_manufacturer_model'), val([device.manufacturer, device.model].filter(Boolean).join(' ')), `${device.manufacturer || ''} ${device.model || ''}`], [t('eq_serial'), val(device.serial), device.serial], [t('eq_os'), val([device.os_name, device.os_version].filter(Boolean).join(' ')), `${device.os_name || ''} ${device.os_version || ''}`], [t('eq_domain'), val(device.domain), device.domain], [t('eq_last_user'), val(device.last_user), device.last_user], [t('eq_ip'), val(device.ip_main), device.ip_main], [t('eq_agent'), val(device.agent_version), device.agent_version], ]; const manualRows = [ [t('common_status'), {t(`eq_status_${device.status}`)}, t(`eq_status_${device.status}`)], [t('eq_assigned_to'), val(device.assigned_user_name), device.assigned_user_name], [t('common_department'), val(device.department_name), device.department_name], [t('eq_first_seen'), fmtDate(device.first_seen), fmtDate(device.first_seen)], [t('eq_last_seen'), fmtDate(device.last_seen), fmtDate(device.last_seen)], [t('eq_notes'), {val(device.notes)}, device.notes], ]; const cpuText = device.cpus.map((c) => `${c.name || ''}${c.cores ? ` · ${c.cores}c/${c.threads || '?'}t` : ''}`).join(' | '); const hwRows = [ [t('eq_cpu'), val(cpuText), cpuText], [t('eq_ram'), gb(device.ram_mb), gb(device.ram_mb)], [t('eq_disk_total'), <>{gb(device.disk_total_mb)} {device.max_disk_used_pct != null && · {t('eq_disk_max_used')} {device.max_disk_used_pct}%}, `${gb(device.disk_total_mb)} ${device.max_disk_used_pct ?? ''}`], ]; const memories = only(device.memories, (r) => [r.slot, gb(r.capacity_mb), r.type, r.speed, r.manufacturer]); const disks = only(device.disks, (r) => [r.model, r.type, gb(r.size_mb), r.serial]); const volumes = only(device.volumes, (r) => [r.mount, r.filesystem, gb(r.total_mb), gb(r.free_mb)]); const monitors = only(device.monitors, (r) => [r.manufacturer, r.model, r.serial]); const hwSection = (title, rows, columns) => (!q || rows.length > 0) && ( <>

{title}

); const sessionNames = device.accounts.filter((a) => a.kind === 'session').map((a) => (a.domain ? `${a.domain}\\${a.name}` : a.name)); const allLocalUsers = device.accounts.filter((a) => a.kind === 'local_user'); const allLocalGroups = device.accounts.filter((a) => a.kind === 'local_group'); const userRows = [ [t('eq_users_session'), val(sessionNames.join(', ')), sessionNames.join(' ')], [t('eq_users_local_count'), String(allLocalUsers.length), String(allLocalUsers.length)], [t('eq_groups_local_count'), String(allLocalGroups.length), String(allLocalGroups.length)], ]; const localUsers = only(allLocalUsers, (r) => [r.name, r.sid]); const localGroups = only(allLocalGroups, (r) => [r.name, r.sid]); const networks = only(device.networks, (r) => [r.description, r.ip, r.mask, r.gateway, r.mac, r.status]); const software = only(device.software.filter((r) => showComponents || !r.is_component), (r) => [r.name, r.version, r.publisher]); const changes = only(device.changes, (r) => [fmtDate(r.created_at), t(`eq_change_${r.change_type}`), r.field, r.old_value, r.new_value]); return ( <>
{tab === 'software' ? ( ) :
} setSearches({ ...searches, [tab]: v })} />
{tab === 'general' && ( <> {!editing ? ( <> {showRows(manualRows)} {q && showRows(infoRows).length + showRows(manualRows).length === 0 &&
{t('common_search_no_results')}
} {canEdit &&
} ) : ( <> setForm({ ...form, status: v })} /> setForm({ ...form, assignedUserId: v })} /> setForm({ ...form, departmentId: v })} /> {fmtDate(device.first_seen)} {fmtDate(device.last_seen)}