// 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 entry.onOpen(entry.capabilities)} style={{
gridColumn: '1 / -1', width: '100%', height: 56,
padding: '0 14px', borderRadius: 18,
border: `1px solid ${DIMP_HAIR2}`, background: DIMP_SURF,
color: DIMP_INK, cursor: disabled ? 'default' : 'pointer',
fontSize: 12.5, letterSpacing: '.1px', opacity: disabled ? .48 : 1,
boxShadow: window.VascoUI?.tokens?.shadow?.small,
}}>
Scanner un document
;
}
// 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
;
}
function DocumentImportPage({ item, height = 230, label, dim = false }) {
return
{item?.kind === 'image'
?
:
{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' && cameraRef.current?.click()} disabled={busy}
style={{ width: '100%', marginBottom: 8 }}>
+ Ajouter une page
}
Continuer
{
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
Type de document
onChange(nextValue || null)} options={options}
placeholder={loading ? 'Chargement des types…' : 'Sans type / autre'}
disabled={disabled || loading || Boolean(error)} />
;
}
function DocumentImportConfirm({ patient, items, review, setReview, runtime, busy, categoryBusy, setCategoryBusy, error, onSave, onReject, onRetake }) {
const { Button } = window.VascoUI;
return <>
1 ? `${items.length} p.` : null} />
setReview((current) => ({
...current, form: { ...current.form, documentType },
}))} />
setReview((current) => ({ ...current, form: { ...current.form, category, categoryId: category?.id || null } }))} />
Le document original fait foi. Vasco conserve chaque page et ne fabrique aucune lecture automatique.
Rejeter
Reprendre
Ajouter au dossier ✓
>;
}
function DocumentImportModal({ patient, patientRuntime, capabilities, onClose, onConfirmed }) {
const runtime = patientRuntime.documentImport;
const dialogRef = React.useRef(null);
// Le prop `document` porte la pièce clinique ; l'objet DOM est donc nommé
// explicitement pour que le retour de focus et le piège clavier ne le visent pas.
const returnFocus = React.useRef(window.document.activeElement);
const actionRef = React.useRef(null);
const storeRef = React.useRef(null);
if (!storeRef.current) {
storeRef.current = window.VascoDocumentImport.createDraftStore(() => {
const user = window.VascoApi.currentUser?.() || {};
return { sessionId: user.session_id || user.sessionId || user.id, organizationId: user.organization_id || user.organizationId };
}, URL);
}
const [step, setStep] = React.useState('sources');
const [items, setItems] = React.useState(storeRef.current.current().items);
const [currentImport, setCurrentImport] = React.useState(null);
const [review, setReview] = React.useState(null);
const [progress, setProgress] = React.useState(null);
const [error, setError] = React.useState(null);
const [busy, setBusy] = React.useState(false);
const [categoryBusy, setCategoryBusy] = React.useState(false);
const createAttempt = React.useRef(runtime.newAttempt());
const confirmAttempt = React.useRef(runtime.newAttempt());
const cancelAttempt = React.useRef(runtime.newAttempt());
React.useEffect(() => storeRef.current.subscribe((draft) => setItems(draft.items)), []);
React.useEffect(() => () => { storeRef.current.clear(); returnFocus.current?.focus?.(); }, []);
const perform = (task) => {
if (actionRef.current) return actionRef.current;
setBusy(true); setError(null);
actionRef.current = Promise.resolve().then(task).finally(() => { actionRef.current = null; setBusy(false); });
return actionRef.current;
};
const cancelReview = async () => {
if (currentImport?.state !== 'review') return true;
try { await runtime.cancel(patient.id, currentImport, cancelAttempt.current); return true; }
catch (failure) { setError(window.VascoDocumentImport.errorMessage(failure, failure?.message || 'Annulation impossible')); return false; }
};
const close = () => perform(async () => {
if (await cancelReview()) { storeRef.current.clear(); onClose(); }
});
React.useEffect(() => {
const keydown = (event) => {
if (event.key !== 'Tab') return;
const focusable = Array.from(dialogRef.current?.querySelectorAll('button:not([disabled]),input:not([disabled]),textarea:not([disabled])') || []);
if (!focusable.length) return;
const first = focusable[0]; const last = focusable[focusable.length - 1];
if (event.shiftKey && window.document.activeElement === first) { event.preventDefault(); last.focus(); }
else if (!event.shiftKey && window.document.activeElement === last) { event.preventDefault(); first.focus(); }
};
window.addEventListener('keydown', keydown);
return () => window.removeEventListener('keydown', keydown);
}, [busy, categoryBusy, currentImport]);
const selectFiles = (source, fileList) => {
setError(null);
const files = Array.from(fileList || []);
try {
const existing = storeRef.current.current().items;
const refusal = window.VascoDocumentImport.checkSelection(existing, files, capabilities);
if (refusal) throw new Error(refusal);
files.forEach((file) => storeRef.current.add({ localId: window.crypto.randomUUID(), file, source, uploadAttempt: runtime.newUploadAttempt() }));
if (files.length) setStep('pages');
} catch (failure) { setError(failure.message); }
};
const upload = () => perform(async () => {
const selected = storeRef.current.current().items;
if (!selected.length) { setError('Ajoutez au moins une page ou un PDF.'); return; }
setStep('processing');
try {
for (let index = 0; index < selected.length; index += 1) {
const item = storeRef.current.current().items[index];
if (!item.remoteId) {
setProgress({ label: `Transfert ${index + 1}/${selected.length}`, value: 0 });
const attachment = await runtime.uploadSource(item.file, item.uploadAttempt, (part) => setProgress(
part.stage === 'scanning' ? { label: 'Contrôle de sécurité…', value: null }
: { label: `Transfert ${index + 1}/${selected.length}`, value: part.completed / part.total },
));
storeRef.current.attachRemote(item.localId, attachment.id);
}
}
setProgress({ label: 'Préparation du dossier…', value: null });
const created = await runtime.create(patient.id, window.VascoDocumentImport.orderedRemoteIds(storeRef.current.current().items), createAttempt.current);
const suggestedTitle = window.VascoDocumentImport.titleFromFileName(storeRef.current.current().items[0]?.file?.name);
setCurrentImport(created);
setReview({
baseline: { version: created.version, title: '', categoryId: null, documentType: null },
form: { title: suggestedTitle, categoryId: null, category: null, documentType: null },
});
setProgress(null); setStep('confirm');
} catch (failure) {
setProgress(null); setError(window.VascoDocumentImport.errorMessage(failure, failure?.message || 'Import impossible')); setStep('pages');
}
});
const confirm = () => perform(async () => {
if (!review.form.title.trim()) { setError('Le titre est obligatoire.'); return; }
try {
const confirmed = await runtime.confirm(patient.id, currentImport, review.form, confirmAttempt.current);
await patientRuntime.documents(patient.id); await patientRuntime.load(patient.id);
storeRef.current.clear(); onConfirmed(confirmed); onClose();
} catch (failure) {
if (failure?.status === 409) {
const fresh = await runtime.get(patient.id, currentImport.id);
setCurrentImport(fresh);
setReview((current) => ({
baseline: { version: fresh.version, title: '', categoryId: null, documentType: null },
form: current.form,
}));
confirmAttempt.current = runtime.newAttempt();
}
setError(window.VascoDocumentImport.errorMessage(failure, failure?.message || 'Confirmation impossible'));
}
});
const retake = () => perform(async () => {
if (await cancelReview()) {
storeRef.current.clear(); setCurrentImport(null); setReview(null); setError(null);
createAttempt.current = runtime.newAttempt(); confirmAttempt.current = runtime.newAttempt(); cancelAttempt.current = runtime.newAttempt();
setStep('sources');
}
});
const backToSources = () => { storeRef.current.clear(); setError(null); setStep('sources'); };
return
{step === 'sources' && }
{step === 'pages' && void upload()} onBack={backToSources} onClose={close}
error={error} busy={busy} />}
{step === 'processing' && }
{step === 'confirm' && review && void confirm()} onReject={() => void close()} onRetake={() => void retake()} />}
;
}
function DocumentCreateModal({ patient, patientRuntime, onClose, onCreated }) {
const { Button } = window.VascoUI;
const dialogRef = React.useRef(null);
const returnFocus = React.useRef(window.document.activeElement);
const actionRef = React.useRef(null);
const runtime = patientRuntime.documentCreate;
const attemptRef = React.useRef(runtime.newAttempt());
const [draft, setDraft] = React.useState({
title: '', documentType: null, categoryId: null, category: null, content: '',
});
const [categoryBusy, setCategoryBusy] = React.useState(false);
const [busy, setBusy] = React.useState(false);
const [error, setError] = React.useState(null);
const close = () => { if (!busy && !categoryBusy) onClose(); };
React.useEffect(() => () => returnFocus.current?.focus?.(), []);
React.useEffect(() => {
const keydown = (event) => {
if (event.key !== 'Tab') return;
const focusable = Array.from(dialogRef.current?.querySelectorAll(
'button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled])',
) || []);
if (!focusable.length) return;
const first = focusable[0]; const last = focusable[focusable.length - 1];
if (event.shiftKey && window.document.activeElement === first) { event.preventDefault(); last.focus(); }
else if (!event.shiftKey && window.document.activeElement === last) { event.preventDefault(); first.focus(); }
};
window.addEventListener('keydown', keydown);
return () => window.removeEventListener('keydown', keydown);
}, [busy, categoryBusy, onClose]);
const save = () => {
if (actionRef.current) return actionRef.current;
const title = draft.title.trim(); const content = draft.content.trim();
if (!title || !content) { setError('Le titre et le contenu sont obligatoires.'); return null; }
setBusy(true); setError(null);
actionRef.current = runtime.create(patient.id, draft, attemptRef.current)
.then((created) => { onCreated(created); onClose(); })
.catch((failure) => {
if (failure?.status === 409) attemptRef.current = runtime.newAttempt();
setError(window.VascoDocumentImport.errorMessage(
failure, failure?.message || 'Création du document impossible',
));
})
.finally(() => { actionRef.current = null; setBusy(false); });
return actionRef.current;
};
const disabled = busy || categoryBusy || !draft.title.trim() || !draft.content.trim();
return
Titre
setDraft({ ...draft, title: event.target.value })}
placeholder="Titre du document" style={{ width: '100%', minHeight: 44, border: `1px solid ${DIMP_HAIR2}`, borderRadius: 12, padding: '0 12px', background: DIMP_SURF, color: DIMP_INK, fontFamily: 'inherit', fontSize: 13 }} />
setDraft({ ...draft, documentType })} />
setDraft({
...draft, category, categoryId: category?.id || null,
})} />
Contenu {draft.content.length.toLocaleString('fr-FR')} / 100 000
Le document sera créé avec le statut « À valider » et devra être relu avant validation.
Annuler
void save()}
disabled={categoryBusy || !draft.title.trim() || !draft.content.trim()}
busy={busy} busyLabel="Création…">Créer le document
;
}
function DocumentImportOriginal({ patientId, documentId, original, runtime }) {
const [url, setUrl] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let live = true; let created = null;
if (original.availability !== 'available') { setError('Original indisponible'); return undefined; }
runtime.originalObjectUrl(patientId, documentId, original).then((value) => {
if (live) { created = value; setUrl(value); } else URL.revokeObjectURL(value);
}).catch(() => { if (live) setError('Original indisponible'); });
return () => { live = false; if (created) URL.revokeObjectURL(created); };
}, [patientId, documentId, original.position]);
if (error) return Original indisponible – aucune copie de remplacement n’est affichée.
;
if (!url) return Chargement de l’original…
;
if ((original.media_type || original.mediaType) === 'application/pdf') return Le PDF ne peut pas être prévisualisé ici.
;
return ;
}
function documentImportDetailDate(value) {
if (!value) return null;
try {
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit', month: '2-digit', year: '2-digit',
timeZone: window.VASCO_RUNTIME?.dashboard?.time_zone || 'Europe/Paris',
}).format(new Date(value));
} catch (_) { return null; }
}
function documentImportStatus(status) {
return ({
draft: { label: 'Brouillon', color: DIMP_SUB, background: DIMP_HAIR },
in_progress: { label: 'En cours', color: '#9a6700', background: 'rgba(217,119,6,.10)' },
awaiting_validation: { label: 'À valider', color: '#9a6700', background: 'rgba(217,119,6,.10)' },
validated: { label: 'Validé', color: DIMP_MED, background: DIMP_MED_TINT },
archived: { label: 'Archivé', color: DIMP_SUB, background: DIMP_HAIR },
})[status] || { label: status || 'État inconnu', color: DIMP_SUB, background: DIMP_HAIR };
}
function documentValidationReason(capability) {
if (!capability) return 'Vérification du droit de validation…';
return ({
role_not_supported: 'Votre rôle ne permet pas de valider ce document.',
feature_unavailable: 'La validation des documents est indisponible.',
source_unavailable: 'La source du document est indisponible.',
patient_archived: 'Le dossier patient est archivé.',
patient_deceased: 'Le dossier patient ne peut plus être modifié.',
document_unavailable: 'Le contenu du document est indisponible.',
document_state_changed: 'Le statut du document a changé.',
document_not_owned: 'Ce document ne vous est pas attribué.',
})[capability.reason_code] || 'La validation de ce document est indisponible.';
}
function documentContentEditReason(capability) {
if (!capability) return 'Vérification du droit de modification…';
return ({
document_archived: 'Un document archivé ne peut pas être modifié.',
document_unavailable: 'Le contenu de ce document est indisponible.',
document_type_unsupported: 'Ce format de document ne peut pas être modifié ici.',
document_not_owned: 'Ce document est attribué à un autre praticien.',
historical_version: 'Une ancienne version reste consultable, mais seule la version actuelle est modifiable.',
})[capability.reason_code] || 'Le contenu de ce document ne peut pas être modifié.';
}
// Le serveur fournit un texte documentaire, pas une analyse structurée. On ne
// fabrique donc ni résumé ni valeurs : les intitulés déjà présents dans le
// document sont seulement séparés de leurs paragraphes pour accélérer la lecture.
function documentImportContentBlocks(content) {
return String(content || '').trim().split(/\n{2,}/).filter(Boolean).map((part) => {
const lines = part.split('\n').map((line) => line.trim()).filter(Boolean);
const first = lines[0] || '';
const isHeading = lines.length > 1
&& first.length <= 80
&& /[A-ZÀ-ÖØ-Þ]/.test(first)
&& first === first.toLocaleUpperCase('fr-FR');
return isHeading
? { heading: first, text: lines.slice(1).join('\n') }
: { heading: null, text: lines.join('\n') };
});
}
function documentImportTypeLabel(document) {
return document.document_type_label
|| (document.document_type === 'STAFF' ? 'Staff' : document.document_type)
|| 'Autre';
}
function documentCCAMError(error) {
if (error?.code === 'request_cancelled') return null;
if (error?.status === 401) return 'Votre session a expiré. Reconnectez-vous.';
if (error?.status === 403 || error?.status === 404) {
return 'Le codage CCAM de ce dossier n’est plus accessible.';
}
return 'Le codage CCAM est momentanément indisponible.';
}
function documentCCAMDate(value) {
if (!value) return '–';
return new Intl.DateTimeFormat('fr-FR', {
day: '2-digit', month: '2-digit', year: 'numeric',
}).format(new Date(value));
}
function documentCCAMMoney(minor, currency) {
if (minor === null || minor === undefined) return 'Tarif non publié';
return new Intl.NumberFormat('fr-FR', {
style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2,
}).format(minor / 100);
}
function documentCCAMBilledMinor(act) {
if (act.amountMinor === null || act.billingRateBasisPoints === null) return null;
return Math.floor((act.amountMinor * act.billingRateBasisPoints + 5_000) / 10_000);
}
function documentCCAMRate(act) {
const rate = act.billingRateBasisPoints;
if (rate === null || rate === undefined) return null;
if (rate === 0) return 'Non facturable à ce rang';
return `${rate / 100} %`;
}
async function documentCCAMCodings(runtime, patientId, signal) {
const pageSize = 100;
let pageNumber = 1;
let result = await runtime.ccamCodings.list(patientId, {
page: pageNumber, pageSize, signal,
});
let items = result.items;
while (pageNumber < result.totalPages) {
pageNumber += 1;
result = await runtime.ccamCodings.list(patientId, {
page: pageNumber, pageSize, signal,
});
items = VascoPatientCCAMCodings.mergePage({ items }, result).items;
}
return items;
}
function documentCCAMActCount(codings) {
return codings.reduce((total, coding) => total + coding.acts.length, 0);
}
function DocumentCCAMEditor({ coding, patient, onClose, onCodingUpdated }) {
const { FormSheet, tokens } = window.VascoUI;
const [message, setMessage] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
let live = true;
setMessage(null);
setError(null);
if (!coding?.conversationId || !coding?.id) {
setError('La carte de codage liée à ce CRO est indisponible.');
return () => { live = false; };
}
window.VascoApi.assistant.getConversation(coding.conversationId)
.then((conversation) => {
if (!live) return;
const exact = (conversation.messages || []).find(
(message) => message.widgetId === coding.id && message.widget === 'ccam',
);
if (!exact) {
setError('La carte de codage liée à ce CRO est introuvable.');
return;
}
setMessage(exact);
})
.catch((failure) => {
if (live) setError(failure?.message || 'Le codage CCAM ne peut pas être chargé.');
});
return () => { live = false; };
}, [coding.id, coding.conversationId]);
const applyWidgetUpdate = (widget) => {
setMessage((current) => current ? {
...current,
data: widget.data,
widgetId: widget.id,
widgetVersion: widget.version,
widgetStatus: widget.status,
widgetCapabilities: widget.capabilities || [],
widgetCroDocumentId: widget.cro_document_id
|| widget.data?._vasco_cro_document_id
|| null,
} : current);
onCodingUpdated?.(widget);
};
return ReactDOM.createPortal(
{!message && !error && Chargement du codage…
}
{error && {error}
}
{message && window.VascoCCAMWidgetEditor &&
}
,
document.getElementById('root'),
);
}
function DocumentCCAMSection({ patient, documentId, codings, loading, error,
onCodingUpdated }) {
const { InlineAction } = window.VascoUI;
const [editingCoding, setEditingCoding] = React.useState(null);
const linked = VascoPatientCCAMCodings.forClinicalDocument(codings, documentId);
if (!loading && !error && linked.length === 0) return null;
const actCount = documentCCAMActCount(linked);
return
Codage CCAM
{actCount > 0 &&
{actCount} {actCount === 1 ? 'acte' : 'actes'} }
{loading && linked.length === 0 && Chargement du codage…
}
{error && linked.length === 0 && {error}
}
{linked.map((coding, codingIndex) =>
{coding.confirmedAt
? `Confirmé le ${documentCCAMDate(coding.confirmedAt)}`
: 'À confirmer'}
{
if (!coding.conversationId || !coding.id) {
showToast('La conversation de ce codage est indisponible', 'error');
return;
}
setEditingCoding(coding);
}}>Modifier le codage
{coding.acts.map((act) => {
const rate = documentCCAMRate(act);
return
{act.billingRateBasisPoints === 0
? '–'
: documentCCAMMoney(documentCCAMBilledMinor(act), coding.currency)}
{rate &&
{rate}
}
;
})}
Total estimé
{documentCCAMMoney(coding.totalMinor, coding.currency)}
)}
{editingCoding && setEditingCoding(null)} onCodingUpdated={onCodingUpdated} />}
;
}
function DocumentImportDocumentDetail({ patient, document: initialDocument, patientRuntime,
ccamCodings, ccamLoading, ccamError, onBack, onDocumentUpdated, onCCAMUpdated }) {
const { Button } = window.VascoUI;
const [document, setDocument] = React.useState(initialDocument);
const [editingCategory, setEditingCategory] = React.useState(false);
const [draftCategory, setDraftCategory] = React.useState(initialDocument.category || null);
const [categoryBusy, setCategoryBusy] = React.useState(false);
const [categorySaving, setCategorySaving] = React.useState(false);
const [categoryError, setCategoryError] = React.useState(null);
const [review, setReview] = React.useState(null);
const [reviewLoading, setReviewLoading] = React.useState(initialDocument.status === 'awaiting_validation');
const [validationBusy, setValidationBusy] = React.useState(false);
const [validationError, setValidationError] = React.useState(null);
const [validationNotice, setValidationNotice] = React.useState(null);
const [validationWarning, setValidationWarning] = React.useState(null);
const [editingContent, setEditingContent] = React.useState(false);
const [draftContent, setDraftContent] = React.useState(initialDocument.content || '');
const [contentSaving, setContentSaving] = React.useState(false);
const [contentError, setContentError] = React.useState(null);
const [contentNotice, setContentNotice] = React.useState(null);
const [contentWarning, setContentWarning] = React.useState(null);
const [pdfBusy, setPdfBusy] = React.useState(null);
const [pdfError, setPdfError] = React.useState(null);
const categoryAttempt = React.useRef(null);
const contentAttempt = React.useRef(null);
const label = documentImportTypeLabel(document);
const date = documentImportDetailDate(document.created_at);
const status = documentImportStatus(document.status);
const originals = document.originals || [];
const hasOriginals = window.VascoDocumentImport.hasDocumentOriginals(originals);
// Pour un document à valider, le texte rendu est exactement celui renvoyé
// avec l'empreinte de relecture par le dashboard. On ne valide jamais le
// contenu plus ancien éventuellement reçu par la fiche patient.
const reviewContent = document.status === 'awaiting_validation'
? (review?.content ?? (reviewLoading ? null : document.content))
: document.content;
const blocks = documentImportContentBlocks(reviewContent);
const dialogRef = React.useRef(null);
const closeRef = React.useRef(null);
const validationButtonRef = React.useRef(null);
const contentEditButtonRef = React.useRef(null);
const contentEditorRef = React.useRef(null);
const validationInFlight = React.useRef(false);
const contentInFlight = React.useRef(false);
const reviewRequestId = React.useRef(0);
const returnFocus = React.useRef(window.document.activeElement);
const categoryChanged = (draftCategory?.id || null) !== (document.category?.id || null);
const globalValidationCapability = patientRuntime.reportReview?.capabilities?.() || null;
const documentValidationCapability = review?.capabilities?.validate || null;
const validationAllowed = document.status === 'awaiting_validation'
&& globalValidationCapability?.enabled !== false
&& documentValidationCapability?.enabled === true
&& Boolean(review?.content_sha256);
const contentCapability = document.capabilities?.edit_content || null;
const contentEditAllowed = contentCapability?.enabled === true
&& Boolean(patientRuntime.documentContent?.update);
const normalizedDraftContent = window.VascoDocumentImport.normalizeEditableContent(draftContent);
const contentChanged = window.VascoDocumentImport.contentHasChanges(
document.content, normalizedDraftContent,
);
const contentWithinLimit = normalizedDraftContent.length > 0
&& normalizedDraftContent.length <= window.VascoDocumentImport.CONTENT_MAX_LENGTH;
const pdfAvailable = window.VascoDocumentImport.canGeneratePdf(document)
&& Boolean(patientRuntime.documentPdf);
const loadValidationReview = async (targetDocument, message = null) => {
const requestId = reviewRequestId.current + 1;
reviewRequestId.current = requestId;
setReview(null);
setValidationError(message);
if (targetDocument.status !== 'awaiting_validation') {
setReviewLoading(false);
return;
}
if (!patientRuntime.reportReview) {
setReviewLoading(false);
setValidationError('La validation de ce document est indisponible.');
return;
}
const capability = patientRuntime.reportReview.capabilities?.() || null;
if (capability?.enabled === false) {
setReviewLoading(false);
setValidationError(documentValidationReason(capability));
return;
}
setReviewLoading(true);
try {
const value = await patientRuntime.reportReview.get(targetDocument.id);
if (reviewRequestId.current !== requestId) return;
setReview(value);
if (value.capabilities?.validate?.enabled === false) {
setValidationError(documentValidationReason(value.capabilities.validate));
} else {
setValidationError(message);
}
} catch (failure) {
if (reviewRequestId.current === requestId) {
setValidationError(window.VascoDocumentImport.errorMessage(
failure, failure?.message || 'Relecture sécurisée indisponible',
));
}
} finally {
if (reviewRequestId.current === requestId) setReviewLoading(false);
}
};
// La relecture automatique n'a lieu qu'à l'ouverture d'un autre document.
// Les changements de version internes (catégorie, conflit) appellent
// explicitement la même fonction : un seul GET est donc actif à la fois.
React.useEffect(() => {
setDocument(initialDocument);
setDraftCategory(initialDocument.category || null);
setDraftContent(initialDocument.content || '');
setEditingContent(false);
setContentError(null);
setContentNotice(null);
setContentWarning(null);
contentAttempt.current = null;
setValidationNotice(null);
setValidationWarning(null);
void loadValidationReview(initialDocument);
return () => { reviewRequestId.current += 1; };
}, [initialDocument.id, patientRuntime]);
const cancelCategoryEdit = () => {
setDraftCategory(document.category || null);
setEditingCategory(false);
setCategoryError(null);
categoryAttempt.current = null;
};
const saveCategory = async () => {
if (!categoryChanged || categoryBusy || categorySaving) return;
if (!categoryAttempt.current) categoryAttempt.current = patientRuntime.documentImport.newAttempt();
setCategorySaving(true);
setCategoryError(null);
try {
await patientRuntime.documentImport.updateCategory(
patient.id, document, draftCategory?.id || null, categoryAttempt.current,
);
const refreshed = await patientRuntime.document(patient.id, document.id);
setDocument(refreshed);
setDraftCategory(refreshed.category || null);
setEditingCategory(false);
categoryAttempt.current = null;
onDocumentUpdated?.(refreshed);
await loadValidationReview(refreshed);
} catch (failure) {
setCategoryError(window.VascoDocumentImport.errorMessage(
failure, failure?.message || 'Modification impossible',
));
} finally { setCategorySaving(false); }
};
const startContentEdit = () => {
if (!contentEditAllowed) return;
setDraftContent(reviewContent ?? document.content ?? '');
setEditingContent(true);
setContentError(null);
setContentNotice(null);
setContentWarning(null);
contentAttempt.current = null;
window.setTimeout(() => contentEditorRef.current?.focus(), 0);
};
const cancelContentEdit = () => {
setDraftContent(document.content || '');
setEditingContent(false);
setContentError(null);
contentAttempt.current = null;
window.setTimeout(() => contentEditButtonRef.current?.focus(), 0);
};
const saveContent = async () => {
if (!contentEditAllowed || !contentChanged || !contentWithinLimit
|| contentInFlight.current) return;
contentInFlight.current = true;
if (!contentAttempt.current) contentAttempt.current = patientRuntime.documentContent.newAttempt();
setContentSaving(true);
setContentError(null);
setContentNotice(null);
setContentWarning(null);
let updated;
try {
updated = await patientRuntime.documentContent.update(
patient.id, document, normalizedDraftContent, contentAttempt.current,
);
} catch (failure) {
if (failure?.status === 409 && failure?.code === 'version_conflict') {
try {
const refreshed = await patientRuntime.document(patient.id, document.id);
const conflict = window.VascoDocumentImport.rebaseContentDraft(refreshed, draftContent);
reviewRequestId.current += 1;
setReview(null);
setReviewLoading(false);
setDocument(conflict.document);
setDraftCategory(conflict.document.category || null);
setDraftContent(conflict.draft);
setContentError(conflict.message);
contentAttempt.current = null;
onDocumentUpdated?.(conflict.document);
} catch (reloadFailure) {
setContentError(window.VascoDocumentImport.errorMessage(
reloadFailure, 'Le document a changé. Votre texte est conservé, mais la version serveur n’a pas pu être rechargée.',
));
}
} else {
setContentError(window.VascoDocumentImport.errorMessage(
failure, failure?.message || 'Enregistrement impossible',
));
}
contentInFlight.current = false;
setContentSaving(false);
window.setTimeout(() => contentEditorRef.current?.focus(), 0);
return;
}
// La mutation est acquise avant toute resynchronisation secondaire.
const committed = window.VascoDocumentImport.applyContentUpdateResult(document, updated);
reviewRequestId.current += 1;
setDocument(committed);
setDraftCategory(committed.category || null);
setDraftContent(committed.content);
setEditingContent(false);
setReview(null);
setReviewLoading(true);
setContentNotice('Contenu enregistré. Le document doit être relu puis validé.');
contentAttempt.current = null;
onDocumentUpdated?.(committed);
let refreshFailed = false;
try {
const refreshed = await patientRuntime.document(patient.id, document.id);
setDocument(refreshed);
setDraftCategory(refreshed.category || null);
setDraftContent(refreshed.content || '');
onDocumentUpdated?.(refreshed);
await loadValidationReview(refreshed);
} catch (_) {
refreshFailed = true;
setReviewLoading(false);
}
try {
await patientRuntime.reportReview.refresh?.();
} catch (_) { refreshFailed = true; }
if (refreshFailed) setContentWarning(window.VascoDocumentImport.contentRefreshWarning());
contentInFlight.current = false;
setContentSaving(false);
window.setTimeout(() => contentEditButtonRef.current?.focus(), 0);
};
const validateDocument = async () => {
if (!validationAllowed || validationInFlight.current) return;
validationInFlight.current = true;
setValidationBusy(true);
setValidationError(null);
setValidationNotice(null);
setValidationWarning(null);
let validated;
try {
validated = await patientRuntime.reportReview.validate(review, review.content_sha256);
} catch (failure) {
if (failure?.status === 409) {
try {
const refreshed = await patientRuntime.document(patient.id, document.id);
setDocument(refreshed);
setDraftCategory(refreshed.category || null);
onDocumentUpdated?.(refreshed);
if (refreshed.status === 'awaiting_validation') {
await loadValidationReview(
refreshed, 'Le document a changé. Relisez son contenu avant de le valider.',
);
} else {
reviewRequestId.current += 1;
setReview(null);
setReviewLoading(false);
setValidationError('Le statut du document a changé. La validation n’a pas été rejouée.');
}
} catch (reloadFailure) {
setReview(null);
setValidationError(window.VascoDocumentImport.errorMessage(
reloadFailure, 'Le document a changé et son rechargement a échoué.',
));
}
} else {
setValidationError(window.VascoDocumentImport.errorMessage(
failure, failure?.message || 'Validation refusée',
));
}
window.setTimeout(() => validationButtonRef.current?.focus(), 0);
validationInFlight.current = false;
setValidationBusy(false);
return;
}
// À partir d'ici la commande serveur a réussi. Son résultat est appliqué
// avant toute lecture secondaire : aucun problème de resynchronisation ne
// peut réafficher le bouton ni être présenté comme un refus de validation.
reviewRequestId.current += 1;
const committed = window.VascoDocumentImport.applyValidationResult(document, validated);
setDocument(committed);
setDraftCategory(committed.category || null);
setReview(null);
setReviewLoading(false);
setValidationNotice('Document validé.');
onDocumentUpdated?.(committed);
let refreshFailed = false;
try {
const refreshed = await patientRuntime.document(patient.id, document.id);
setDocument(refreshed);
setDraftCategory(refreshed.category || null);
onDocumentUpdated?.(refreshed);
} catch (_) { refreshFailed = true; }
try {
await patientRuntime.reportReview.refresh?.();
} catch (_) { refreshFailed = true; }
if (refreshFailed) {
setValidationWarning(window.VascoDocumentImport.validationRefreshWarning());
}
window.setTimeout(() => closeRef.current?.focus(), 0);
validationInFlight.current = false;
setValidationBusy(false);
};
const openPdf = async (download) => {
if (!pdfAvailable || pdfBusy) return;
let previewWindow = null;
if (!download) {
previewWindow = window.open('', '_blank');
if (!previewWindow) {
setPdfError('Autorisez l’ouverture d’un nouvel onglet pour afficher le PDF.');
return;
}
previewWindow.opener = null;
}
setPdfBusy(download ? 'download' : 'preview');
setPdfError(null);
let result = null;
try {
result = await patientRuntime.documentPdf(patient.id, document, download);
if (download) {
const link = window.document.createElement('a');
link.href = result.url;
link.download = result.filename;
link.style.display = 'none';
window.document.body.appendChild(link);
link.click();
link.remove();
window.setTimeout(() => URL.revokeObjectURL(result.url), 1000);
} else {
previewWindow.location.replace(result.url);
window.setTimeout(() => URL.revokeObjectURL(result.url), 60000);
}
} catch (failure) {
previewWindow?.close();
if (result?.url) URL.revokeObjectURL(result.url);
setPdfError(window.VascoDocumentImport.errorMessage(
failure, failure?.message || 'Le PDF est indisponible',
));
} finally {
setPdfBusy(null);
}
};
React.useEffect(() => {
closeRef.current?.focus();
const keydown = (event) => {
if (event.key !== 'Tab') return;
const focusable = Array.from(dialogRef.current?.querySelectorAll('button:not([disabled]),object') || []);
if (!focusable.length) return;
const first = focusable[0]; const last = focusable[focusable.length - 1];
if (event.shiftKey && window.document.activeElement === first) { event.preventDefault(); last.focus(); }
else if (!event.shiftKey && window.document.activeElement === last) { event.preventDefault(); first.focus(); }
};
window.addEventListener('keydown', keydown);
return () => { window.removeEventListener('keydown', keydown); returnFocus.current?.focus?.(); };
}, [onBack]);
return
{document.title}
{status.label}
Version {document.version}
{document.author?.display_name &&
· {document.author.display_name}
}
{!editingCategory ?
Étiquette
{document.category?.name || 'Sans étiquette'}
{
setDraftCategory(document.category || null);
setEditingCategory(true);
setCategoryError(null);
}} style={{ color: DIMP_MED }}>Modifier
: <>
{
setDraftCategory(category);
setCategoryError(null);
categoryAttempt.current = null;
}} />
Annuler
void saveCategory()}
disabled={!categoryChanged || categoryBusy} busy={categorySaving}
busyLabel="Enregistrement…">Enregistrer
>}
{hasOriginals && <>
Document original
Le document original fait foi. Le texte ci-dessous facilite sa lecture sans le remplacer.
{originals.map((original) =>
{original.filename} · {original.page_count} page(s)
)}
>}
Contenu du document
{!editingContent &&
Modifier le contenu }
{!contentEditAllowed &&
{documentContentEditReason(contentCapability)}
}
{reviewLoading &&
Chargement du contenu à relire…
}
{editingContent ?
: !reviewLoading && blocks.length ?
{blocks.map((block, index) =>
{block.heading &&
{block.heading}
}
{block.text}
)}
: !reviewLoading &&
Aucun contenu textuel disponible.
}
{!editingContent &&
}
{contentNotice && !editingContent &&
{contentNotice}
}
{contentWarning && !editingContent &&
{contentWarning}
}
{validationNotice &&
{validationNotice}
}
{validationWarning &&
{validationWarning}
}
{document.document_type === 'CRO' &&
}
{pdfAvailable && !editingContent && <>
void openPdf(false)}
disabled={Boolean(pdfBusy)} busy={pdfBusy === 'preview'}
busyLabel="Ouverture…">Aperçu PDF
void openPdf(true)}
disabled={Boolean(pdfBusy)} busy={pdfBusy === 'download'}
busyLabel="Préparation…">Télécharger PDF
>}
{!editingContent && document.status === 'awaiting_validation' && validationAllowed &&
void validateDocument()} busy={validationBusy}
busyLabel="Validation…" style={{ width: '100%', marginTop: 16 }}>
Valider ce document
}
;
}
function DocumentImportProvider({ props, children }) {
const [capabilities, setCapabilities] = React.useState(null);
const [capabilityError, setCapabilityError] = React.useState(null);
const [modal, setModal] = React.useState(null);
const [writeModal, setWriteModal] = React.useState(false);
React.useEffect(() => {
let live = true;
props.runtime?.documentImport?.capabilities(props.p.id)
.then((value) => { if (live) { setCapabilities(value); setCapabilityError(null); } })
.catch((failure) => { if (live) setCapabilityError(failure?.message || 'Import indisponible'); });
return () => { live = false; };
}, [props.p.id, props.runtime]);
const context = { capabilities, error: capabilityError, onOpen: setModal, onWrite: () => setWriteModal(true) };
return
{children}
{modal && ReactDOM.createPortal(
setModal(null)}
onConfirmed={(document) => props.onDocumentConfirmed?.(document)} />,
document.getElementById('root'),
)}
{writeModal && ReactDOM.createPortal(
setWriteModal(false)}
onCreated={(document) => props.onDocumentConfirmed?.(document)} />,
document.getElementById('root'),
)}
;
}
function DocumentImportResume(props) {
return
;
}
function DocumentImportDocumentsList({ p, runtime, revision, onDocumentUpdated }) {
const { Button, ChoiceChip, InteractiveRow } = window.VascoUI;
const entry = React.useContext(DocumentImportActionContext);
const [filter, setFilter] = React.useState('all');
const [docs, setDocs] = React.useState([]);
const [counts, setCounts] = React.useState(null);
const [opened, setOpened] = React.useState(null);
const [busy, setBusy] = React.useState(true);
const [error, setError] = React.useState(null);
const [ccamCodings, setCCAMCodings] = React.useState([]);
const [ccamLoading, setCCAMLoading] = React.useState(true);
const [ccamError, setCCAMError] = React.useState(null);
const types = ['all', 'CRO', 'CRC', 'CRH', 'IMG', 'EXAM', 'STAFF', 'ORD', 'LETTRE'];
const labels = {
all: 'Tous', CRO: 'Op.', CRC: 'Cons.', CRH: 'Hospi.',
IMG: 'Imagerie', EXAM: 'Examens', STAFF: 'Staff', ORD: 'Ordonnances', LETTRE: 'Courriers',
};
React.useEffect(() => {
let live = true;
setBusy(true);
runtime.documents(p.id, filter)
.then((page) => {
if (!live) return;
setDocs(page.items);
setCounts(page.counts);
setError(null);
})
.catch((failure) => { if (live) setError(failure?.message || 'Documents indisponibles'); })
.finally(() => { if (live) setBusy(false); });
return () => { live = false; };
}, [runtime, p.id, filter, revision]);
React.useEffect(() => {
const controller = new AbortController();
let live = true;
setCCAMLoading(true);
setCCAMError(null);
documentCCAMCodings(runtime, p.id, controller.signal)
.then((items) => { if (live) setCCAMCodings(items); })
.catch((failure) => {
if (live) setCCAMError(documentCCAMError(failure));
})
.finally(() => { if (live) setCCAMLoading(false); });
return () => {
live = false;
controller.abort();
};
}, [runtime, p.id, revision]);
const open = async (item) => {
setBusy(true);
setError(null);
try { setOpened(await runtime.document(p.id, item.id)); }
catch (failure) { setError(failure?.message || 'Document indisponible'); }
finally { setBusy(false); }
};
if (opened) {
return ReactDOM.createPortal(
setOpened(null)}
onDocumentUpdated={(updated) => { setOpened(updated); onDocumentUpdated?.(updated); }}
onCCAMUpdated={() => onDocumentUpdated?.(opened)} />,
document.getElementById('root'),
);
}
const importDisabled = !entry?.capabilities?.enabled;
const importReason = entry?.error || documentImportReason(entry?.capabilities);
return
{types.map((type) => {
const active = filter === type;
const count = counts?.[type] ?? counts?.[type.toLowerCase()];
return setFilter(type)} style={{ flexShrink: 0 }}>
{labels[type]}{count !== undefined ? ` ${count}` : ''}
;
})}
entry?.onOpen(entry.capabilities)}
icon={
}>Importer
entry?.onWrite()}
icon={
}>Rédiger
{busy && !docs.length &&
Chargement…
}
{!busy && !docs.length &&
Aucun document de ce type.
}
{!!docs.length &&
{docs.map((doc) => {
const awaiting = doc.status === 'awaiting_validation';
const typeTone = documentTypeBadgeTone(doc.status);
const linkedCCAM = doc.type === 'CRO'
? VascoPatientCCAMCodings.forClinicalDocument(ccamCodings, doc.id)
: [];
const ccamActCount = documentCCAMActCount(linkedCCAM);
return
void open(doc)} disabled={busy || doc.available === false} style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 10,
padding: '14px 0', border: 0,
borderBottom: `1px solid ${DIMP_HAIR}`,
background: 'transparent', cursor: busy ? 'default' : 'pointer',
textAlign: 'left',
opacity: doc.available === false ? .5 : 1,
}}>
{documentTypeBadgeLabel(doc.type)}
{doc.label}
{doc.d}{awaiting && <> · À VALIDER>}
{doc.category?.name && {' · '}{doc.category.name} }
{ccamActCount > 0 &&
{' · '}{ccamActCount} {ccamActCount === 1 ? 'acte CCAM' : 'actes CCAM'}
}
{docSignature(doc)}
;
})}
}
;
}
function DocumentImportDocuments(props) {
const [revision, setRevision] = React.useState(0);
const providerProps = {
...props,
onDocumentConfirmed: () => setRevision((value) => value + 1),
};
return
setRevision((value) => value + 1)} />
;
}
TabResume = DocumentImportResume;
TabDocuments = DocumentImportDocuments;
// La file de relecture contient désormais aussi les textes IMG, EXAM et STAFF. La
// référence V5 reste gelée : cette décoration corrige uniquement les libellés
// devenus trop étroits, sans recopier le Dashboard ni modifier sa structure.
const DocumentImportBaseDashboard = PF_ORIGINAL_DASHBOARD;
const DocumentImportBaseSmartStack = PF_ORIGINAL_SMART_STACK;
const DocumentImportBaseValidationSheet = CRValidationSheet;
function documentQueueCopy(node) {
if (typeof node === 'string') {
return node
.replace(/COMPTES RENDUS À VALIDER/g, 'DOCUMENTS À VALIDER')
.replace(/Comptes rendus à valider/g, 'Documents à valider')
.replace(/Valider ce compte rendu/g, 'Valider ce document')
.replace(/^STAFF$/, 'Staff');
}
if (Array.isArray(node)) return node.map(documentQueueCopy);
if (!React.isValidElement(node) || node.props.children === undefined) return node;
const children = Array.isArray(node.props.children)
? node.props.children.map(documentQueueCopy)
: [documentQueueCopy(node.props.children)];
// Conserver des enfants JSX statiques : injecter un tableau unique ferait
// croire à React qu'il s'agit d'une liste dynamique nécessitant des clés.
return React.cloneElement(node, undefined, ...children);
}
Dashboard = function DocumentImportDashboard(props) {
return
{documentQueueCopy(DocumentImportBaseDashboard(props))}
;
};
window.Dashboard = Dashboard;
SmartStack = function DocumentImportSmartStack(props) {
return <>
{documentQueueCopy(DocumentImportBaseSmartStack(props))}
>;
};
window.SmartStack = SmartStack;
CRValidationSheet = function DocumentImportValidationSheet(props) {
return documentQueueCopy(DocumentImportBaseValidationSheet(props));
};
window.CRValidationSheet = CRValidationSheet;