feat(ui): padroniza abas no segmented control (componente SegmentedTabs)
Cria components/SegmentedTabs.tsx (controle segmentado padrão, responsivo, com cor de destaque + ícone + badge) e converte as barras de aba que usavam sublinhado ou pills soltos: Comissões, Glosas, Lab Clientes, Prótese (OS + cotações), Contratos, Profissionais (marketplace) e HorariosConfig. Telas que já usavam o visual (Gestão de Equipe, Notificações, RX, Salas) ficam como referência. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
// ownership: horário/feriado da clínica = dono; horário do dentista = ele ou dono).
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { X, Clock, CalendarOff, Plus, Trash2, Save, Loader2, User, Copy, AlertTriangle } from 'lucide-react';
|
||||
import { SegmentedTabs } from './SegmentedTabs.tsx';
|
||||
|
||||
const API = (import.meta as any).env?.VITE_API_URL || '/api';
|
||||
const BASE = `${API}/nw/agenda-config`;
|
||||
@@ -204,13 +205,10 @@ export const HorariosConfig: React.FC<{ isOpen: boolean; onClose: () => void; cl
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-1 px-8 pt-4 border-b border-gray-50">
|
||||
{(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas', 'situacoes'] as const)).map((t) => (
|
||||
<button key={t} onClick={() => setAba(t)}
|
||||
className={`px-4 py-2.5 text-xs font-black uppercase tracking-wide rounded-t-xl transition-colors ${aba === t ? 'bg-teal-600 text-white' : 'text-gray-400 hover:text-gray-600'}`}>
|
||||
{t === 'clinica' ? 'Clínica' : t === 'dentistas' ? 'Dentistas' : 'Situações'}
|
||||
</button>
|
||||
))}
|
||||
<div className="px-8 pt-4">
|
||||
<SegmentedTabs
|
||||
tabs={(soDentista ? (['dentistas'] as const) : (['clinica', 'dentistas', 'situacoes'] as const)).map((t) => ({ id: t, label: t === 'clinica' ? 'Clínica' : t === 'dentistas' ? 'Dentistas' : 'Situações' }))}
|
||||
value={aba} onChange={setAba} />
|
||||
</div>
|
||||
|
||||
{(erro || ok) && (
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// SegmentedTabs — controle segmentado (abas em cápsula) PADRÃO do app.
|
||||
// Grupo de abas mutuamente exclusivas num trilho cinza; a ativa vira um cartão
|
||||
// branco com a cor de destaque. Responsivo: no mobile ocupa a largura toda e
|
||||
// distribui igual; no desktop encolhe ao conteúdo. Referência: /minhas-salas.
|
||||
import React from 'react';
|
||||
|
||||
export interface SegTab<T extends string = string> {
|
||||
id: T;
|
||||
label: string;
|
||||
icon?: React.ElementType;
|
||||
badge?: number;
|
||||
}
|
||||
|
||||
interface SegmentedTabsProps<T extends string> {
|
||||
tabs: SegTab<T>[];
|
||||
value: T;
|
||||
onChange: (id: T) => void;
|
||||
/** Cor de destaque da aba ativa (texto + badge). Padrão: teal do app. */
|
||||
accent?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SegmentedTabs<T extends string>({ tabs, value, onChange, accent = '#0d9488', className = '' }: SegmentedTabsProps<T>) {
|
||||
return (
|
||||
<div className={`flex gap-1 bg-gray-100/80 p-1 rounded-2xl w-full sm:w-fit overflow-x-auto ${className}`}>
|
||||
{tabs.map((t) => {
|
||||
const active = value === t.id;
|
||||
const Icon = t.icon;
|
||||
return (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => onChange(t.id)}
|
||||
className={`flex-1 sm:flex-none flex items-center justify-center gap-2 px-3 sm:px-5 py-2.5 rounded-xl text-[11px] font-black uppercase tracking-wider whitespace-nowrap transition-all ${active ? 'bg-white shadow-sm' : 'text-gray-500 hover:text-gray-700'}`}
|
||||
style={active ? { color: accent } : {}}
|
||||
>
|
||||
{Icon && <Icon size={14} className="shrink-0" />}
|
||||
<span className="truncate">{t.label}</span>
|
||||
{t.badge ? (
|
||||
<span className="text-[9px] font-black px-1.5 py-0.5 rounded-full text-white shrink-0" style={{ backgroundColor: accent }}>{t.badge}</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { HybridBackend } from '../services/backend.ts';
|
||||
import { useToast } from '../contexts/ToastContext.tsx';
|
||||
import { useConfirm } from '../contexts/ConfirmContext.tsx';
|
||||
import { PageHeader } from '../components/PageHeader.tsx';
|
||||
import { SegmentedTabs } from '../components/SegmentedTabs.tsx';
|
||||
|
||||
const COLOR = '#0d9488';
|
||||
const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||||
@@ -22,14 +23,9 @@ export const ComissoesView: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6 pb-10">
|
||||
<PageHeader title="COMISSÕES & REPASSE" />
|
||||
<div className="flex gap-2 border-b border-gray-200">
|
||||
{([['relatorio', 'Relatório / Fechamento', Wallet], ['regras', 'Regras de %', Percent], ['historico', 'Histórico', History]] as const).map(([id, label, Icon]) => (
|
||||
<button key={id} onClick={() => setAba(id)}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 text-xs font-black uppercase tracking-wide border-b-2 -mb-px transition-colors ${aba === id ? 'border-teal-600 text-teal-700' : 'border-transparent text-gray-400 hover:text-gray-600'}`}>
|
||||
<Icon size={15} /> {label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<SegmentedTabs
|
||||
tabs={[{ id: 'relatorio', label: 'Relatório / Fechamento', icon: Wallet }, { id: 'regras', label: 'Regras de %', icon: Percent }, { id: 'historico', label: 'Histórico', icon: History }]}
|
||||
value={aba} onChange={setAba} />
|
||||
|
||||
{aba === 'relatorio' && <AbaRelatorio toast={toast} confirm={confirm} />}
|
||||
{aba === 'regras' && <AbaRegras toast={toast} confirm={confirm} />}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { HybridBackend } from '../services/backend.ts';
|
||||
import { useToast } from '../contexts/ToastContext.tsx';
|
||||
import { useConfirm } from '../contexts/ConfirmContext.tsx';
|
||||
import { PageHeader } from '../components/PageHeader.tsx';
|
||||
import { SegmentedTabs } from '../components/SegmentedTabs.tsx';
|
||||
|
||||
const COLOR = '#0d9488'; // teal
|
||||
|
||||
@@ -473,13 +474,7 @@ export const ContratosView: React.FC = () => {
|
||||
: <button onClick={() => setEditor('novo')} className="bg-teal-600 hover:bg-teal-700 text-white px-4 py-2 rounded-lg flex items-center gap-2 transition-colors uppercase font-bold text-sm shadow-sm"><Plus size={18} /> NOVO MODELO</button>}
|
||||
</PageHeader>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{([['contratos', 'Contratos', FileSignature], ['modelos', 'Modelos', Library]] as const).map(([id, label, Icon]) => (
|
||||
<button key={id} onClick={() => setTab(id)} className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-[11px] font-black uppercase tracking-wider transition-all ${tab === id ? 'text-white shadow-md' : 'bg-white text-gray-500 border border-gray-200 hover:bg-gray-50'}`} style={tab === id ? { backgroundColor: COLOR } : {}}>
|
||||
<Icon size={14} /> {label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<SegmentedTabs tabs={[{ id: 'contratos', label: 'Contratos', icon: FileSignature }, { id: 'modelos', label: 'Modelos', icon: Library }]} value={tab} onChange={setTab} accent={COLOR} />
|
||||
|
||||
{loading ? <div className="flex justify-center py-16"><RefreshCw size={24} className="animate-spin" style={{ color: COLOR }} /></div> : (
|
||||
<>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HybridBackend } from '../services/backend.ts';
|
||||
import { useToast } from '../contexts/ToastContext.tsx';
|
||||
import { useConfirm } from '../contexts/ConfirmContext.tsx';
|
||||
import { PageHeader } from '../components/PageHeader.tsx';
|
||||
import { SegmentedTabs } from '../components/SegmentedTabs.tsx';
|
||||
|
||||
const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||||
const dataBR = (d: string) => (d || '').slice(0, 10).split('-').reverse().join('/');
|
||||
@@ -137,11 +138,7 @@ export const GlosasView: React.FC = () => {
|
||||
return (
|
||||
<div className="space-y-6 pb-10">
|
||||
<PageHeader title="GLOSAS DE CONVÊNIO" />
|
||||
<div className="flex gap-2 border-b border-gray-200">
|
||||
{([['registrar', 'Recebíveis / Marcar glosa'], ['glosas', `Glosas (${glosas.length})`]] as const).map(([id, label]) => (
|
||||
<button key={id} onClick={() => setAba(id)} className={`px-4 py-2.5 text-xs font-black uppercase tracking-wide border-b-2 -mb-px transition-colors ${aba === id ? 'border-teal-600 text-teal-700' : 'border-transparent text-gray-400 hover:text-gray-600'}`}>{label}</button>
|
||||
))}
|
||||
</div>
|
||||
<SegmentedTabs tabs={[{ id: 'registrar', label: 'Recebíveis / Marcar glosa' }, { id: 'glosas', label: `Glosas (${glosas.length})` }]} value={aba} onChange={setAba} />
|
||||
|
||||
{loading ? <div className="flex justify-center py-16"><RefreshCw className="animate-spin text-teal-600" size={28} /></div> : aba === 'registrar' ? (
|
||||
<div className="bg-white rounded-2xl border border-gray-100 shadow-sm overflow-hidden">
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Building2, Wallet, Clock, RefreshCw, Inbox, CheckCircle2, Phone, CalendarDays, Stethoscope, Briefcase, User } from 'lucide-react';
|
||||
import { HybridBackend } from '../services/backend.ts';
|
||||
import { PageHeader } from '../components/PageHeader.tsx';
|
||||
import { SegmentedTabs } from '../components/SegmentedTabs.tsx';
|
||||
|
||||
const fmtBRL = (v: number) => 'R$ ' + Number(v || 0).toLocaleString('pt-BR', { minimumFractionDigits: 2 });
|
||||
const dataBR = (d: string) => (d || '').slice(0, 10).split('-').reverse().join('/');
|
||||
@@ -55,11 +56,7 @@ export const LabClientesView: React.FC<{ tab?: 'clientes' | 'financeiro' }> = ({
|
||||
<button onClick={carregar} className="bg-white border border-gray-200 text-gray-600 p-2 rounded-lg hover:bg-gray-50 flex items-center gap-2 text-xs font-bold shadow-sm"><RefreshCw size={16} /></button>
|
||||
</PageHeader>
|
||||
|
||||
<div className="flex border-b border-gray-100">
|
||||
{(['clientes', 'financeiro'] as const).map(t => (
|
||||
<button key={t} onClick={() => setAba(t)} className={`px-4 py-2.5 text-xs font-black uppercase border-b-2 -mb-px transition-colors ${aba === t ? 'border-teal-600 text-teal-700' : 'border-transparent text-gray-400 hover:text-gray-600'}`}>{t === 'clientes' ? 'Clientes (CRM)' : 'Financeiro'}</button>
|
||||
))}
|
||||
</div>
|
||||
<SegmentedTabs tabs={[{ id: 'clientes', label: 'Clientes (CRM)' }, { id: 'financeiro', label: 'Financeiro' }]} value={aba} onChange={setAba} />
|
||||
|
||||
{loading ? <div className="flex justify-center py-16"><RefreshCw className="animate-spin text-teal-600" size={28} /></div> : (
|
||||
<>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { HybridBackend } from '../services/backend.ts';
|
||||
import { useToast } from '../contexts/ToastContext.tsx';
|
||||
import { useConfirm } from '../contexts/ConfirmContext.tsx';
|
||||
import { PageHeader } from '../components/PageHeader.tsx';
|
||||
import { SegmentedTabs } from '../components/SegmentedTabs.tsx';
|
||||
import { rotulosPaciente } from '../utils/pacienteLabel.ts';
|
||||
import { ProteticoPicker } from '../components/ProteticoPicker.tsx';
|
||||
import { SearchSelect } from '../components/SearchSelect.tsx';
|
||||
@@ -318,11 +319,8 @@ const OSDetailModal: React.FC<{ id: string; modo: 'bancada' | 'clinica'; onClose
|
||||
</div>
|
||||
</div>
|
||||
{/* Abas */}
|
||||
<div className="flex border-b border-gray-100 px-2 overflow-x-auto">
|
||||
{TABS.map(t => (
|
||||
<button key={t.id} onClick={() => setTab(t.id)}
|
||||
className={`px-3 py-2.5 text-xs font-black uppercase border-b-2 -mb-px whitespace-nowrap transition-colors ${tab === t.id ? 'border-violet-600 text-violet-700' : 'border-transparent text-gray-400 hover:text-gray-600'}`}>{t.label}</button>
|
||||
))}
|
||||
<div className="px-4 pt-3">
|
||||
<SegmentedTabs tabs={TABS.map(t => ({ id: t.id, label: t.label }))} value={tab} onChange={setTab} accent="#7c3aed" />
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-5">
|
||||
@@ -1037,10 +1035,8 @@ const CotacoesClinicaModal: React.FC<{ onClose: () => void; onEscolhida: () => v
|
||||
<h3 className="font-black text-gray-800 uppercase text-sm flex items-center gap-2"><FileText size={16} className="text-violet-600" /> Cotações de prótese</h3>
|
||||
<button onClick={onClose}><X size={20} className="text-gray-400" /></button>
|
||||
</div>
|
||||
<div className="flex border-b border-gray-100 px-2">
|
||||
{(['lista', 'novo'] as const).map(t => (
|
||||
<button key={t} onClick={() => setAba(t)} className={`px-3 py-2.5 text-xs font-black uppercase border-b-2 -mb-px ${aba === t ? 'border-violet-600 text-violet-700' : 'border-transparent text-gray-400'}`}>{t === 'lista' ? 'Meus pedidos' : 'Pedir cotação'}</button>
|
||||
))}
|
||||
<div className="px-4 pt-3">
|
||||
<SegmentedTabs tabs={[{ id: 'lista', label: 'Meus pedidos' }, { id: 'novo', label: 'Pedir cotação' }]} value={aba} onChange={setAba} accent="#7c3aed" />
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-5">
|
||||
{aba === 'novo' ? (
|
||||
|
||||
@@ -147,6 +147,7 @@ const VitrineProtetico: React.FC<{ proteticoId: string }> = ({ proteticoId }) =>
|
||||
);
|
||||
};
|
||||
import { PageHeader } from '../../components/PageHeader.tsx';
|
||||
import { SegmentedTabs } from '../../components/SegmentedTabs.tsx';
|
||||
import { useToast } from '../../contexts/ToastContext.tsx';
|
||||
import { useConfirm } from '../../contexts/ConfirmContext.tsx';
|
||||
import { HybridBackend } from '../../services/backend.ts';
|
||||
@@ -705,22 +706,8 @@ export const ProfissionaisPlugin: React.FC = () => {
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{TABS.filter(t => t.show).map(t => {
|
||||
const Icon = t.icon;
|
||||
const active = tab === t.id;
|
||||
return (
|
||||
<button key={t.id} onClick={() => setTab(t.id)}
|
||||
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-[11px] font-black uppercase tracking-wider transition-all ${active ? 'text-white shadow-md' : 'bg-white text-gray-500 border border-gray-200 hover:bg-gray-50'}`}
|
||||
style={active ? { backgroundColor: COLOR } : {}}>
|
||||
<Icon size={14} /> {t.label}
|
||||
{t.id === 'propostas' && pendentesRecebidas > 0 && (
|
||||
<span className={`text-[9px] font-black px-1.5 py-0.5 rounded-full ${active ? 'bg-white/25' : 'bg-amber-100 text-amber-700'}`}>{pendentesRecebidas}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<SegmentedTabs accent={COLOR} value={tab} onChange={setTab}
|
||||
tabs={TABS.filter(t => t.show).map(t => ({ id: t.id, label: t.label, icon: t.icon, badge: t.id === 'propostas' && pendentesRecebidas > 0 ? pendentesRecebidas : undefined }))} />
|
||||
|
||||
{/* BUSCAR — bloqueia se faltar endereço/CEP da origem */}
|
||||
{tab === 'buscar' && enderecoOk === false && (
|
||||
|
||||
Reference in New Issue
Block a user