fix(mobile): refactor headers and orientation choose UI to avoid layout break
continuous-integration/webhook Deploy concluído (VPS4)
continuous-integration/webhook Deploy concluído (VPS4)
This commit is contained in:
@@ -1 +1 @@
|
|||||||
2.1.48
|
2.1.49
|
||||||
|
|||||||
@@ -762,10 +762,10 @@ body {
|
|||||||
|
|
||||||
/* ---- Header da página: empilha, busca full-width ---- */
|
/* ---- Header da página: empilha, busca full-width ---- */
|
||||||
.header { padding: 12px 16px; }
|
.header { padding: 12px 16px; }
|
||||||
.header-content { flex-direction: column; align-items: stretch; gap: 12px; }
|
.header-content { flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; }
|
||||||
.header-left { width: 100%; }
|
.header-left { flex: 1; min-width: 50%; }
|
||||||
.header h1 { font-size: 1.25rem; }
|
.header h1 { font-size: 1.15rem; }
|
||||||
.header-actions { width: 100%; flex-wrap: wrap; gap: 8px; }
|
.header-actions { width: auto; flex: none; flex-wrap: nowrap; gap: 8px; align-items: center; }
|
||||||
.search-input { min-width: 0; flex: 1 1 100%; width: 100%; font-size: 16px; } /* 16px evita zoom iOS */
|
.search-input { min-width: 0; flex: 1 1 100%; width: 100%; font-size: 16px; } /* 16px evita zoom iOS */
|
||||||
.client-filter { flex: 1 1 100%; min-width: 0; width: 100%; }
|
.client-filter { flex: 1 1 100%; min-width: 0; width: 100%; }
|
||||||
|
|
||||||
@@ -773,6 +773,22 @@ body {
|
|||||||
.hide-mobile { display: none !important; }
|
.hide-mobile { display: none !important; }
|
||||||
.show-mobile { display: inline-flex !important; }
|
.show-mobile { display: inline-flex !important; }
|
||||||
|
|
||||||
|
.mobile-icon-actions { display: flex !important; gap: 6px; align-items: center; }
|
||||||
|
.btn-icon {
|
||||||
|
width: 40px !important;
|
||||||
|
height: 40px !important;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 !important;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.btn-icon:active { background: var(--hover-color); transform: scale(0.95); }
|
||||||
|
|
||||||
/* ---- Grid de cards: thumbnails menores ---- */
|
/* ---- Grid de cards: thumbnails menores ---- */
|
||||||
.images-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
|
.images-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 14px; }
|
||||||
.image-preview { height: 150px; }
|
.image-preview { height: 150px; }
|
||||||
|
|||||||
@@ -311,34 +311,11 @@ export default function DashboardPage() {
|
|||||||
🔄
|
🔄
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
{/* Mobile: ações em dropdown */}
|
{/* Mobile: botões de ação em linha */}
|
||||||
<span className="show-mobile">
|
<span className="show-mobile mobile-icon-actions">
|
||||||
<DropdownMenu
|
<button className="btn-icon" onClick={() => setIsSearchMobileOpen(!isSearchMobileOpen)} title="Pesquisar">🔍</button>
|
||||||
label="Ações"
|
<button className="btn-icon" onClick={() => setShowDisabled(!showDisabled)} title={showDisabled ? 'Ver Ativas' : 'Ver Ocultas'}>👁️</button>
|
||||||
buttonClassName="btn btn-primary"
|
<button className="btn-icon" onClick={() => loadPatients()} title="Atualizar">🔄</button>
|
||||||
items={[
|
|
||||||
{ label: 'Pesquisar', icon: '🔍', onClick: () => setIsSearchMobileOpen(!isSearchMobileOpen) },
|
|
||||||
{ 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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -348,8 +325,24 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{view === 'patients' && isSearchMobileOpen && (
|
|
||||||
<div className="show-mobile" style={{ padding: '0 20px 15px', width: '100%' }}>
|
{/* Linha adicional Mobile para Filtros e Busca */}
|
||||||
|
{view === 'patients' && isMobile && (
|
||||||
|
<div className="show-mobile" style={{ padding: '0 20px 10px', width: '100%', display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||||
|
<select
|
||||||
|
className="client-filter form-control"
|
||||||
|
value={selectedClient}
|
||||||
|
onChange={(e) => setSelectedClient(e.target.value)}
|
||||||
|
style={{ width: '100%', height: 44 }}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
{isSearchMobileOpen && (
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="form-control"
|
className="form-control"
|
||||||
@@ -359,6 +352,7 @@ export default function DashboardPage() {
|
|||||||
style={{ width: '100%', height: 44, fontSize: '1rem' }}
|
style={{ width: '100%', height: 44, fontSize: '1rem' }}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -351,16 +351,11 @@ export default function GtosPage() {
|
|||||||
<button className="btn btn-secondary" style={{ height:42, padding:'10px 14px', marginLeft:8 }} onClick={reload}>🔄</button>
|
<button className="btn btn-secondary" style={{ height:42, padding:'10px 14px', marginLeft:8 }} onClick={reload}>🔄</button>
|
||||||
</span>
|
</span>
|
||||||
{/* Mobile: ações em menu flutuante */}
|
{/* Mobile: ações em menu flutuante */}
|
||||||
<span className="show-mobile">
|
{/* Mobile: ações com icon buttons */}
|
||||||
<DropdownMenu
|
<span className="show-mobile mobile-icon-actions">
|
||||||
label="Ações"
|
<button className="btn-icon" onClick={() => setIsSearchMobileOpen(!isSearchMobileOpen)} title="Pesquisar">🔍</button>
|
||||||
buttonClassName="btn btn-primary"
|
<button className="btn-icon" onClick={() => setShowCreate(true)} title="Nova GTO">➕</button>
|
||||||
items={[
|
<button className="btn-icon" onClick={reload} title="Atualizar">🔄</button>
|
||||||
{ label:'Pesquisar', icon:'🔍', onClick:() => setIsSearchMobileOpen(!isSearchMobileOpen) },
|
|
||||||
{ label:'Nova GTO', icon:'➕', onClick:() => setShowCreate(true) },
|
|
||||||
{ label:'Atualizar lista', icon:'🔄', onClick:reload },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -281,64 +281,57 @@ export default function ImageDetailsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Gerencia opções (4 no total). Se mobile + landscape(horizontal) divide em 2 páginas.
|
// Gerencia opções (4 no total). No mobile exibe APENAS UMA GIGANTE por vez.
|
||||||
const allRotations = [0, 90, 180, 270];
|
const allRotations = [0, 90, 180, 270];
|
||||||
const useTwoCards = isMobile && !isPortrait;
|
const useOneCard = isMobile;
|
||||||
const currentRotations = useTwoCards
|
const currentRotations = useOneCard ? [allRotations[orientationSubpage]] : allRotations;
|
||||||
? allRotations.slice(orientationSubpage * 2, (orientationSubpage * 2) + 2)
|
|
||||||
: allRotations;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="layout">
|
<div className="layout">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="main-content">
|
<main className="main-content">
|
||||||
{/* Top Header para navegação / Voltar */}
|
{/* Top Header para navegação / Voltar */}
|
||||||
<header className="header" style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
{/* Top Header para navegação / Voltar */}
|
||||||
<button className="btn btn-secondary" onClick={() => navigate('/patients')} title="Voltar ao Painel">
|
<header className="header" style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||||
← Voltar
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, width: '100%' }}>
|
||||||
|
<button className="btn btn-secondary" onClick={() => navigate('/patients')} style={{ padding: '8px 12px' }} title="Voltar ao Painel">
|
||||||
|
{isMobile ? '←' : '← Voltar'}
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<h1 style={{ fontSize: '1.1rem', margin: 0, color: 'var(--text-primary)' }}>
|
<h1 style={{ fontSize: '1.1rem', margin: 0, color: 'var(--text-primary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
{image.patient_name || 'Desconhecido'}
|
{image.patient_name || 'Desconhecido'}
|
||||||
</h1>
|
</h1>
|
||||||
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
|
<div style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
|
||||||
{formatDate(image.created_at)}
|
{formatDate(image.created_at)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{!isMobile && (
|
||||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8, alignItems: 'center' }}>
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||||
{activeView === 'transform' && step === 'finetune' && (
|
{activeView === 'transform' && step === 'finetune' && (
|
||||||
<button
|
<button type="button" className="btn btn-secondary btn-small" onClick={() => setStep('choose')} title="Voltar para escolher a orientação">← Posições</button>
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary btn-small"
|
|
||||||
onClick={() => setStep('choose')}
|
|
||||||
title="Voltar para escolher a orientação"
|
|
||||||
>← Posições</button>
|
|
||||||
)}
|
)}
|
||||||
<button
|
<button className={`btn btn-small ${activeView === 'transform' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setActiveView('transform')} title="Ajustar Imagem">🔄 Ajustar</button>
|
||||||
className={`btn btn-small ${activeView === 'transform' ? 'btn-primary' : 'btn-secondary'}`}
|
<button className={`btn btn-small ${activeView === 'gto' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setActiveView('gto')} title="Gerenciar GTOs">📋 GTOs</button>
|
||||||
onClick={() => setActiveView('transform')}
|
<button className={`btn btn-small ${showInfo ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setShowInfo(!showInfo)} title="Informações">ℹ️ Info</button>
|
||||||
title="Ajustar Imagem"
|
|
||||||
>🔄 Ajustar</button>
|
|
||||||
<button
|
|
||||||
className={`btn btn-small ${activeView === 'gto' ? 'btn-primary' : 'btn-secondary'}`}
|
|
||||||
onClick={() => setActiveView('gto')}
|
|
||||||
title="Gerenciar GTOs"
|
|
||||||
>📋 GTOs</button>
|
|
||||||
<button
|
|
||||||
className={`btn btn-small ${showInfo ? 'btn-primary' : 'btn-secondary'}`}
|
|
||||||
onClick={() => setShowInfo(!showInfo)}
|
|
||||||
title="Informações"
|
|
||||||
>ℹ️ Info</button>
|
|
||||||
{image.original_image_id && (
|
{image.original_image_id && (
|
||||||
<button
|
<button className="btn btn-small btn-danger" onClick={resetToOriginal} disabled={resetting} title="Resetar para Original">↩️ Reset</button>
|
||||||
className="btn btn-small btn-danger"
|
|
||||||
onClick={resetToOriginal}
|
|
||||||
disabled={resetting}
|
|
||||||
title="Resetar para Original"
|
|
||||||
>↩️ Reset</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isMobile && (
|
||||||
|
<div style={{ display: 'flex', gap: 8, width: '100%', overflowX: 'auto', paddingBottom: 4 }}>
|
||||||
|
{activeView === 'transform' && step === 'finetune' && (
|
||||||
|
<button type="button" className="btn btn-secondary btn-small" onClick={() => setStep('choose')} style={{ flexShrink: 0 }}>← Posições</button>
|
||||||
|
)}
|
||||||
|
<button className={`btn btn-small ${activeView === 'transform' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setActiveView('transform')} style={{ flex: 1, flexShrink: 0 }}>🔄 Ajustar</button>
|
||||||
|
<button className={`btn btn-small ${activeView === 'gto' ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setActiveView('gto')} style={{ flex: 1, flexShrink: 0 }}>📋 GTOs</button>
|
||||||
|
<button className={`btn btn-small ${showInfo ? 'btn-primary' : 'btn-secondary'}`} onClick={() => setShowInfo(!showInfo)} style={{ flex: 1, flexShrink: 0 }}>ℹ️ Info</button>
|
||||||
|
{image.original_image_id && (
|
||||||
|
<button className="btn btn-small btn-danger" onClick={resetToOriginal} disabled={resetting} style={{ flexShrink: 0 }}>↩️ Reset</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="content-scroll" style={{ padding: isMobile ? '10px' : '20px' }}>
|
<div className="content-scroll" style={{ padding: isMobile ? '10px' : '20px' }}>
|
||||||
@@ -378,52 +371,50 @@ export default function ImageDetailsPage() {
|
|||||||
>Categoria →</button>
|
>Categoria →</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controles de Sub-página (quando horizontal no mobile para mostrar só 2) */}
|
{/* Controles de Rotação (Mobile exibe 1 por vez) */}
|
||||||
{useTwoCards && (
|
{useOneCard && (
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 16, marginBottom: 16 }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 16, marginBottom: 16 }}>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-small"
|
className="btn btn-primary btn-small"
|
||||||
disabled={orientationSubpage === 0}
|
onClick={() => setOrientationSubpage((p) => (p - 1 + 4) % 4)}
|
||||||
onClick={() => setOrientationSubpage(0)}
|
>← Opção Anterior</button>
|
||||||
>← Opções Anteriores</button>
|
<span style={{ fontSize: '0.9rem', color: 'var(--text-primary)', fontWeight: 600 }}>{orientationSubpage + 1} de 4</span>
|
||||||
<span style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{orientationSubpage + 1}/2</span>
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-small"
|
className="btn btn-primary btn-small"
|
||||||
disabled={orientationSubpage === 1}
|
onClick={() => setOrientationSubpage((p) => (p + 1) % 4)}
|
||||||
onClick={() => setOrientationSubpage(1)}
|
>Próxima Opção →</button>
|
||||||
>Mais Opções →</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: useTwoCards ? '1fr' : (isMobile ? 'repeat(2, 1fr)' : 'repeat(4, 1fr)'), gap: 16 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: useOneCard ? '1fr' : 'repeat(4, 1fr)', gap: 16 }}>
|
||||||
{currentRotations.map((deg) => {
|
{currentRotations.map((deg) => {
|
||||||
const pg = choosePages[choosePage];
|
const pg = choosePages[choosePage];
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
key={deg}
|
key={deg}
|
||||||
type="button"
|
|
||||||
onClick={() => pickOrientation(deg)}
|
|
||||||
style={{
|
style={{
|
||||||
border: '2px solid var(--glass-border)', borderRadius: 10, background: '#0b1220',
|
border: '1px solid var(--border-color)', borderRadius: 10, background: 'var(--bg-color)',
|
||||||
padding: useTwoCards ? 20 : 16, cursor: 'pointer', display: 'flex', flexDirection: 'column',
|
padding: useOneCard ? 0 : 16, display: 'flex', flexDirection: 'column',
|
||||||
alignItems: 'center', gap: 12, transition: 'all .2s ease'
|
alignItems: 'center', transition: 'all .2s ease', overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--primary-color)'; e.currentTarget.style.transform = 'translateY(-2px)'; }}
|
|
||||||
onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--glass-border)'; e.currentTarget.style.transform = 'translateY(0)'; }}
|
|
||||||
title={`Usar ${deg}° ${pg.flipH ? '(espelho H)' : pg.flipV ? '(espelho V)' : ''}`}
|
|
||||||
>
|
>
|
||||||
<div style={{ width: '100%', aspectRatio: useTwoCards ? 'auto' : '1 / 1', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<div style={{ width: '100%', aspectRatio: useOneCard ? 'auto' : '1 / 1', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: useOneCard ? 20 : 0, background: '#0b1220' }}>
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={`${deg}°`}
|
alt={`${deg}°`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ width: useTwoCards ? '90%' : '80%', maxHeight: useTwoCards ? 300 : '100%', objectFit: 'contain', transform: cssTransform(deg, pg.flipH, pg.flipV), transformOrigin: 'center' }}
|
style={{ width: useOneCard ? '100%' : '80%', maxHeight: useOneCard ? '50vh' : '100%', objectFit: 'contain', transform: cssTransform(deg, pg.flipH, pg.flipV), transformOrigin: 'center' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span style={{ fontSize: '0.9rem', fontWeight: 600, color: 'var(--text-secondary)' }}>
|
<div style={{ padding: '16px', display: 'flex', flexDirection: 'column', gap: 8, alignItems: 'center', width: '100%', background: 'var(--bg-surface)' }}>
|
||||||
|
<span style={{ fontSize: '1rem', fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||||
{deg}°{pg.flipH ? ' ⇆' : pg.flipV ? ' ⇅' : ''}
|
{deg}°{pg.flipH ? ' ⇆' : pg.flipV ? ' ⇅' : ''}
|
||||||
</span>
|
</span>
|
||||||
|
<button className="btn btn-primary" style={{ width: '100%' }} onClick={() => pickOrientation(deg)}>
|
||||||
|
✅ Escolher esta
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -127,16 +127,10 @@ export default function PatientsPage() {
|
|||||||
🔄
|
🔄
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<span className="show-mobile">
|
<span className="show-mobile mobile-icon-actions">
|
||||||
<DropdownMenu
|
<button className="btn-icon" onClick={() => setIsSearchMobileOpen(!isSearchMobileOpen)} title="Pesquisar">🔍</button>
|
||||||
label="Ações"
|
<button className="btn-icon" onClick={() => setShowCreate(true)} title="Novo Paciente">➕</button>
|
||||||
buttonClassName="btn btn-primary"
|
<button className="btn-icon" onClick={() => loadPatients(true)} title="Atualizar">🔄</button>
|
||||||
items={[
|
|
||||||
{ label: 'Pesquisar', icon: '🔍', onClick: () => setIsSearchMobileOpen(!isSearchMobileOpen) },
|
|
||||||
{ label: 'Novo Paciente', icon: '➕', onClick: () => setShowCreate(true) },
|
|
||||||
{ label: 'Atualizar lista', icon: '🔄', onClick: () => loadPatients(true) },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user