This commit is contained in:
@@ -1 +1 @@
|
||||
2.1.29
|
||||
2.1.46
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
/**
|
||||
* Seção colapsável com chevron-down.
|
||||
* Quando fechada, o conteúdo NÃO é renderizado → zero espaço em tela.
|
||||
*
|
||||
* Props:
|
||||
* - title: string — título da seção
|
||||
* - defaultOpen: boolean (default false)
|
||||
* - children: ReactNode
|
||||
* - className: string (opcional) — classe extra no container
|
||||
* - compact: boolean (default false) — estilo menor para uso inline
|
||||
*/
|
||||
export default function CollapsibleSection({
|
||||
title,
|
||||
defaultOpen = false,
|
||||
children,
|
||||
className = '',
|
||||
compact = false,
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
|
||||
return (
|
||||
<div className={`collapsible-section${compact ? ' collapsible-compact' : ''} ${className}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="collapsible-header"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
<span className="collapsible-title">{title}</span>
|
||||
<i
|
||||
className="fa-solid fa-chevron-down collapsible-chevron"
|
||||
style={{ transform: open ? 'rotate(180deg)' : 'none' }}
|
||||
/>
|
||||
</button>
|
||||
{open && <div className="collapsible-body">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,23 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Modal from './Modal';
|
||||
import CollapsibleSection from './CollapsibleSection';
|
||||
import api from '../api/client';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
|
||||
/** Detecta viewport mobile (<= 1023px) */
|
||||
function useIsMobile() {
|
||||
const [mobile, setMobile] = useState(() =>
|
||||
typeof window !== 'undefined' ? window.innerWidth <= 1023 : false
|
||||
);
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia('(max-width: 1023px)');
|
||||
const handler = (e) => setMobile(e.matches);
|
||||
mql.addEventListener('change', handler);
|
||||
return () => mql.removeEventListener('change', handler);
|
||||
}, []);
|
||||
return mobile;
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '—';
|
||||
return new Date(dateString).toLocaleString('pt-BR', {
|
||||
@@ -13,6 +28,7 @@ function formatDate(dateString) {
|
||||
|
||||
export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
const { showToast } = useToast();
|
||||
const isMobile = useIsMobile();
|
||||
const [activeView, setActiveView] = useState('transform'); // 'transform' | 'gto'
|
||||
const [step, setStep] = useState('choose'); // 'choose' (4 posições) | 'finetune'
|
||||
const [choosePage, setChoosePage] = useState(0); // 0=rotações, 1=flip H, 2=flip V
|
||||
@@ -338,13 +354,15 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
{activeView === 'transform' && step === 'finetune' && (
|
||||
<div className="transform-view-wrapper">
|
||||
<div className="new-transform-layout">
|
||||
{/* Original */}
|
||||
<div className="transform-panel panel-original">
|
||||
<h3 className="panel-title">Original</h3>
|
||||
<div className="img-container">
|
||||
<img src={originalUrl || imageUrl} alt="Original" />
|
||||
{/* Original — escondido no mobile, mostrado via CollapsibeSection */}
|
||||
{!isMobile && (
|
||||
<div className="transform-panel panel-original">
|
||||
<h3 className="panel-title">Original</h3>
|
||||
<div className="img-container">
|
||||
<img src={originalUrl || imageUrl} alt="Original" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Preview (orientação escolhida + ajuste fino) */}
|
||||
<div className="transform-panel panel-preview">
|
||||
@@ -362,8 +380,10 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
|
||||
{/* Controles de rotação */}
|
||||
<div className="transform-sidebar">
|
||||
<h3 className="sidebar-title" style={{ fontSize: '0.65rem', textTransform: 'uppercase', letterSpacing: 1, color: 'var(--text-muted)', marginBottom: 12 }}>Ajuste</h3>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center', width: '100%' }}>
|
||||
{!isMobile && (
|
||||
<h3 className="sidebar-title" style={{ fontSize: '0.65rem', textTransform: 'uppercase', letterSpacing: 1, color: 'var(--text-muted)', marginBottom: 12 }}>Ajuste</h3>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'row' : 'column', gap: 8, alignItems: 'center', width: isMobile ? 'auto' : '100%' }}>
|
||||
{[
|
||||
{ label: '-5°', delta: -5, icon: '↺' },
|
||||
{ label: '-1°', delta: -1, icon: '↺' },
|
||||
@@ -374,7 +394,7 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
</button>
|
||||
))}
|
||||
|
||||
<div className="fine-tune-display" style={{ margin: '8px 0' }}>
|
||||
<div className="fine-tune-display" style={{ margin: isMobile ? '0 4px' : '8px 0' }}>
|
||||
{fineTuneAngle > 0 ? '+' : ''}{fineTuneAngle}°
|
||||
</div>
|
||||
|
||||
@@ -389,7 +409,7 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
))}
|
||||
|
||||
{fineTuneAngle !== 0 && (
|
||||
<button type="button" className="btn btn-danger btn-small" onClick={resetAngle} style={{ marginTop: 12, width: '100%' }}>
|
||||
<button type="button" className="btn btn-danger btn-small" onClick={resetAngle} style={{ marginTop: isMobile ? 0 : 12, width: isMobile ? 'auto' : '100%' }}>
|
||||
Reset
|
||||
</button>
|
||||
)}
|
||||
@@ -397,40 +417,92 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Original colapsável — apenas no mobile */}
|
||||
{isMobile && (
|
||||
<CollapsibleSection title="Ver Original" compact>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '8px 0' }}>
|
||||
<img
|
||||
src={originalUrl || imageUrl}
|
||||
alt="Original"
|
||||
style={{ maxWidth: '80%', maxHeight: 200, objectFit: 'contain', borderRadius: 'var(--radius-sm)' }}
|
||||
/>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
)}
|
||||
|
||||
{/* Área de ação */}
|
||||
<div className="transform-action-area">
|
||||
<div className="form-group">
|
||||
<label htmlFor="transform-remark">Nova Observação / Detalhes (Opcional):</label>
|
||||
<textarea
|
||||
id="transform-remark"
|
||||
name="remark"
|
||||
className="form-control"
|
||||
rows={2}
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
placeholder="Ex: Raio-X Dente 45..."
|
||||
/>
|
||||
{isMobile ? (
|
||||
<CollapsibleSection title="Ações e Observação">
|
||||
<div className="form-group" style={{ marginBottom: 12 }}>
|
||||
<label htmlFor="transform-remark">Observação (Opcional):</label>
|
||||
<textarea
|
||||
id="transform-remark"
|
||||
name="remark"
|
||||
className="form-control"
|
||||
rows={2}
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
placeholder="Ex: Raio-X Dente 45..."
|
||||
/>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={saveTransform}
|
||||
disabled={saving}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{saving ? 'Salvando...' : '💾 Salvar imagem e orientação'}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setActiveView('gto')} style={{ width: '100%' }}>
|
||||
📋 Vincular à GTO
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => downloadImage(true)} style={{ width: '100%' }}>
|
||||
⬇️ Baixar orientada
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => downloadImage(false)} style={{ width: '100%' }}>
|
||||
⬇️ Baixar original
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={onClose} style={{ width: '100%' }}>
|
||||
Cancelar
|
||||
</button>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
) : (
|
||||
<div className="transform-action-area">
|
||||
<div className="form-group">
|
||||
<label htmlFor="transform-remark">Nova Observação / Detalhes (Opcional):</label>
|
||||
<textarea
|
||||
id="transform-remark"
|
||||
name="remark"
|
||||
className="form-control"
|
||||
rows={2}
|
||||
value={remark}
|
||||
onChange={(e) => setRemark(e.target.value)}
|
||||
placeholder="Ex: Raio-X Dente 45..."
|
||||
/>
|
||||
</div>
|
||||
<div className="form-actions-inline" style={{ flexWrap: 'wrap', gap: 8 }}>
|
||||
<button className="btn btn-secondary" onClick={onClose}>Cancelar</button>
|
||||
<button className="btn btn-secondary" onClick={() => downloadImage(true)} title="Baixar a imagem com a orientação aplicada">
|
||||
⬇️ Baixar orientada
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => downloadImage(false)} title="Baixar a imagem original sem alterações">
|
||||
⬇️ Baixar original
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setActiveView('gto')} title="Vincular esta imagem a uma GTO">
|
||||
📋 Vincular à GTO
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={saveTransform}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Salvando...' : '💾 Salvar imagem e orientação'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-actions-inline" style={{ flexWrap: 'wrap', gap: 8 }}>
|
||||
<button className="btn btn-secondary" onClick={onClose}>Cancelar</button>
|
||||
<button className="btn btn-secondary" onClick={() => downloadImage(true)} title="Baixar a imagem com a orientação aplicada">
|
||||
⬇️ Baixar orientada
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => downloadImage(false)} title="Baixar a imagem original sem alterações">
|
||||
⬇️ Baixar original
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => setActiveView('gto')} title="Vincular esta imagem a uma GTO">
|
||||
📋 Vincular à GTO
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={saveTransform}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Salvando...' : '💾 Salvar imagem e orientação'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -438,7 +510,7 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
{activeView === 'gto' && (
|
||||
<div>
|
||||
{/* Baixar imagem atual */}
|
||||
<div className="form-actions-inline" style={{ marginBottom: 12, gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ marginBottom: 12, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-secondary btn-small" onClick={() => downloadImage(false)} title="Baixar a imagem original">
|
||||
⬇️ Baixar imagem
|
||||
</button>
|
||||
@@ -449,32 +521,54 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Criar GTO */}
|
||||
<div className="gto-create-section">
|
||||
<h3 className="section-title">Cadastrar Nova GTO</h3>
|
||||
<div className="form-actions-inline" style={{ marginTop: 12 }}>
|
||||
<input
|
||||
id="gto-number" name="gtoNumber"
|
||||
type="text" className="form-control"
|
||||
placeholder="Número/ID da GTO"
|
||||
value={gtoNumber}
|
||||
onChange={(e) => setGtoNumber(e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<input
|
||||
id="gto-desc" name="gtoDesc"
|
||||
type="text" className="form-control"
|
||||
placeholder="Descrição (Opcional)"
|
||||
value={gtoDesc}
|
||||
onChange={(e) => setGtoDesc(e.target.value)}
|
||||
style={{ flex: 2 }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={createGto}>Cadastrar</button>
|
||||
{/* Criar GTO — colapsável no mobile */}
|
||||
{isMobile ? (
|
||||
<CollapsibleSection title="Cadastrar Nova GTO">
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<input
|
||||
id="gto-number" name="gtoNumber"
|
||||
type="text" className="form-control"
|
||||
placeholder="Número/ID da GTO"
|
||||
value={gtoNumber}
|
||||
onChange={(e) => setGtoNumber(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
id="gto-desc" name="gtoDesc"
|
||||
type="text" className="form-control"
|
||||
placeholder="Descrição (Opcional)"
|
||||
value={gtoDesc}
|
||||
onChange={(e) => setGtoDesc(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={createGto} style={{ width: '100%' }}>Cadastrar</button>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
) : (
|
||||
<div className="gto-create-section">
|
||||
<h3 className="section-title">Cadastrar Nova GTO</h3>
|
||||
<div className="form-actions-inline" style={{ marginTop: 12 }}>
|
||||
<input
|
||||
id="gto-number" name="gtoNumber"
|
||||
type="text" className="form-control"
|
||||
placeholder="Número/ID da GTO"
|
||||
value={gtoNumber}
|
||||
onChange={(e) => setGtoNumber(e.target.value)}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<input
|
||||
id="gto-desc" name="gtoDesc"
|
||||
type="text" className="form-control"
|
||||
placeholder="Descrição (Opcional)"
|
||||
value={gtoDesc}
|
||||
onChange={(e) => setGtoDesc(e.target.value)}
|
||||
style={{ flex: 2 }}
|
||||
/>
|
||||
<button className="btn btn-primary" onClick={createGto}>Cadastrar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lista de GTOs */}
|
||||
<div className="gto-list-container">
|
||||
<div className="gto-list-container" style={{ marginTop: 12 }}>
|
||||
{loadingGtos ? (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: '#888' }}>Carregando GTOs...</div>
|
||||
) : gtos.length === 0 ? (
|
||||
@@ -482,9 +576,21 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
) : (
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
||||
{gtos.map((gto, idx) => (
|
||||
<li key={gto.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 15, background: idx % 2 === 0 ? '#fff' : '#f9f9f9', borderBottom: '1px solid #eee' }}>
|
||||
<li
|
||||
key={gto.id}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: isMobile ? 'column' : 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: isMobile ? 'stretch' : 'center',
|
||||
padding: isMobile ? 12 : 15,
|
||||
background: idx % 2 === 0 ? '#fff' : '#f9f9f9',
|
||||
borderBottom: '1px solid #eee',
|
||||
gap: isMobile ? 10 : 0,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: '1.05rem', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: isMobile ? '0.95rem' : '1.05rem', display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
{gto.gto_number}
|
||||
{gto.sent && (
|
||||
<span style={{ fontSize: '0.7rem', fontWeight: 700, color: '#fff', background: '#10b981', padding: '2px 8px', borderRadius: 999 }}>
|
||||
@@ -492,19 +598,22 @@ export default function TransformModal({ isOpen, onClose, image, onSaved }) {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ color: '#666', fontSize: '0.9rem' }}>{gto.description || 'Sem descrição'}</div>
|
||||
<div style={{ color: '#999', fontSize: '0.8rem', marginTop: 4 }}>📅 {formatDate(gto.created_at)}</div>
|
||||
<div style={{ color: '#666', fontSize: '0.85rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: isMobile ? '70vw' : 'none' }}>
|
||||
{gto.description || 'Sem descrição'}
|
||||
</div>
|
||||
<div style={{ color: '#999', fontSize: '0.75rem', marginTop: 3 }}>📅 {formatDate(gto.created_at)}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button className="btn btn-primary btn-small" onClick={() => linkToGto(gto.id)}>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
<button className="btn btn-primary btn-small" onClick={() => linkToGto(gto.id)} style={{ flex: isMobile ? 1 : 'none' }}>
|
||||
✅ Vincular
|
||||
</button>
|
||||
<button
|
||||
className={`btn btn-small ${gto.sent ? 'btn-secondary' : 'btn-success'}`}
|
||||
onClick={() => markGtoSent(gto.id, !gto.sent)}
|
||||
title={gto.sent ? 'Desmarcar envio' : 'Marcar GTO como enviada'}
|
||||
style={{ flex: isMobile ? 1 : 'none' }}
|
||||
>
|
||||
{gto.sent ? '↩️ Desmarcar' : '📤 Imagem enviada'}
|
||||
{gto.sent ? '↩️ Desmarcar' : '📤 Enviada'}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -621,6 +621,60 @@ body {
|
||||
.dropdown-custom { padding: 6px 8px; }
|
||||
.dropdown-custom select, .dropdown-custom input { width: 100%; }
|
||||
|
||||
/* ================================================================
|
||||
COLLAPSIBLE SECTION — Seção colapsável com chevron-down
|
||||
================================================================ */
|
||||
.collapsible-section {
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsible-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
width: 100%; padding: 12px 14px;
|
||||
background: none; border: none;
|
||||
cursor: pointer; font-family: inherit;
|
||||
font-size: 0.85rem; font-weight: 700;
|
||||
color: var(--text-secondary); text-align: left;
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.collapsible-header:hover { background: rgba(0,0,0,0.03); }
|
||||
.collapsible-header:active { background: rgba(0,0,0,0.06); }
|
||||
|
||||
.collapsible-chevron {
|
||||
font-size: 0.65rem; color: var(--text-muted);
|
||||
transition: transform 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapsible-body {
|
||||
padding: 0 14px 14px;
|
||||
animation: collapsibleIn 0.15s ease;
|
||||
}
|
||||
|
||||
.collapsible-compact {
|
||||
border: none; background: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.collapsible-compact .collapsible-header {
|
||||
padding: 8px 0;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.collapsible-compact .collapsible-body {
|
||||
padding: 0 0 8px;
|
||||
}
|
||||
|
||||
@keyframes collapsibleIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Helpers de visibilidade por viewport (desktop = padrão) */
|
||||
.show-mobile { display: none !important; }
|
||||
.hide-mobile { display: contents; }
|
||||
@@ -779,12 +833,97 @@ body {
|
||||
width: 100% !important; height: 100dvh !important;
|
||||
max-width: 100% !important; border-radius: 0 !important;
|
||||
}
|
||||
.new-transform-layout { flex-direction: column; align-items: center; }
|
||||
.transform-panel { max-width: 100%; width: 100%; }
|
||||
.transform-sidebar { width: 100%; flex-direction: row; border-radius: var(--radius-lg); justify-content: space-around; padding: 12px; }
|
||||
|
||||
/* ---- Modal header compacto ---- */
|
||||
.modal-header { padding: 12px 16px; }
|
||||
.modal-header h2 {
|
||||
font-size: 0.95rem;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
max-width: 55vw;
|
||||
}
|
||||
.modal-title-group {
|
||||
flex-wrap: wrap; gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
.modal-actions-group { gap: 4px; }
|
||||
.modal-actions-group .btn { width: 36px; height: 36px; font-size: 1rem; }
|
||||
.modal-body { padding: 12px; }
|
||||
|
||||
/* ---- TransformModal: layout mobile otimizado ---- */
|
||||
.new-transform-layout {
|
||||
flex-direction: column; align-items: center;
|
||||
gap: 12px; padding: 8px 0; min-height: auto;
|
||||
}
|
||||
.transform-panel {
|
||||
max-width: 100%; width: 100%;
|
||||
padding: 8px; border-radius: var(--radius-sm);
|
||||
}
|
||||
.transform-panel .img-container {
|
||||
max-height: none; aspect-ratio: auto;
|
||||
min-height: 200px;
|
||||
}
|
||||
.transform-panel img {
|
||||
max-width: 90%; max-height: 90%;
|
||||
}
|
||||
/* Painel original oculto por padrão no mobile (mostrado via CollapsibleSection) */
|
||||
.transform-panel.panel-original.mobile-collapsible {
|
||||
display: none;
|
||||
}
|
||||
.panel-title { font-size: 0.85rem; margin-bottom: 8px; }
|
||||
|
||||
/* Barra de controles de rotação — horizontal fixa no bottom */
|
||||
.transform-sidebar {
|
||||
width: 100%; flex-direction: row;
|
||||
border-radius: var(--radius-lg);
|
||||
justify-content: space-around;
|
||||
padding: 10px; gap: 6px;
|
||||
position: sticky; bottom: 0;
|
||||
z-index: 10;
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: 0 -4px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
.sidebar-btn { width: 48px; height: 48px; }
|
||||
.sidebar-btn .icon { font-size: 1rem; }
|
||||
.sidebar-btn .label { font-size: 0.6rem; }
|
||||
.fine-tune-display { margin: 0 !important; min-width: 50px; }
|
||||
|
||||
/* Área de ação do transform — compacta */
|
||||
.transform-action-area {
|
||||
margin-top: 12px; padding: 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
/* Escolha de orientação — thumbnails */
|
||||
.transform-choose-grid {
|
||||
grid-template-columns: repeat(4, 1fr) !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
/* ---- GTO dentro do TransformModal: cards compactos ---- */
|
||||
.gto-create-section { padding: 14px; }
|
||||
.gto-create-section .form-actions-inline {
|
||||
flex-direction: column;
|
||||
}
|
||||
.gto-create-section .form-control { width: 100% !important; flex: none !important; }
|
||||
.gto-link-section { flex-direction: column; gap: 10px; align-items: stretch; }
|
||||
|
||||
/* ---- Cards de paciente: ocultar remark no mobile ---- */
|
||||
.image-doctor-remark { display: none; }
|
||||
|
||||
/* ---- Extra info panel compacto ---- */
|
||||
.extra-info-panel { padding: 10px 14px; font-size: 0.85rem; margin-bottom: 12px; }
|
||||
|
||||
/* ---- Botões maiores para toque ---- */
|
||||
.btn { min-height: 40px; }
|
||||
|
||||
/* ---- Form actions empilham no mobile ---- */
|
||||
.form-actions-inline { flex-direction: column; gap: 8px; }
|
||||
.form-actions-inline .btn { width: 100%; }
|
||||
|
||||
/* ---- Toast no mobile ---- */
|
||||
.toast-container { bottom: 16px; right: 16px; left: 16px; }
|
||||
.toast { max-width: 100%; font-size: 0.9rem; padding: 12px 16px; }
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
@@ -795,10 +934,23 @@ body {
|
||||
.images-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
|
||||
.image-preview { height: 130px; }
|
||||
.image-info { padding: 10px; }
|
||||
.image-doctor-remark { display: none; } /* economiza espaço no card compacto */
|
||||
.image-patient-name { font-size: 0.85rem; }
|
||||
.image-meta { font-size: 0.75rem; }
|
||||
.stat-card-row > * { flex: 1 1 100% !important; }
|
||||
.header h1 { font-size: 1.15rem; }
|
||||
.modal-header h2 { font-size: 1.05rem; }
|
||||
.modal-header h2 { font-size: 0.9rem; max-width: 45vw; }
|
||||
.modal-actions-group .btn { width: 32px; height: 32px; font-size: 0.9rem; }
|
||||
|
||||
/* TransformModal — preview ainda maior */
|
||||
.transform-panel img { max-width: 95%; max-height: 95%; }
|
||||
.sidebar-btn { width: 44px; height: 44px; }
|
||||
|
||||
/* Paginação: só anterior/próximo */
|
||||
.pagination-controls .btn:not(:first-child):not(:last-child) { display: none; }
|
||||
|
||||
/* Collapsible mais compacto */
|
||||
.collapsible-header { padding: 10px 12px; font-size: 0.78rem; }
|
||||
.collapsible-body { padding: 0 12px 12px; }
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
@@ -811,4 +963,6 @@ body {
|
||||
.mobile-topbar-brand span:not(.mobile-topbar-logo) { font-size: 0.95rem; }
|
||||
.responsive-table td { font-size: 0.85rem; }
|
||||
.btn { font-size: 0.88rem; padding: 9px 14px; }
|
||||
.sidebar-btn { width: 40px; height: 40px; }
|
||||
.sidebar-btn .label { display: none; }
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ export default function AdminClinicsPage() {
|
||||
</header>
|
||||
<div className="content-scroll">
|
||||
{/* Stats */}
|
||||
<div style={{ display: 'flex', gap: 16, marginBottom: 24, flexWrap: 'wrap' }}>
|
||||
<div className="stat-card-row" style={{ display: 'flex', gap: 16, marginBottom: 24, flexWrap: 'wrap' }}>
|
||||
{[
|
||||
{ label: 'Total de Dispositivos', value: total, color: '#4f46e5' },
|
||||
{ label: 'Dispositivos Ativos', value: active, color: '#10b981' },
|
||||
|
||||
@@ -12,6 +12,8 @@ import UserManagementModal from '../components/UserManagementModal';
|
||||
import PluginsModal from '../components/PluginsModal';
|
||||
import SyncModal from '../components/SyncModal';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import CollapsibleSection from '../components/CollapsibleSection';
|
||||
import DropdownMenu from '../components/DropdownMenu';
|
||||
|
||||
function formatDate(d) {
|
||||
if (!d) return '—';
|
||||
@@ -273,26 +275,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
{view === 'patients' && (
|
||||
<div className="header-actions">
|
||||
<select
|
||||
className="client-filter"
|
||||
value={selectedClient}
|
||||
onChange={(e) => setSelectedClient(e.target.value)}
|
||||
style={{ height: 42 }}
|
||||
>
|
||||
<option value="">📋 Todos os Clientes</option>
|
||||
{clientsList.map((c) => (
|
||||
<option key={c.name} value={c.name}>
|
||||
{c.name === 'Upload Web' ? 'Upload Web 💻 (Imagens do Painel)' : `${c.name}${c.status === 'identified' ? ' ✅' : c.status === 'historic' ? ' 📚' : ''}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setShowDisabled(!showDisabled)}
|
||||
style={{ height: 42, padding: '10px 16px', display: 'flex', alignItems: 'center', gap: 6 }}
|
||||
>
|
||||
👁️ {showDisabled ? 'Ver Ativas' : 'Ver Ocultas'}
|
||||
</button>
|
||||
{/* Busca sempre visível */}
|
||||
<input
|
||||
type="search"
|
||||
className="search-input"
|
||||
@@ -301,9 +284,60 @@ export default function DashboardPage() {
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
style={{ height: 42, minWidth: 200 }}
|
||||
/>
|
||||
<button className="btn btn-secondary" onClick={() => loadPatients()} style={{ height: 42, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '10px 16px' }}>
|
||||
🔄
|
||||
</button>
|
||||
{/* Desktop: filtros inline */}
|
||||
<span className="hide-mobile">
|
||||
<select
|
||||
className="client-filter"
|
||||
value={selectedClient}
|
||||
onChange={(e) => setSelectedClient(e.target.value)}
|
||||
style={{ height: 42 }}
|
||||
>
|
||||
<option value="">📋 Todos os Clientes</option>
|
||||
{clientsList.map((c) => (
|
||||
<option key={c.name} value={c.name}>
|
||||
{c.name === 'Upload Web' ? 'Upload Web 💻 (Imagens do Painel)' : `${c.name}${c.status === 'identified' ? ' ✅' : c.status === 'historic' ? ' 📚' : ''}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setShowDisabled(!showDisabled)}
|
||||
style={{ height: 42, padding: '10px 16px', display: 'flex', alignItems: 'center', gap: 6 }}
|
||||
>
|
||||
👁️ {showDisabled ? 'Ver Ativas' : 'Ver Ocultas'}
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => loadPatients()} style={{ height: 42, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '10px 16px' }}>
|
||||
🔄
|
||||
</button>
|
||||
</span>
|
||||
{/* Mobile: ações em dropdown */}
|
||||
<span className="show-mobile">
|
||||
<DropdownMenu
|
||||
label="Ações"
|
||||
buttonClassName="btn btn-primary"
|
||||
items={[
|
||||
{ label: showDisabled ? 'Ver Ativas' : 'Ver Ocultas', icon: '👁️', onClick: () => setShowDisabled(!showDisabled) },
|
||||
{ label: 'Atualizar', icon: '🔄', onClick: () => loadPatients() },
|
||||
]}
|
||||
>
|
||||
{/* Filtro de clientes dentro do dropdown */}
|
||||
<div className="dropdown-custom">
|
||||
<select
|
||||
className="client-filter"
|
||||
value={selectedClient}
|
||||
onChange={(e) => setSelectedClient(e.target.value)}
|
||||
style={{ height: 38, fontSize: '0.85rem', width: '100%' }}
|
||||
>
|
||||
<option value="">📋 Todos os Clientes</option>
|
||||
{clientsList.map((c) => (
|
||||
<option key={c.name} value={c.name}>
|
||||
{c.name === 'Upload Web' ? 'Upload Web 💻' : `${c.name}${c.status === 'identified' ? ' ✅' : c.status === 'historic' ? ' 📚' : ''}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{view === 'patient-images' && (
|
||||
|
||||
@@ -128,7 +128,7 @@ export default function SettingsPage() {
|
||||
|
||||
{!loadingCron && cronJobs.length > 0 && (
|
||||
<div style={{ overflowX: 'auto' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
||||
<table className="responsive-table" style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
{['Nome', 'Descrição', 'Intervalo', 'Status', 'Última execução', 'Próxima execução', 'Resultado', ''].map(col => (
|
||||
@@ -145,28 +145,28 @@ export default function SettingsPage() {
|
||||
<tbody>
|
||||
{cronJobs.map(job => (
|
||||
<tr key={job.key} style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<td style={{ padding: '14px 16px', fontWeight: 600, whiteSpace: 'nowrap' }}>{job.name}</td>
|
||||
<td style={{ padding: '14px 16px', color: 'var(--text-secondary)', maxWidth: 240, fontSize: '0.82rem' }}>{job.description}</td>
|
||||
<td style={{ padding: '14px 16px', whiteSpace: 'nowrap', color: 'var(--text-secondary)' }}>
|
||||
<td data-label="Nome" style={{ padding: '14px 16px', fontWeight: 600, whiteSpace: 'nowrap' }}>{job.name}</td>
|
||||
<td data-label="Descrição" style={{ padding: '14px 16px', color: 'var(--text-secondary)', maxWidth: 240, fontSize: '0.82rem' }}>{job.description}</td>
|
||||
<td data-label="Intervalo" style={{ padding: '14px 16px', whiteSpace: 'nowrap', color: 'var(--text-secondary)' }}>
|
||||
A cada {job.intervalMinutes} min
|
||||
</td>
|
||||
<td style={{ padding: '14px 16px' }}>
|
||||
<td data-label="Status" style={{ padding: '14px 16px' }}>
|
||||
<StatusBadge running={job.running} lastResult={job.lastResult} />
|
||||
</td>
|
||||
<td style={{ padding: '14px 16px', whiteSpace: 'nowrap', color: 'var(--text-secondary)', fontSize: '0.82rem' }}>
|
||||
<td data-label="Última execução" style={{ padding: '14px 16px', whiteSpace: 'nowrap', color: 'var(--text-secondary)', fontSize: '0.82rem' }}>
|
||||
{fmtDate(job.lastRun)}
|
||||
</td>
|
||||
<td style={{ padding: '14px 16px', whiteSpace: 'nowrap', color: 'var(--text-secondary)', fontSize: '0.82rem' }}>
|
||||
<td data-label="Próxima" style={{ padding: '14px 16px', whiteSpace: 'nowrap', color: 'var(--text-secondary)', fontSize: '0.82rem' }}>
|
||||
{fmtDate(job.nextRun)}
|
||||
</td>
|
||||
<td style={{ padding: '14px 16px', fontSize: '0.82rem', color: 'var(--text-secondary)' }}>
|
||||
<td data-label="Resultado" style={{ padding: '14px 16px', fontSize: '0.82rem', color: 'var(--text-secondary)' }}>
|
||||
{job.lastResult ? (
|
||||
job.lastResult.status === 'error'
|
||||
? <span style={{ color: '#ef4444' }}>{job.lastResult.error}</span>
|
||||
: `${job.lastResult.total} imagem(ns) processada(s)`
|
||||
) : '—'}
|
||||
</td>
|
||||
<td style={{ padding: '14px 16px' }}>
|
||||
<td data-label="Ações" style={{ padding: '14px 16px' }}>
|
||||
<button
|
||||
className="btn btn-small btn-secondary"
|
||||
style={{ whiteSpace: 'nowrap', padding: '6px 12px', fontSize: '0.8rem' }}
|
||||
|
||||
Reference in New Issue
Block a user