// Parcours documentaire connecté. La géométrie et la hiérarchie suivent la V6 // figée ; les opérations restent celles du serveur, sans OCR ni succès simulé. const DocumentImportBaseResume = TabResume; const DocumentImportBaseAction = QAction; const DocumentImportActionContext = React.createContext(null); const DIMP_VT = window.VT || {}; const DIMP_INK = DIMP_VT.ink || '#0a0a0a'; const DIMP_SUB = DIMP_VT.sub || '#6b7280'; const DIMP_BG = DIMP_VT.paper || '#f6f5f1'; const DIMP_SURF = DIMP_VT.surface || '#fff'; const DIMP_HAIR = DIMP_VT.hair || 'rgba(10,10,10,.06)'; const DIMP_HAIR2 = DIMP_VT.hair2 || 'rgba(10,10,10,.10)'; const DIMP_MED = DIMP_VT.medBlue || '#3d6b8f'; const DIMP_MED_TINT = DIMP_VT.medBlueTint || 'rgba(61,107,143,.10)'; const DIMP_DANGER = DIMP_VT.danger || '#b91c1c'; const DIMP_MOTION = window.VascoUI?.tokens?.motion || Object.freeze({ quick: 150, enter: 220, spinner: 750, sheet: 'cubic-bezier(.16,1,.3,1)', }); const DIMP_TRUNCATE = { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }; const DIMP_DOCUMENT_TYPE_COLUMN_WIDTH = 68; function documentTypeBadgeLabel(value) { const label = String(value || '').trim(); if (label.startsWith('Compte rendu')) return 'Compte rendu'; return label; } function documentTypeBadgeTone(status) { if (status === 'awaiting_validation') { return { border: '1px solid #d97706', background: '#d97706', color: '#fff' }; } if (status === 'archived') { return { border: `1px solid ${DIMP_HAIR2}`, background: DIMP_HAIR, color: DIMP_SUB }; } return { border: `1px solid ${DIMP_HAIR2}`, background: 'transparent', color: DIMP_INK }; } function DocumentImportError({ message }) { if (!message) return null; return
{message}
; } function documentImportReason(capabilities) { if (!capabilities) return 'Vérification de la disponibilité…'; return ({ storage_unavailable: 'Stockage des originaux indisponible.', scanner_unavailable: 'Contrôle de sécurité indisponible.', })[capabilities.reasonCode] || null; } function DocumentImportScanAction() { const { Button } = window.VascoUI; const entry = React.useContext(DocumentImportActionContext); if (!entry) return null; const disabled = !entry.capabilities?.enabled; const reason = entry.error || documentImportReason(entry.capabilities); const icon = ; return ; } // La V5 active ne porte pas encore le geste Scanner de la V6. Son QAction est // décoré uniquement dans le résumé : QR code est le premier geste propre à la // grille (appel et courriel vivent dans Coordonnées), Scanner se place devant. QAction = function DocumentImportAwareAction(props) { const entry = React.useContext(DocumentImportActionContext); if (entry && props.label === 'QR code') { return <>; } return ; }; function DocumentImportHeader({ title, patient, sub = null, onClose, onBack, closeRef }) { const { IconButton, icons } = window.VascoUI; return
{onBack && }
{title}
{sub || <>Dossier {patient.last} {patient.first}}
; } function DocumentImportShell({ step, dialogRef, onClose, busy = false, children }) { const { DetailSheet, tokens } = window.VascoUI; return
{children}
; } function DocumentImportPage({ item, height = 230, label, dim = false }) { return
{item?.kind === 'image' ? {item.file.name} :
{item?.file?.name || 'Document PDF'}
} {label && {label}}
; } function DocumentImportSources({ patient, onSelect, onClose, error }) { const { InteractiveRow } = window.VascoUI; const cameraRef = React.useRef(null); const galleryRef = React.useRef(null); const pdfRef = React.useRef(null); const handle = (source, multiple = false) => (event) => { const files = Array.from(event.target.files || []); event.target.value = ''; if (files.length) onSelect(source, multiple ? files : [files[0]]); }; const sources = [ { label: 'Prendre une photo', sub: 'Appareil photo, document posé à plat', ref: cameraRef, icon: <> }, { label: 'Galerie photo', sub: 'Une ou plusieurs images déjà prises', ref: galleryRef, icon: <> }, { label: 'Fichier PDF', sub: 'Compte rendu reçu par courriel', ref: pdfRef, icon: <> }, ]; return <>
{sources.map((source) => source.ref.current?.click()} style={{ width: '100%', display: 'flex', alignItems: 'center', gap: 12, marginBottom: 10, padding: 14, borderRadius: DIMP_VT.rLg || 16, border: `1px solid ${DIMP_HAIR}`, background: DIMP_SURF, cursor: 'pointer', textAlign: 'left', }}> {source.icon} {source.label} {source.sub} )}
; } function DocumentImportPages({ patient, items, store, onSelect, onContinue, onBack, onClose, error, busy }) { const { Button, InlineAction } = window.VascoUI; const cameraRef = React.useRef(null); const title = items[0]?.kind === 'pdf' ? 'Document PDF' : items.length > 1 ? `${items.length} pages` : 'Page 1'; return <>
{items.length > 1 &&
{items.slice(1).map((item, index) =>
)}
}
Vérifiez l’ordre des pages avant le transfert. L’original restera consultable dans le dossier.
{items.length > 1 &&
{items.map((item, index) =>
P{index + 1} {item.file.name} store.move(item.localId, index - 1)} disabled={index === 0 || busy}>Monter store.move(item.localId, index + 1)} disabled={index === items.length - 1 || busy}>Descendre store.remove(item.localId)} disabled={busy}>Retirer
)}
}
{items[0]?.kind !== 'pdf' && }
{ const files = Array.from(event.target.files || []); event.target.value = ''; if (files.length) onSelect('camera', [files[0]]); }} /> ; } function DocumentImportProcessing({ progress }) { return
VASCO sécurise le document…
{progress?.label || 'Préparation du transfert'}
{progress?.value != null &&
}
; } function DocumentCategoryPicker({ runtime, value, onChange, disabled, onBusyChange }) { const contract = window.VascoDocumentImport; const [categories, setCategories] = React.useState([]); const [query, setQuery] = React.useState(''); const [busy, setBusy] = React.useState(true); const [error, setError] = React.useState(null); const createAttempt = React.useRef(null); React.useEffect(() => onBusyChange?.(busy), [busy, onBusyChange]); const load = React.useCallback(async () => { setBusy(true); try { const page = await runtime.categories(); setCategories(page.items); setError(null); return page.items; } catch (failure) { setError(failure?.message || 'Étiquettes indisponibles'); return []; } finally { setBusy(false); } }, [runtime]); React.useEffect(() => { void load(); }, [load]); const normalized = contract.normalizedCategoryName(query); const exact = categories.find((category) => contract.normalizedCategoryName(category.name) === normalized); const canCreate = Boolean(normalized && !exact && !busy && !disabled); const create = async (requestedName) => { const requestedNormalized = contract.normalizedCategoryName(requestedName); const requestedExact = categories.find( (category) => contract.normalizedCategoryName(category.name) === requestedNormalized, ); if (!requestedNormalized || requestedExact || busy || disabled) return; if (createAttempt.current?.normalized !== requestedNormalized) { createAttempt.current = { normalized: requestedNormalized, attempt: runtime.newAttempt() }; } setBusy(true); setError(null); try { const result = await runtime.createCategory(requestedName.trim(), createAttempt.current.attempt); setCategories((current) => [...current.filter((item) => item.id !== result.category.id), result.category].sort((a, b) => a.name.localeCompare(b.name, 'fr'))); onChange(result.category); setQuery(''); createAttempt.current = null; } catch (failure) { if (failure?.status === 409 && failure?.code === 'document_category_already_exists') { const fresh = await load(); const existing = fresh.find((category) => category.id === failure.detail?.existing_id || contract.normalizedCategoryName(category.name) === requestedNormalized); if (existing) { onChange(existing); setQuery(''); createAttempt.current = null; return; } } setError(contract.errorMessage(failure, failure?.message || 'Création impossible')); } finally { setBusy(false); } }; const { SearchSelect } = window.VascoUI; return
Étiquette (facultative)
{ setQuery(nextQuery.slice(0, 120)); createAttempt.current = null; }} onOpenChange={(nextOpen) => { if (!nextOpen) setQuery(''); }} onChange={(_categoryId, category) => onChange(category?.id ? category : null)} getOptionValue={(category) => category.id} getOptionLabel={(category) => category.name} searchPlaceholder="Rechercher ou créer une étiquette…" loading={busy && !categories.length} loadingLabel="Chargement des étiquettes…" error={error} emptyLabel="Aucune étiquette existante" onCreate={!busy && !disabled && (!normalized || canCreate) ? (name) => { if (!name) return false; console.info('[DocumentCategoryPicker] création demandée dans le dropdown', { nameLength: name.length, }); void create(name); return true; } : null} allowEmptyCreate createLabel={(name) => name ? `Créer « ${name} »` : 'Créer une étiquette'} createPosition="before" maxOptionsHeight={210} disabled={disabled} />
; } function DocumentTypePicker({ runtime, value, onChange, disabled = false }) { const { SimpleSelect } = window.VascoUI; const [catalogue, setCatalogue] = React.useState([]); const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); React.useEffect(() => { let active = true; setLoading(true); runtime.documentTypes() .then((page) => { if (active) { setCatalogue(page.items); setError(null); } }) .catch((failure) => { if (active) setError(failure?.message || 'Types indisponibles'); }) .finally(() => { if (active) setLoading(false); }); return () => { active = false; }; }, [runtime]); const options = [['', 'Sans type / autre'], ...catalogue.map((item) => [item.code, item.label])]; return ; } function DocumentImportConfirm({ patient, items, review, setReview, runtime, busy, categoryBusy, setCategoryBusy, error, onSave, onReject, onRetake }) { const { Button } = window.VascoUI; return <>
1 ? `${items.length} p.` : null} />
Titre