feat(gtos): adicionar botao abrir imagem com opcoes de baixar e copiar customizados
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
This commit is contained in:
@@ -40,7 +40,7 @@ function StatusBadge({ sent, imageCount }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Modal de detalhes/edição de GTO ─────────────────────────── */
|
/* ── Modal de detalhes/edição de GTO ─────────────────────────── */
|
||||||
function GtoDetailModal({ isOpen, onClose, gto, onUpdated }) {
|
function GtoDetailModal({ isOpen, onClose, gto, onUpdated, onOpenImage }) {
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [detail, setDetail] = useState(null);
|
const [detail, setDetail] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -142,13 +142,24 @@ function GtoDetailModal({ isOpen, onClose, gto, onUpdated }) {
|
|||||||
<ThumbImg
|
<ThumbImg
|
||||||
src={`/api/images/${img.id}/thumbnail`}
|
src={`/api/images/${img.id}/thumbnail`}
|
||||||
className="preview-img"
|
className="preview-img"
|
||||||
style={{ width:'100%', height:90, objectFit:'cover', display:'block' }}
|
style={{ width:'100%', height:90, objectFit:'cover', display:'block', cursor:'pointer' }}
|
||||||
label=""
|
label=""
|
||||||
|
onClick={() => onOpenImage?.(img)}
|
||||||
/>
|
/>
|
||||||
<div style={{ padding:'4px 6px', fontSize:'0.65rem', color:'var(--text-secondary)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>
|
<div
|
||||||
|
style={{ padding:'4px 6px', fontSize:'0.65rem', color:'var(--text-secondary)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis', cursor:'pointer' }}
|
||||||
|
onClick={() => onOpenImage?.(img)}
|
||||||
|
>
|
||||||
{img.patient_name || img.filename}
|
{img.patient_name || img.filename}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
|
title="Abrir imagem"
|
||||||
|
onClick={() => onOpenImage?.(img)}
|
||||||
|
style={{ position:'absolute', top:4, left:4, background:'rgba(59,130,246,0.85)', color:'#fff', border:'none', borderRadius:4, width:22, height:22, cursor:'pointer', fontSize:11, display:'flex', alignItems:'center', justifyContent:'center' }}
|
||||||
|
>👁️</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
title="Desvincular imagem"
|
title="Desvincular imagem"
|
||||||
onClick={() => unlinkImage(img.id)}
|
onClick={() => unlinkImage(img.id)}
|
||||||
style={{ position:'absolute', top:4, right:4, background:'rgba(239,68,68,0.85)', color:'#fff', border:'none', borderRadius:4, width:22, height:22, cursor:'pointer', fontSize:11, display:'flex', alignItems:'center', justifyContent:'center' }}
|
style={{ position:'absolute', top:4, right:4, background:'rgba(239,68,68,0.85)', color:'#fff', border:'none', borderRadius:4, width:22, height:22, cursor:'pointer', fontSize:11, display:'flex', alignItems:'center', justifyContent:'center' }}
|
||||||
@@ -248,6 +259,66 @@ export default function GtosPage() {
|
|||||||
const [showSync, setShowSync] = useState(false);
|
const [showSync, setShowSync] = useState(false);
|
||||||
const [syncTab, setSyncTab] = useState('devices');
|
const [syncTab, setSyncTab] = useState('devices');
|
||||||
const [showNewPatient, setShowNewPatient] = useState(false);
|
const [showNewPatient, setShowNewPatient] = useState(false);
|
||||||
|
const [selectedImage, setSelectedImage] = useState(null);
|
||||||
|
|
||||||
|
const handleDownload = async (img) => {
|
||||||
|
try {
|
||||||
|
const url = img.filename?.startsWith('http')
|
||||||
|
? img.filename
|
||||||
|
: `/uploads/${img.filename}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
// Sanitizar nome do paciente e observação
|
||||||
|
const pName = (img.patient_name || 'paciente').trim().replace(/[\/\\:*?"<>|]/g, '').replace(/\s+/g, '_');
|
||||||
|
const obs = (img.remark || '').trim().replace(/[\/\\:*?"<>|]/g, '').replace(/\s+/g, '_');
|
||||||
|
|
||||||
|
const fileName = obs ? `${pName}-${obs}.jpg` : `${pName}.jpg`;
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = fileName;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Erro ao baixar imagem', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = (img) => {
|
||||||
|
const url = img.filename?.startsWith('http')
|
||||||
|
? img.filename
|
||||||
|
: `/uploads/${img.filename}`;
|
||||||
|
const imageObj = new Image();
|
||||||
|
imageObj.crossOrigin = 'anonymous';
|
||||||
|
imageObj.onload = () => {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = imageObj.naturalWidth;
|
||||||
|
canvas.height = imageObj.naturalHeight;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
ctx.drawImage(imageObj, 0, 0);
|
||||||
|
canvas.toBlob(async (blob) => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.write([
|
||||||
|
new ClipboardItem({ 'image/png': blob })
|
||||||
|
]);
|
||||||
|
showToast('Imagem copiada para a área de transferência!', 'success');
|
||||||
|
} catch (err) {
|
||||||
|
showToast('Erro ao copiar imagem para área de transferência.', 'error');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}, 'image/png');
|
||||||
|
} catch (e) {
|
||||||
|
showToast('Erro ao processar imagem para cópia.', 'error');
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
imageObj.onerror = () => showToast('Erro ao carregar imagem para cópia.', 'error');
|
||||||
|
imageObj.src = url;
|
||||||
|
};
|
||||||
|
|
||||||
const searchTimerRef = useRef(null);
|
const searchTimerRef = useRef(null);
|
||||||
|
|
||||||
@@ -560,12 +631,53 @@ export default function GtosPage() {
|
|||||||
|
|
||||||
{/* Modals */}
|
{/* Modals */}
|
||||||
<CreateGtoModal isOpen={showCreate} onClose={() => setShowCreate(false)} onCreated={reload} />
|
<CreateGtoModal isOpen={showCreate} onClose={() => setShowCreate(false)} onCreated={reload} />
|
||||||
<GtoDetailModal isOpen={!!detailGto} gto={detailGto} onClose={() => setDetailGto(null)} onUpdated={reload} />
|
<GtoDetailModal isOpen={!!detailGto} gto={detailGto} onClose={() => setDetailGto(null)} onUpdated={reload} onOpenImage={(img) => setSelectedImage(img)} />
|
||||||
<CreatePatientModal isOpen={showNewPatient} onClose={() => setShowNewPatient(false)} />
|
<CreatePatientModal isOpen={showNewPatient} onClose={() => setShowNewPatient(false)} />
|
||||||
<SettingsModal isOpen={showSettings} onClose={() => setShowSettings(false)} />
|
<SettingsModal isOpen={showSettings} onClose={() => setShowSettings(false)} />
|
||||||
<UserManagementModal isOpen={showUsers} onClose={() => setShowUsers(false)} />
|
<UserManagementModal isOpen={showUsers} onClose={() => setShowUsers(false)} />
|
||||||
<PluginsModal isOpen={showPlugins} onClose={() => setShowPlugins(false)} />
|
<PluginsModal isOpen={showPlugins} onClose={() => setShowPlugins(false)} />
|
||||||
<SyncModal isOpen={showSync} onClose={() => setShowSync(false)} defaultTab={syncTab} />
|
<SyncModal isOpen={showSync} onClose={() => setShowSync(false)} defaultTab={syncTab} />
|
||||||
|
{selectedImage && (
|
||||||
|
<Modal
|
||||||
|
isOpen={!!selectedImage}
|
||||||
|
onClose={() => setSelectedImage(null)}
|
||||||
|
title={selectedImage.patient_name || 'Visualizar Imagem'}
|
||||||
|
large
|
||||||
|
>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, alignItems: 'center' }}>
|
||||||
|
<div style={{ width: '100%', background: '#0b1220', borderRadius: 8, padding: 12, display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<img
|
||||||
|
src={selectedImage.filename?.startsWith('http') ? selectedImage.filename : `/uploads/${selectedImage.filename}`}
|
||||||
|
alt={selectedImage.patient_name}
|
||||||
|
style={{ maxHeight: '60vh', maxWidth: '100%', objectFit: 'contain' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selectedImage.remark && (
|
||||||
|
<div style={{ width: '100%', padding: '10px 14px', background: 'var(--bg-color)', borderRadius: 8, fontSize: '0.95rem' }}>
|
||||||
|
<strong>📝 Observação:</strong> {selectedImage.remark}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', gap: 12, width: '100%' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => handleDownload(selectedImage)}
|
||||||
|
style={{ flex: 1, height: 46, fontSize: '0.95rem', fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}
|
||||||
|
>
|
||||||
|
💾 Baixar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => handleCopy(selectedImage)}
|
||||||
|
style={{ flex: 1, height: 46, fontSize: '0.95rem', fontWeight: 600, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}
|
||||||
|
>
|
||||||
|
📋 Copiar para Área de Transferência
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user