Exemplos de Integração
Aprenda a integrar a API TranscriMed através de exemplos práticos do mundo real para diferentes casos de uso em saúde.
Sistema de Registro Eletrônico (EHR)
Integração com Upload de Áudio
Este exemplo mostra como adicionar capacidades de transcrição médica a um sistema EHR existente.
// components/AudioTranscription.jsx
import React, { useState } from 'react';
function AudioTranscription({ patientId, onRecordGenerated }) {
const [isUploading, setIsUploading] = useState(false);
const [progress, setProgress] = useState(0);
const handleAudioUpload = async (audioFile) => {
setIsUploading(true);
try {
// Converter arquivo para base64
const audioBase64 = await fileToBase64(audioFile);
// Gerar registro médico
const response = await fetch('/api/medical-records/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAccessToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
audio: audioBase64,
template_id: 'consulta-geral',
language: 'pt',
patient_id: patientId,
mode: 'async' // Para arquivos grandes
})
});
const result = await response.json();
if (result.success) {
// Acompanhar progresso da tarefa
await trackJobProgress(result.data.job_id);
}
} catch (error) {
console.error('Erro na transcrição:', error);
} finally {
setIsUploading(false);
}
};
const trackJobProgress = async (jobId) => {
const pollInterval = setInterval(async () => {
const statusResponse = await fetch(`/api/jobs/${jobId}`, {
headers: { 'Authorization': `Bearer ${getAccessToken()}` }
});
const statusData = await statusResponse.json();
const job = statusData.data.job;
setProgress(job.progress);
if (job.status === 'completed') {
clearInterval(pollInterval);
// Buscar resultado
const resultResponse = await fetch(`/api/jobs/${jobId}/result`, {
headers: { 'Authorization': `Bearer ${getAccessToken()}` }
});
const resultData = await resultResponse.json();
onRecordGenerated(resultData.data.medical_record);
} else if (job.status === 'failed') {
clearInterval(pollInterval);
throw new Error('Processamento falhou');
}
}, 2000);
};
return (
<div className="audio-transcription">
<h3>Transcrição de Áudio</h3>
<input
type="file"
accept="audio/*"
onChange={(e) => handleAudioUpload(e.target.files[0])}
disabled={isUploading}
/>
{isUploading && (
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${progress}%` }}
/>
<span>{progress}% processado</span>
</div>
)}
</div>
);
}
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(',')[1]);
reader.onerror = error => reject(error);
});
}
Backend Integration (Node.js/Express)
// routes/medical-records.js
const express = require('express');
const { TranscriMedClient } = require('@transcrimed/api');
const multer = require('multer');
const router = express.Router();
const upload = multer({ storage: multer.memoryStorage() });
// Middleware de autenticação
const authenticate = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'Token de acesso necessário' });
}
const token = authHeader.split(' ')[1];
req.accessToken = token;
next();
};
// Endpoint para upload e transcrição
router.post('/generate', authenticate, upload.single('audio'), async (req, res) => {
try {
const { patientId, templateId = 'consulta-geral' } = req.body;
const audioBuffer = req.file.buffer;
// Inicializar cliente TranscriMed
const client = new TranscriMedClient({
accessToken: req.accessToken
});
// Gerar registro médico
const result = await client.medicalRecords.generate({
audio: audioBuffer,
templateId,
language: 'pt',
patientId,
mode: 'async'
});
// Salvar referência da tarefa no banco de dados
await saveJobReference(result.jobId, patientId, req.user.id);
res.json({
success: true,
jobId: result.jobId,
message: 'Processamento iniciado'
});
} catch (error) {
console.error('Erro na geração:', error);
res.status(500).json({
success: false,
error: 'Falha no processamento'
});
}
});
// Endpoint para verificar status da tarefa
router.get('/jobs/:jobId/status', authenticate, async (req, res) => {
try {
const client = new TranscriMedClient({
accessToken: req.accessToken
});
const job = await client.jobs.get(req.params.jobId);
res.json({
success: true,
job: {
id: job.id,
status: job.status,
progress: job.progress,
result: job.status === 'completed' ? job.result : null
}
});
} catch (error) {
res.status(404).json({
success: false,
error: 'Tarefa não encontrada'
});
}
});
module.exports = router;
Plataforma de Telemedicina
Transcrição em Tempo Real Durante Consultas
// components/RealTimeTranscription.jsx
import React, { useState, useRef, useEffect } from 'react';
function RealTimeTranscription({ consultationId }) {
const [isRecording, setIsRecording] = useState(false);
const [transcript, setTranscript] = useState('');
const [finalRecord, setFinalRecord] = useState(null);
const mediaRecorderRef = useRef(null);
const audioChunksRef = useRef([]);
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
sampleRate: 16000,
channelCount: 1,
echoCancellation: true,
noiseSuppression: true
}
});
mediaRecorderRef.current = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
});
mediaRecorderRef.current.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data);
}
};
mediaRecorderRef.current.onstop = async () => {
const audioBlob = new Blob(audioChunksRef.current, {
type: 'audio/webm;codecs=opus'
});
await processFullRecording(audioBlob);
audioChunksRef.current = [];
};
mediaRecorderRef.current.start(1000); // Coleta chunks a cada segundo
setIsRecording(true);
} catch (error) {
console.error('Erro ao iniciar gravação:', error);
}
};
const stopRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop();
mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop());
setIsRecording(false);
}
};
const processFullRecording = async (audioBlob) => {
try {
const formData = new FormData();
formData.append('audio', audioBlob);
formData.append('consultationId', consultationId);
formData.append('templateId', 'consulta-telemedicina');
const response = await fetch('/api/consultations/transcribe', {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAccessToken()}`
},
body: formData
});
const result = await response.json();
if (result.success) {
setFinalRecord(result.data.medical_record);
// Salvar automaticamente no prontuário
await saveToPatientRecord(result.data.medical_record);
}
} catch (error) {
console.error('Erro no processamento:', error);
}
};
const saveToPatientRecord = async (medicalRecord) => {
await fetch(`/api/patients/${consultationId}/records`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAccessToken()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
recordId: medicalRecord.id,
consultationId
})
});
};
return (
<div className="real-time-transcription">
<div className="recording-controls">
<button
onClick={isRecording ? stopRecording : startRecording}
className={`record-btn ${isRecording ? 'recording' : ''}`}
>
{isRecording ? 'Parar Gravação' : 'Iniciar Gravação'}
</button>
</div>
{isRecording && (
<div className="recording-indicator">
<div className="pulse"></div>
<span>Gravando consulta...</span>
</div>
)}
{transcript && (
<div className="live-transcript">
<h4>Transcrição em Tempo Real:</h4>
<p>{transcript}</p>
</div>
)}
{finalRecord && (
<div className="final-record">
<h4>Registro Médico Gerado:</h4>
<div className="record-content">
<h5>{finalRecord.title}</h5>
<div dangerouslySetInnerHTML={{ __html: finalRecord.content }} />
</div>
<button onClick={() => downloadRecord(finalRecord)}>
Baixar Registro
</button>
</div>
)}
</div>
);
}
Sistema de Documentação Clínica
Batch Processing de Múltiplos Arquivos
# batch_processor.py
import asyncio
import aiohttp
import base64
from pathlib import Path
from typing import List, Dict
import logging
class TranscriMedBatchProcessor:
def __init__(self, access_token: str, base_url: str = "https://api.transcrimed.com.br"):
self.access_token = access_token
self.base_url = base_url
self.session = None
async def __aenter__(self):
self.session = aiohttp.ClientSession(
headers={'Authorization': f'Bearer {self.access_token}'}
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.session.close()
async def process_audio_file(self, file_path: Path, template_id: str = "consulta-geral") -> Dict:
"""Processa um único arquivo de áudio"""
try:
# Ler e codificar arquivo
with open(file_path, 'rb') as f:
audio_data = base64.b64encode(f.read()).decode('utf-8')
# Submeter para processamento
payload = {
'audio': audio_data,
'template_id': template_id,
'language': 'pt',
'mode': 'async',
'metadata': {
'source_file': file_path.name,
'batch_id': f'batch_{asyncio.current_task().get_name()}'
}
}
async with self.session.post(
f'{self.base_url}/api/v1/medical-records/generate',
json=payload
) as response:
result = await response.json()
if result['success']:
job_id = result['data']['job_id']
logging.info(f"Arquivo {file_path.name} submetido: {job_id}")
# Aguardar conclusão
final_result = await self._wait_for_completion(job_id)
return {
'file': file_path.name,
'job_id': job_id,
'status': 'completed',
'medical_record': final_result
}
else:
return {
'file': file_path.name,
'status': 'failed',
'error': result['error']
}
except Exception as e:
logging.error(f"Erro processando {file_path.name}: {e}")
return {
'file': file_path.name,
'status': 'error',
'error': str(e)
}
async def _wait_for_completion(self, job_id: str) -> Dict:
"""Aguarda conclusão de uma tarefa"""
while True:
async with self.session.get(f'{self.base_url}/api/v1/jobs/{job_id}') as response:
job_data = await response.json()
job = job_data['data']['job']
if job['status'] == 'completed':
# Buscar resultado
async with self.session.get(
f'{self.base_url}/api/v1/jobs/{job_id}/result'
) as result_response:
return await result_response.json()
elif job['status'] == 'failed':
raise Exception(f"Tarefa falhou: {job.get('error_data', 'Erro desconhecido')}")
# Aguardar antes de verificar novamente
await asyncio.sleep(5)
async def process_directory(self, directory: Path, template_mapping: Dict[str, str] = None) -> List[Dict]:
"""Processa todos os arquivos de áudio em um diretório"""
audio_extensions = {'.wav', '.mp3', '.m4a', '.flac', '.ogg'}
audio_files = [
f for f in directory.iterdir()
if f.suffix.lower() in audio_extensions
]
logging.info(f"Encontrados {len(audio_files)} arquivos de áudio")
# Mapear modelos baseado no nome do arquivo (se fornecido)
tasks = []
for file_path in audio_files:
template_id = "consulta-geral"
if template_mapping:
for pattern, template in template_mapping.items():
if pattern.lower() in file_path.name.lower():
template_id = template
break
task = asyncio.create_task(
self.process_audio_file(file_path, template_id),
name=f"process_{file_path.stem}"
)
tasks.append(task)
# Executar em lotes para evitar sobrecarga
batch_size = 5
results = []
for i in range(0, len(tasks), batch_size):
batch = tasks[i:i + batch_size]
logging.info(f"Processando lote {i//batch_size + 1}")
batch_results = await asyncio.gather(*batch, return_exceptions=True)
results.extend(batch_results)
return results
# Exemplo de uso
async def main():
# Configurar logging
logging.basicConfig(level=logging.INFO)
# Mapear tipos de consulta baseado no nome do arquivo
template_mapping = {
'cardio': 'consulta-cardiologia',
'pediatria': 'consulta-pediatria',
'gineco': 'consulta-ginecologia',
'geral': 'consulta-geral'
}
async with TranscriMedBatchProcessor('seu_token_aqui') as processor:
directory = Path('./audio_files')
results = await processor.process_directory(directory, template_mapping)
# Relatório de processamento
successful = [r for r in results if r.get('status') == 'completed']
failed = [r for r in results if r.get('status') in ['failed', 'error']]
print(f"\n=== RELATÓRIO DE PROCESSAMENTO ===")
print(f"Total de arquivos: {len(results)}")
print(f"Sucessos: {len(successful)}")
print(f"Falhas: {len(failed)}")
if failed:
print("\nArquivos com falha:")
for item in failed:
print(f"- {item['file']}: {item.get('error', 'Erro desconhecido')}")
# Salvar registros médicos
for item in successful:
record = item['medical_record']
output_file = f"output/{item['file']}.md"
Path('output').mkdir(exist_ok=True)
with open(output_file, 'w', encoding='utf-8') as f:
f.write(f"# {record['title']}\n\n")
f.write(record['content'])
print(f"Registro salvo: {output_file}")
if __name__ == "__main__":
asyncio.run(main())
Aplicação Móvel (React Native)
Gravação e Upload em Background
// services/AudioRecordingService.js
import { Audio } from 'expo-av';
import * as FileSystem from 'expo-file-system';
import { TranscriMedClient } from '@transcrimed/api-mobile';
class AudioRecordingService {
constructor(accessToken) {
this.client = new TranscriMedClient({ accessToken });
this.recording = null;
this.isRecording = false;
}
async startRecording(patientId, templateId = 'consulta-geral') {
try {
// Solicitar permissões
const permission = await Audio.requestPermissionsAsync();
if (permission.status !== 'granted') {
throw new Error('Permissão de áudio negada');
}
// Configurar modo de áudio
await Audio.setAudioModeAsync({
allowsRecordingIOS: true,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: false,
staysActiveInBackground: true,
});
// Iniciar gravação
const recordingOptions = {
android: {
extension: '.wav',
outputFormat: Audio.RECORDING_OPTION_ANDROID_OUTPUT_FORMAT_PCM_16BIT,
audioEncoder: Audio.RECORDING_OPTION_ANDROID_AUDIO_ENCODER_PCM_16BIT,
sampleRate: 16000,
numberOfChannels: 1,
bitRate: 256000,
},
ios: {
extension: '.wav',
audioQuality: Audio.RECORDING_OPTION_IOS_AUDIO_QUALITY_HIGH,
sampleRate: 16000,
numberOfChannels: 1,
bitRate: 256000,
linearPCMBitDepth: 16,
linearPCMIsBigEndian: false,
linearPCMIsFloat: false,
},
};
this.recording = new Audio.Recording();
await this.recording.prepareToRecordAsync(recordingOptions);
await this.recording.startAsync();
this.isRecording = true;
this.patientId = patientId;
this.templateId = templateId;
return { success: true };
} catch (error) {
console.error('Erro ao iniciar gravação:', error);
return { success: false, error: error.message };
}
}
async stopRecording() {
if (!this.recording || !this.isRecording) {
return { success: false, error: 'Nenhuma gravação ativa' };
}
try {
await this.recording.stopAndUnloadAsync();
const uri = this.recording.getURI();
this.isRecording = false;
// Processar em background
return await this.processRecording(uri);
} catch (error) {
console.error('Erro ao parar gravação:', error);
return { success: false, error: error.message };
}
}
async processRecording(audioUri) {
try {
// Ler arquivo de áudio
const audioBase64 = await FileSystem.readAsStringAsync(audioUri, {
encoding: FileSystem.EncodingType.Base64,
});
// Submeter para processamento
const result = await this.client.medicalRecords.generate({
audio: audioBase64,
templateId: this.templateId,
language: 'pt',
patientId: this.patientId,
mode: 'async'
});
if (result.success) {
// Salvar referência da tarefa localmente
await this.saveJobReference(result.jobId, this.patientId);
// Iniciar monitoramento em background
this.monitorJobInBackground(result.jobId);
return {
success: true,
jobId: result.jobId,
message: 'Processamento iniciado'
};
}
return result;
} catch (error) {
console.error('Erro no processamento:', error);
return { success: false, error: error.message };
} finally {
// Limpar arquivo temporário
if (audioUri) {
await FileSystem.deleteAsync(audioUri, { idempotent: true });
}
}
}
async monitorJobInBackground(jobId) {
const checkJob = async () => {
try {
const job = await this.client.jobs.get(jobId);
if (job.status === 'completed') {
const result = await this.client.jobs.getResult(jobId);
// Notificar usuário
await this.sendLocalNotification(
'Registro médico pronto!',
`O registro para ${this.patientId} foi gerado com sucesso.`
);
// Salvar resultado localmente
await this.saveRecordLocally(result.medicalRecord);
} else if (job.status === 'failed') {
await this.sendLocalNotification(
'Erro no processamento',
'Houve um erro ao processar o áudio. Tente novamente.'
);
} else {
// Continuar monitorando
setTimeout(checkJob, 10000); // 10 segundos
}
} catch (error) {
console.error('Erro ao verificar tarefa:', error);
setTimeout(checkJob, 15000); // Tentar novamente em 15 segundos
}
};
setTimeout(checkJob, 5000); // Primeira verificação em 5 segundos
}
async saveJobReference(jobId, patientId) {
const jobData = {
id: jobId,
patientId,
timestamp: new Date().toISOString(),
status: 'processing'
};
await AsyncStorage.setItem(`job_${jobId}`, JSON.stringify(jobData));
}
async saveRecordLocally(medicalRecord) {
const records = await AsyncStorage.getItem('medical_records');
const recordsList = records ? JSON.parse(records) : [];
recordsList.push({
...medicalRecord,
savedAt: new Date().toISOString()
});
await AsyncStorage.setItem('medical_records', JSON.stringify(recordsList));
}
async sendLocalNotification(title, body) {
const { Notifications } = require('expo-notifications');
await Notifications.scheduleNotificationAsync({
content: { title, body },
trigger: null, // Imediato
});
}
}
export default AudioRecordingService;
Sistema de Farmácia Hospitalar
Integração com Prescrições Médicas
<?php
// src/Services/TranscriMedPrescriptionService.php
namespace App\Services;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Cache;
class TranscriMedPrescriptionService
{
private $client;
private $accessToken;
private $baseUrl;
public function __construct()
{
$this->baseUrl = config('transcrimed.api_url');
$this->accessToken = $this->getAccessToken();
$this->client = new Client([
'base_uri' => $this->baseUrl,
'headers' => [
'Authorization' => "Bearer {$this->accessToken}",
'Content-Type' => 'application/json',
]
]);
}
/**
* Processar prescrição ditada
*/
public function processPrescriptionAudio($audioData, $patientId, $physicianId)
{
try {
$response = $this->client->post('/api/v1/medical-records/generate', [
'json' => [
'audio' => base64_encode($audioData),
'template_id' => 'prescricao-medica',
'language' => 'pt',
'mode' => 'sync',
'patient_id' => $patientId,
'metadata' => [
'physician_id' => $physicianId,
'type' => 'prescription',
'department' => 'pharmacy'
]
]
]);
$result = json_decode($response->getBody(), true);
if ($result['success']) {
$prescription = $this->extractPrescriptionData($result['data']['medical_record']);
// Validar medicamentos contra base de dados
$validatedPrescription = $this->validateMedications($prescription);
// Salvar no sistema hospitalar
return $this->savePrescriptionToHIS($validatedPrescription, $patientId, $physicianId);
}
throw new \Exception($result['error']['message']);
} catch (\Exception $e) {
Log::error('Erro no processamento de prescrição', [
'patient_id' => $patientId,
'physician_id' => $physicianId,
'error' => $e->getMessage()
]);
throw $e;
}
}
/**
* Extrair dados estruturados da prescrição
*/
private function extractPrescriptionData($medicalRecord)
{
// Usar expressões regulares para extrair medicamentos
$content = $medicalRecord['content'];
$medications = [];
// Padrão para medicamentos: Nome do medicamento + dosagem + frequência
$pattern = '/(?:prescrevo|receitar?|medicação?)\s*:?\s*(.+?)(?:\n|$)/mi';
preg_match_all($pattern, $content, $matches);
foreach ($matches[1] as $medicationLine) {
$medication = $this->parseMedicationLine($medicationLine);
if ($medication) {
$medications[] = $medication;
}
}
return [
'title' => $medicalRecord['title'],
'content' => $content,
'medications' => $medications,
'issued_at' => now(),
'transcrimed_id' => $medicalRecord['id']
];
}
/**
* Analisar linha de medicamento
*/
private function parseMedicationLine($line)
{
// Remover pontuação extra e normalizar
$line = trim($line, ".,;:");
// Padrões comuns para medicamentos
$patterns = [
// Parafuso + dosagem + frequência
'/(.+?)\s+(\d+(?:\.\d+)?(?:mg|mcg|g|ml|comprimidos?))\s+(.+)/i',
// Nome + "de" + dosagem + frequência
'/(.+?)\s+de\s+(\d+(?:\.\d+)?(?:mg|mcg|g|ml))\s+(.+)/i',
// Formato simples
'/(.+?)\s+(\d+(?:\.\d+)?(?:mg|mcg|g|ml|cp))\s*(.*)$/i'
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $line, $matches)) {
return [
'name' => trim($matches[1]),
'dosage' => trim($matches[2]),
'frequency' => trim($matches[3] ?? ''),
'original_text' => $line
];
}
}
// Se não conseguir analisar, retornar como texto livre
return [
'name' => $line,
'dosage' => '',
'frequency' => '',
'original_text' => $line,
'requires_review' => true
];
}
/**
* Validar medicamentos contra base de dados
*/
private function validateMedications($prescription)
{
foreach ($prescription['medications'] as &$medication) {
// Buscar na base de dados de medicamentos
$dbMedication = $this->searchMedicationDatabase($medication['name']);
if ($dbMedication) {
$medication['drug_id'] = $dbMedication['id'];
$medication['validated_name'] = $dbMedication['name'];
$medication['interactions'] = $this->checkInteractions($dbMedication['id']);
$medication['status'] = 'validated';
} else {
$medication['status'] = 'requires_pharmacist_review';
$medication['suggestions'] = $this->getSimilarMedications($medication['name']);
}
}
return $prescription;
}
/**
* Buscar medicamento na base de dados
*/
private function searchMedicationDatabase($medicationName)
{
// Normalizar nome para busca
$normalizedName = $this->normalizeMedicationName($medicationName);
return \DB::table('medications')
->where('normalized_name', 'LIKE', "%{$normalizedName}%")
->orWhere('alternative_names', 'LIKE', "%{$normalizedName}%")
->first();
}
/**
* Salvar prescrição no sistema hospitalar
*/
private function savePrescriptionToHIS($prescription, $patientId, $physicianId)
{
\DB::beginTransaction();
try {
// Criar registro de prescrição
$prescriptionRecord = \DB::table('prescriptions')->insertGetId([
'patient_id' => $patientId,
'physician_id' => $physicianId,
'title' => $prescription['title'],
'content' => $prescription['content'],
'transcrimed_id' => $prescription['transcrimed_id'],
'status' => 'pending_pharmacy_review',
'created_at' => now(),
'updated_at' => now()
]);
// Salvar medicamentos individualmente
foreach ($prescription['medications'] as $medication) {
\DB::table('prescription_medications')->insert([
'prescription_id' => $prescriptionRecord,
'drug_id' => $medication['drug_id'] ?? null,
'medication_name' => $medication['name'],
'dosage' => $medication['dosage'],
'frequency' => $medication['frequency'],
'status' => $medication['status'],
'requires_review' => $medication['requires_review'] ?? false,
'created_at' => now()
]);
}
// Notificar farmácia se necessário
if (collect($prescription['medications'])->contains('status', 'requires_pharmacist_review')) {
$this->notifyPharmacy($prescriptionRecord, $patientId);
}
\DB::commit();
return [
'success' => true,
'prescription_id' => $prescriptionRecord,
'requires_review' => collect($prescription['medications'])->contains('requires_review', true)
];
} catch (\Exception $e) {
\DB::rollback();
throw $e;
}
}
/**
* Obter token de acesso (com cache)
*/
private function getAccessToken()
{
return Cache::remember('transcrimed_access_token', 55, function () {
// Implementar fluxo OAuth2 para obter token
$response = $this->authenticateWithTranscriMed();
return $response['access_token'];
});
}
}
Monitoramento e Analytics
Dashboard de Métricas de Uso
// components/TranscriMedDashboard.jsx
import React, { useState, useEffect } from 'react';
import { LineChart, Line, BarChart, Bar, XAxis, YAxis, ResponsiveContainer } from 'recharts';
function TranscriMedDashboard() {
const [metrics, setMetrics] = useState(null);
const [timeRange, setTimeRange] = useState('7days');
useEffect(() => {
fetchMetrics();
}, [timeRange]);
const fetchMetrics = async () => {
try {
const response = await fetch(`/api/analytics/transcrimed?range=${timeRange}`, {
headers: { 'Authorization': `Bearer ${getAccessToken()}` }
});
const data = await response.json();
setMetrics(data);
} catch (error) {
console.error('Erro ao carregar métricas:', error);
}
};
if (!metrics) return <div>Carregando...</div>;
return (
<div className="transcrimed-dashboard">
<h2>Dashboard TranscriMed</h2>
<div className="time-range-selector">
<select value={timeRange} onChange={(e) => setTimeRange(e.target.value)}>
<option value="24hours">Últimas 24 horas</option>
<option value="7days">Últimos 7 dias</option>
<option value="30days">Últimos 30 dias</option>
</select>
</div>
<div className="metrics-grid">
<div className="metric-card">
<h3>Registros Processados</h3>
<div className="metric-value">{metrics.totalRecords}</div>
<div className="metric-change">
+{metrics.recordsGrowth}% vs período anterior
</div>
</div>
<div className="metric-card">
<h3>Tempo Médio de Processamento</h3>
<div className="metric-value">{metrics.avgProcessingTime}s</div>
<div className="metric-change">
{metrics.processingTimeChange > 0 ? '+' : ''}{metrics.processingTimeChange}% vs período anterior
</div>
</div>
<div className="metric-card">
<h3>Taxa de Sucesso</h3>
<div className="metric-value">{metrics.successRate}%</div>
</div>
<div className="metric-card">
<h3>Economia de Tempo</h3>
<div className="metric-value">{metrics.timeSaved}h</div>
<div className="metric-subtitle">vs documentação manual</div>
</div>
</div>
<div className="charts-section">
<div className="chart-container">
<h3>Volume de Processamento</h3>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={metrics.dailyVolume}>
<XAxis dataKey="date" />
<YAxis />
<Line type="monotone" dataKey="records" stroke="#8884d8" />
</LineChart>
</ResponsiveContainer>
</div>
<div className="chart-container">
<h3>Distribuição por Especialidade</h3>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={metrics.specialtyDistribution}>
<XAxis dataKey="specialty" />
<YAxis />
<Bar dataKey="count" fill="#82ca9d" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
<div className="recent-activity">
<h3>Atividade Recente</h3>
<div className="activity-list">
{metrics.recentActivity.map((activity, index) => (
<div key={index} className="activity-item">
<div className="activity-time">{activity.timestamp}</div>
<div className="activity-description">{activity.description}</div>
<div className={`activity-status ${activity.status}`}>
{activity.status}
</div>
</div>
))}
</div>
</div>
</div>
);
}
export default TranscriMedDashboard;
Implementação de Segurança Avançada
Token Management e Refresh Automático
// utils/AuthManager.js
class AuthManager {
constructor() {
this.accessToken = localStorage.getItem('transcrimed_access_token');
this.refreshToken = localStorage.getItem('transcrimed_refresh_token');
this.tokenExpiry = localStorage.getItem('transcrimed_token_expiry');
this.refreshPromise = null;
}
async getValidToken() {
// Verificar se o token está próximo do vencimento (5 minutos antes)
const now = Date.now();
const expiry = parseInt(this.tokenExpiry);
const fiveMinutes = 5 * 60 * 1000;
if (expiry && (now + fiveMinutes) >= expiry) {
return await this.refreshAccessToken();
}
return this.accessToken;
}
async refreshAccessToken() {
// Evitar múltiplas chamadas simultâneas de refresh
if (this.refreshPromise) {
return await this.refreshPromise;
}
this.refreshPromise = this._performTokenRefresh();
try {
const result = await this.refreshPromise;
return result;
} finally {
this.refreshPromise = null;
}
}
async _performTokenRefresh() {
try {
const response = await fetch('https://api.transcrimed.com.br/api/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: process.env.REACT_APP_TRANSCRIMED_CLIENT_ID,
client_secret: process.env.REACT_APP_TRANSCRIMED_CLIENT_SECRET
})
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const tokens = await response.json();
// Armazenar novos tokens
this.accessToken = tokens.access_token;
this.tokenExpiry = Date.now() + (tokens.expires_in * 1000);
localStorage.setItem('transcrimed_access_token', this.accessToken);
localStorage.setItem('transcrimed_token_expiry', this.tokenExpiry.toString());
// Refresh token pode ser rotacionado
if (tokens.refresh_token) {
this.refreshToken = tokens.refresh_token;
localStorage.setItem('transcrimed_refresh_token', this.refreshToken);
}
return this.accessToken;
} catch (error) {
console.error('Token refresh failed:', error);
// Limpar tokens inválidos e redirecionar para login
this.clearTokens();
window.location.href = '/login';
throw error;
}
}
clearTokens() {
this.accessToken = null;
this.refreshToken = null;
this.tokenExpiry = null;
localStorage.removeItem('transcrimed_access_token');
localStorage.removeItem('transcrimed_refresh_token');
localStorage.removeItem('transcrimed_token_expiry');
}
// Interceptor para requisições fetch
async authenticatedFetch(url, options = {}) {
const token = await this.getValidToken();
const authOptions = {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${token}`
}
};
const response = await fetch(url, authOptions);
// Se receber 401, tentar refresh uma vez
if (response.status === 401 && !options._retried) {
await this.refreshAccessToken();
const retryOptions = {
...authOptions,
_retried: true,
headers: {
...authOptions.headers,
'Authorization': `Bearer ${this.accessToken}`
}
};
return fetch(url, retryOptions);
}
return response;
}
}
export default new AuthManager();
Exemplos de Integração de Lista de Trabalho
Enviar Itens de Lista de Trabalho do RIS/PACS
JavaScript/Node.js
class WorklistIntegration {
constructor(accessToken) {
this.accessToken = accessToken;
this.baseUrl = 'https://api.transcrimed.com.br';
}
async sendWorklistItems(items) {
const idempotencyKey = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
try {
const response = await fetch(`${this.baseUrl}/api/v1/worklists/ingest`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(items)
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Falha na ingestão da lista de trabalho: ${error.error.message}`);
}
const result = await response.json();
console.log(`Processado com sucesso: ${result.data.inserted} inseridos, ${result.data.updated} atualizados`);
if (result.data.errors.length > 0) {
console.warn('Alguns itens tiveram erros:', result.data.errors);
}
return result;
} catch (error) {
console.error('Erro na ingestão da lista de trabalho:', error);
throw error;
}
}
async updateItemStatus(itemId, status, reason) {
try {
const response = await fetch(`${this.baseUrl}/api/v1/worklists/items/${itemId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status, reason })
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Falha na atualização de status: ${error.error.message}`);
}
return await response.json();
} catch (error) {
console.error('Erro na atualização de status:', error);
throw error;
}
}
}
// Exemplo de uso
const worklist = new WorklistIntegration('seu_token_de_acesso');
const dadosExame = [
{
accession_number: "2024-001234",
patient_id: "PAT-456789",
patient_name: "João Silva",
patient_sex: "M",
patient_birth_date: "1980-05-15",
modality: "CR",
exam_datetime: "2024-01-15T10:30:00Z",
exam_room: "Sala 1",
exam_description: "Raio-X de Tórax PA/Perfil",
study_uid: "1.2.826.0.1.3680043.6.15372.14625.20240115103000",
referring_physician: "Dr. Maria Garcia",
hospital_name: "Hospital Central",
location: "Departamento de Radiologia"
}
];
// Enviar itens da lista de trabalho
worklist.sendWorklistItems(dadosExame)
.then(result => console.log('Itens enviados com sucesso'))
.catch(error => console.error('Falha ao enviar itens:', error));
Python
import requests
import json
import uuid
from datetime import datetime
from typing import List, Dict, Any
class WorklistIntegration:
def __init__(self, access_token: str):
self.access_token = access_token
self.base_url = 'https://api.transcrimed.com.br'
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {access_token}',
'Content-Type': 'application/json'
})
def send_worklist_items(self, items: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Enviar itens de lista de trabalho para o TranscriMed"""
idempotency_key = f"{int(datetime.now().timestamp())}-{str(uuid.uuid4())[:8]}"
headers = {
'Idempotency-Key': idempotency_key
}
try:
response = self.session.post(
f'{self.base_url}/api/v1/worklists/ingest',
json=items,
headers=headers
)
response.raise_for_status()
result = response.json()
print(f"Processado com sucesso: {result['data']['inserted']} inseridos, "
f"{result['data']['updated']} atualizados")
if result['data']['errors']:
print(f"Erros: {result['data']['errors']}")
return result
except requests.exceptions.RequestException as e:
print(f"Erro na ingestão da lista de trabalho: {e}")
raise
def update_item_status(self, item_id: str, status: str, reason: str = None) -> Dict[str, Any]:
"""Atualizar status do item da lista de trabalho"""
payload = {'status': status}
if reason:
payload['reason'] = reason
try:
response = self.session.patch(
f'{self.base_url}/api/v1/worklists/items/{item_id}',
json=payload
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"Erro na atualização de status: {e}")
raise
def validate_worklist_item(self, item: Dict[str, Any]) -> List[str]:
"""Validar item da lista de trabalho antes de enviar"""
errors = []
# Pelo menos um identificador obrigatório
if not item.get('accession_number') and not item.get('study_uid'):
errors.append('accession_number ou study_uid é obrigatório')
# Validar modalidade se presente
valid_modalities = ['CR', 'CT', 'MR', 'US', 'XA', 'RF', 'DX', 'MG', 'PT', 'NM']
if item.get('modality') and item['modality'] not in valid_modalities:
errors.append(f"Modalidade inválida: {item['modality']}")
# Validar sexo se presente
valid_sex_values = ['M', 'F', 'O', 'U', 'Male', 'Female', 'MASCULINO', 'FEMININO']
if item.get('patient_sex') and item['patient_sex'] not in valid_sex_values:
errors.append(f"patient_sex inválido: {item['patient_sex']}")
return errors
# Exemplo de uso
if __name__ == "__main__":
worklist = WorklistIntegration('seu_token_de_acesso')
dados_exame = [
{
"accession_number": "2024-001234",
"patient_id": "PAT-456789",
"patient_name": "João Silva",
"patient_sex": "M",
"patient_birth_date": "1980-05-15",
"modality": "CR",
"exam_datetime": "2024-01-15T10:30:00Z",
"exam_room": "Sala 1",
"exam_description": "Raio-X de Tórax PA/Perfil",
"study_uid": "1.2.826.0.1.3680043.6.15372.14625.20240115103000",
"referring_physician": "Dr. Maria Santos",
"hospital_name": "Hospital Central",
"location": "Ala Norte - Radiologia"
}
]
# Validar itens antes de enviar
for item in dados_exame:
errors = worklist.validate_worklist_item(item)
if errors:
print(f"Erros de validação para item {item.get('accession_number', 'desconhecido')}: {errors}")
continue
# Enviar itens da lista de trabalho
try:
result = worklist.send_worklist_items(dados_exame)
print("Itens enviados com sucesso")
except Exception as e:
print(f"Falha ao enviar itens: {e}")
Manipulador de Webhook para Entrega de Documentos
Node.js/Express
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return signature === `sha256=${expectedSignature}`;
}
app.post('/webhooks/transcrimed', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = req.body;
// Verificar assinatura do webhook
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
console.error('Assinatura de webhook inválida');
return res.status(401).send('Não autorizado');
}
try {
const data = JSON.parse(payload);
console.log('Webhook recebido:', {
event: data.event,
accession_number: data.accession_number,
patient_name: data.patient?.name,
document_id: data.document?.id
});
// Processar documento completo
if (data.event === 'document.completed') {
processCompletedDocument(data);
}
res.status(200).send('OK');
} catch (error) {
console.error('Erro no processamento do webhook:', error);
res.status(400).send('Requisição Inválida');
}
});
async function processCompletedDocument(webhookData) {
const {
accession_number,
document,
patient,
metadata
} = webhookData;
try {
// Salvar documento no seu sistema
await saveDocumentToRIS({
accessionNumber: accession_number,
patientId: patient.id,
patientName: patient.name,
documentContent: document.content,
documentFormat: document.format,
createdAt: document.created_at,
processingTime: metadata.processing_duration_ms
});
console.log(`Documento salvo para acesso ${accession_number}`);
} catch (error) {
console.error(`Falha ao processar documento para ${accession_number}:`, error);
// Implementar lógica de retry ou fila de dead letter
}
}
async function saveDocumentToRIS(documentData) {
// Implementar sua integração RIS/PACS aqui
// Isso poderia ser salvar no banco de dados, chamar outra API, etc.
console.log('Salvando documento no RIS:', documentData.accessionNumber);
}
app.listen(3000, () => {
console.log('Servidor de webhook escutando na porta 3000');
});
Estes exemplos cobrem os principais casos de uso para integração da API TranscriMed em diferentes tipos de aplicações de saúde. Cada exemplo pode ser adaptado para as necessidades específicas do seu projeto.