import { SearchableSelect } from '../../components/SearchableSelect'; import { useEffect, useRef, useState } from 'react'; import type { FormEvent } from 'react'; import { Alert, LoadingBlock, errorMessage } from '../../components/Feedback'; import { Icon } from '../../components/Icon'; import { getAssetMediaBlob, listAssetMedia, removeAssetMedia, updateAssetMedia, uploadAssetMedia, } from '../../lib/api'; import type { AssetMedia, AssetMediaKind } from '../../lib/api'; import { formatDate } from '../../lib/format'; function localDateTime(value: string | number | Date): string { const date = new Date(value); if (Number.isNaN(date.getTime())) return ''; const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000); return local.toISOString().slice(0, 16); } function fileSize(value: number) { return value >= 1024 * 1024 ? `${(value / (1024 * 1024)).toFixed(1)} MB` : `${Math.max(1, Math.round(value / 1024))} KB`; } function AssetPhotoPreview({ media }: { media: AssetMedia }) { const [url, setUrl] = useState(''); useEffect(() => { let active = true; let objectUrl = ''; getAssetMediaBlob(media.id) .then((blob) => { if (!active) return; objectUrl = URL.createObjectURL(blob); setUrl(objectUrl); }) .catch(() => undefined); return () => { active = false; if (objectUrl) URL.revokeObjectURL(objectUrl); }; }, [media.id]); return url ? {media.title :
; } export function AssetMediaPanel({ assetId, assetName, canManage, onChanged = () => undefined, }: { assetId: string; assetName: string; canManage: boolean; onChanged?: () => void; }) { const fileRef = useRef(null); const [items, setItems] = useState([]); const [kind, setKind] = useState('PHOTO'); const [file, setFile] = useState(null); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [capturedAt, setCapturedAt] = useState(''); const [latitude, setLatitude] = useState(''); const [longitude, setLongitude] = useState(''); const [accuracyM, setAccuracyM] = useState(''); const [editing, setEditing] = useState(null); const [editTitle, setEditTitle] = useState(''); const [editDescription, setEditDescription] = useState(''); const [editCapturedAt, setEditCapturedAt] = useState(''); const [editLatitude, setEditLatitude] = useState(''); const [editLongitude, setEditLongitude] = useState(''); const [editAccuracyM, setEditAccuracyM] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [locating, setLocating] = useState(false); const [error, setError] = useState(''); const [success, setSuccess] = useState(''); const load = () => listAssetMedia(assetId).then(setItems); useEffect(() => { setLoading(true); load() .catch((requestError) => setError(errorMessage(requestError))) .finally(() => setLoading(false)); }, [assetId]); const resetUpload = () => { setFile(null); setTitle(''); setDescription(''); setCapturedAt(''); setLatitude(''); setLongitude(''); setAccuracyM(''); if (fileRef.current) fileRef.current.value = ''; }; const useDeviceLocation = () => { if (!navigator.geolocation) { setError('Este navegador no permite obtener la ubicación del dispositivo.'); return; } setLocating(true); setError(''); navigator.geolocation.getCurrentPosition( (position) => { setLatitude(position.coords.latitude.toFixed(6)); setLongitude(position.coords.longitude.toFixed(6)); setAccuracyM(position.coords.accuracy.toFixed(3)); if (!capturedAt) setCapturedAt(localDateTime(position.timestamp)); setLocating(false); }, () => { setError('No fue posible obtener la ubicación del dispositivo.'); setLocating(false); }, { enableHighAccuracy: true, timeout: 15_000, maximumAge: 0 }, ); }; const upload = async (event: FormEvent) => { event.preventDefault(); if (!file) return; setSaving(true); setError(''); setSuccess(''); try { await uploadAssetMedia(assetId, { file, kind, title: title.trim() || undefined, description: description.trim() || undefined, capturedAt: capturedAt ? new Date(capturedAt).toISOString() : undefined, latitude: latitude ? Number(latitude) : undefined, longitude: longitude ? Number(longitude) : undefined, accuracyM: accuracyM ? Number(accuracyM) : undefined, }); await load(); resetUpload(); setSuccess('Archivo incorporado correctamente'); onChanged(); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const openEdit = (media: AssetMedia) => { setEditing(media); setEditTitle(media.title ?? ''); setEditDescription(media.description ?? ''); setEditCapturedAt(media.capturedAt ? localDateTime(media.capturedAt) : ''); setEditLatitude(media.latitude == null ? '' : String(media.latitude)); setEditLongitude(media.longitude == null ? '' : String(media.longitude)); setEditAccuracyM(media.accuracyM == null ? '' : String(media.accuracyM)); }; const saveEdit = async (event: FormEvent) => { event.preventDefault(); if (!editing) return; setSaving(true); setError(''); setSuccess(''); try { await updateAssetMedia(editing.id, { title: editTitle.trim() || null, description: editDescription.trim() || null, capturedAt: editCapturedAt ? new Date(editCapturedAt).toISOString() : null, latitude: editLatitude ? Number(editLatitude) : null, longitude: editLongitude ? Number(editLongitude) : null, accuracyM: editAccuracyM ? Number(editAccuracyM) : null, }); await load(); setEditing(null); setSuccess('Metadatos actualizados correctamente'); onChanged(); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const remove = async (media: AssetMedia) => { if (!window.confirm(`¿Retirar “${media.title || media.originalName}” de ${assetName}? El original se conservará en el almacenamiento protegido.`)) return; setSaving(true); setError(''); setSuccess(''); try { await removeAssetMedia(media.id); await load(); setSuccess('Archivo retirado; el original quedó preservado'); onChanged(); } catch (requestError) { setError(errorMessage(requestError)); } finally { setSaving(false); } }; const download = async (media: AssetMedia) => { setError(''); try { const blob = await getAssetMediaBlob(media.id, true); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = media.originalName; link.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); } catch (requestError) { setError(errorMessage(requestError)); } }; const photos = items.filter((item) => item.kind === 'PHOTO'); const documents = items.filter((item) => item.kind === 'DOCUMENT'); return
ARCHIVOS DEL INVENTARIO

Fotografías y documentos

{items.length} archivo{items.length === 1 ? '' : 's'}

Originales protegidos con hash SHA-256, autor, fecha y ubicación opcional. Formatos permitidos: JPG, PNG, WebP y PDF de hasta 15 MB.

{error && {error}}{success && {success}} {canManage &&