develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
3 changed files with 87 additions and 95 deletions
Showing only changes of commit 07c0533224 - Show all commits

View File

@ -602,7 +602,7 @@ export default function PacientePage() {
{/* Cards com Informações */} {/* Cards com Informações */}
<div className="grid grid-cols-1 gap-3 sm:gap-4 md:gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-3 sm:gap-4 md:gap-4 md:grid-cols-2">
<Card className="group rounded-2xl border border-border/60 bg-card/70 p-4 sm:p-5 md:p-5 backdrop-blur-sm shadow-sm transition hover:shadow-md"> <Card className="group rounded-2xl border border-border/60 bg-card p-4 sm:p-5 md:p-5 shadow-sm transition hover:shadow-md">
<div className="flex h-32 sm:h-36 md:h-40 w-full flex-col items-center justify-center gap-2 sm:gap-3"> <div className="flex h-32 sm:h-36 md:h-40 w-full flex-col items-center justify-center gap-2 sm:gap-3">
<div className="flex h-10 w-10 sm:h-11 sm:w-11 md:h-12 md:w-12 items-center justify-center rounded-full bg-primary/10 text-primary"> <div className="flex h-10 w-10 sm:h-11 sm:w-11 md:h-12 md:w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
<Calendar className="h-5 w-5 sm:h-5 sm:w-5 md:h-6 md:w-6" aria-hidden /> <Calendar className="h-5 w-5 sm:h-5 sm:w-5 md:h-6 md:w-6" aria-hidden />
@ -616,7 +616,7 @@ export default function PacientePage() {
</div> </div>
</Card> </Card>
<Card className="group rounded-2xl border border-border/60 bg-card/70 p-4 sm:p-5 md:p-5 backdrop-blur-sm shadow-sm transition hover:shadow-md"> <Card className="group rounded-2xl border border-border/60 bg-card p-4 sm:p-5 md:p-5 shadow-sm transition hover:shadow-md">
<div className="flex h-32 sm:h-36 md:h-40 w-full flex-col items-center justify-center gap-2 sm:gap-3"> <div className="flex h-32 sm:h-36 md:h-40 w-full flex-col items-center justify-center gap-2 sm:gap-3">
<div className="flex h-10 w-10 sm:h-11 sm:w-11 md:h-12 md:w-12 items-center justify-center rounded-full bg-primary/10 text-primary"> <div className="flex h-10 w-10 sm:h-11 sm:w-11 md:h-12 md:w-12 items-center justify-center rounded-full bg-primary/10 text-primary">
<FileText className="h-5 w-5 sm:h-5 sm:w-5 md:h-6 md:w-6" aria-hidden /> <FileText className="h-5 w-5 sm:h-5 sm:w-5 md:h-6 md:w-6" aria-hidden />
@ -1960,8 +1960,8 @@ export default function PacientePage() {
{/* Layout com sidebar e conteúdo */} {/* Layout com sidebar e conteúdo */}
<div className="grid grid-cols-1 md:grid-cols-[200px_1fr] lg:grid-cols-[220px_1fr] gap-4 sm:gap-5 md:gap-6"> <div className="grid grid-cols-1 md:grid-cols-[200px_1fr] lg:grid-cols-[220px_1fr] gap-4 sm:gap-5 md:gap-6">
{/* Sidebar vertical - sticky */} {/* Sidebar vertical - sticky */}
<aside className="sticky top-24 h-fit md:top-24"> <aside className="sticky top-24 h-fit md:top-24 z-40">
<nav aria-label="Navegação do dashboard" className="bg-card shadow-md rounded-lg border border-border p-1.5 sm:p-2 md:p-3 z-30"> <nav aria-label="Navegação do dashboard" className="relative isolate bg-card shadow-lg rounded-lg border border-border p-1.5 sm:p-2 md:p-3 z-50">
<div className="grid grid-cols-2 md:grid-cols-1 gap-1 sm:gap-1.5"> <div className="grid grid-cols-2 md:grid-cols-1 gap-1 sm:gap-1.5">
<Button <Button
variant={tab==='dashboard'?'default':'ghost'} variant={tab==='dashboard'?'default':'ghost'}

View File

@ -33,81 +33,68 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
}, [dropdownOpen]); }, [dropdownOpen]);
return ( return (
<header className="h-16 border-b border-border bg-background px-6 flex items-center justify-between"> <header className="sticky top-0 z-40 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 px-3 sm:px-6 py-2 flex flex-wrap items-center gap-3">
<div className="flex flex-row items-center gap-4"> <div className="flex items-center gap-3 min-w-0">
<SidebarTrigger /> <SidebarTrigger />
<div className="flex items-start flex-col justify-center py-2"> <div className="flex flex-col justify-center leading-tight min-w-0">
<h1 className="text-lg font-semibold text-foreground">{title}</h1> <h1 className="text-sm sm:text-lg font-semibold text-foreground truncate max-w-[55vw] sm:max-w-none">{title}</h1>
<p className="text-muted-foreground">{subtitle}</p> {subtitle && (
<p className="text-[11px] sm:text-xs text-muted-foreground truncate max-w-[55vw] sm:max-w-none">{subtitle}</p>
)}
</div> </div>
</div> </div>
<div className="flex items-center gap-2 ml-auto">
<div className="flex items-center space-x-4"> <Button variant="ghost" size="icon" className="hover-primary-blue hidden xs:flex">
<Button variant="ghost" size="icon" className="hover-primary-blue">
<Bell className="h-4 w-4" /> <Bell className="h-4 w-4" />
</Button> </Button>
<SimpleThemeToggle /> <SimpleThemeToggle />
<Button
variant="outline"
className="text-blue-500 border-blue-500 bg-transparent shadow-sm shadow-blue-500/10 border hover-primary-blue"
asChild
></Button>
{/* Avatar Dropdown Simples */}
<div className="relative" ref={dropdownRef}> <div className="relative" ref={dropdownRef}>
<Button <Button
variant="ghost" variant="ghost"
className="relative h-8 w-8 rounded-full border-2 border-border hover:border-primary" className="relative h-8 w-8 rounded-full border border-border hover:border-primary"
onClick={() => setDropdownOpen(!dropdownOpen)} onClick={() => setDropdownOpen(!dropdownOpen)}
aria-label="Abrir menu do perfil"
> >
{/* Mostrar foto do usuário quando disponível; senão, mostrar fallback com iniciais */}
<Avatar className="h-8 w-8"> <Avatar className="h-8 w-8">
{ {(() => {
(() => { const userPhoto = (user as any)?.profile?.foto_url || (user as any)?.profile?.fotoUrl || (user as any)?.profile?.avatar_url
const userPhoto = (user as any)?.profile?.foto_url || (user as any)?.profile?.fotoUrl || (user as any)?.profile?.avatar_url const alt = user?.name || user?.email || 'Usuário'
const alt = user?.name || user?.email || 'Usuário' const getInitials = (name?: string, email?: string) => {
if (name) {
const getInitials = (name?: string, email?: string) => { const parts = name.trim().split(/\s+/)
if (name) { const first = parts[0]?.charAt(0) ?? ''
const parts = name.trim().split(/\s+/) const second = parts[1]?.charAt(0) ?? ''
const first = parts[0]?.charAt(0) ?? '' return (first + second).toUpperCase() || (email?.charAt(0) ?? 'U').toUpperCase()
const second = parts[1]?.charAt(0) ?? ''
return (first + second).toUpperCase() || (email?.charAt(0) ?? 'U').toUpperCase()
}
if (email) return email.charAt(0).toUpperCase()
return 'U'
} }
if (email) return email.charAt(0).toUpperCase()
return ( return 'U'
<> }
<AvatarImage src={userPhoto || undefined} alt={alt} /> return (
<AvatarFallback className="bg-primary text-primary-foreground font-semibold">{getInitials(user?.name, user?.email)}</AvatarFallback> <>
</> <AvatarImage src={userPhoto || undefined} alt={alt} />
) <AvatarFallback className="bg-primary text-primary-foreground font-semibold">{getInitials(user?.name, user?.email)}</AvatarFallback>
})() </>
} )
})()}
</Avatar> </Avatar>
</Button> </Button>
{/* Dropdown Content */}
{dropdownOpen && ( {dropdownOpen && (
<div className="absolute right-0 mt-2 w-80 bg-popover border border-border rounded-md shadow-lg z-50 text-popover-foreground"> <div className="absolute right-0 mt-2 w-64 sm:w-80 bg-popover border border-border rounded-md shadow-lg z-50 text-popover-foreground animate-in fade-in slide-in-from-top-2">
<div className="p-4 border-b border-border"> <div className="p-3 sm:p-4 border-b border-border">
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<p className="text-sm font-semibold leading-none"> <p className="text-xs sm:text-sm font-semibold leading-none">
{user?.userType === 'administrador' ? 'Administrador da Clínica' : 'Usuário do Sistema'} {user?.userType === 'administrador' ? 'Administrador da Clínica' : 'Usuário do Sistema'}
</p> </p>
{user?.email ? ( {user?.email ? (
<p className="text-xs leading-none text-muted-foreground">{user.email}</p> <p className="text-[10px] sm:text-xs leading-none text-muted-foreground truncate">{user.email}</p>
) : ( ) : (
<p className="text-xs leading-none text-muted-foreground">Email não disponível</p> <p className="text-[10px] sm:text-xs leading-none text-muted-foreground">Email não disponível</p>
)} )}
<p className="text-xs leading-none text-primary font-medium"> <p className="text-[10px] sm:text-xs leading-none text-primary font-medium">
Tipo: {user?.userType === 'administrador' ? 'Administrador' : user?.userType || 'Não definido'} Tipo: {user?.userType === 'administrador' ? 'Administrador' : user?.userType || 'Não definido'}
</p> </p>
</div> </div>
</div> </div>
<div className="py-1"> <div className="py-1">
<button <button
onClick={(e) => { onClick={(e) => {
@ -115,23 +102,20 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
setDropdownOpen(false); setDropdownOpen(false);
router.push('/perfil'); router.push('/perfil');
}} }}
className="w-full text-left px-4 py-2 text-sm hover:bg-accent cursor-pointer" className="w-full text-left px-3 sm:px-4 py-2 text-xs sm:text-sm hover:bg-accent cursor-pointer"
> >
Perfil Perfil
</button> </button>
<div className="border-t border-border my-1" />
<div className="border-t border-border my-1"></div>
<button <button
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setDropdownOpen(false); setDropdownOpen(false);
// Usar sempre o logout do hook useAuth (ele já redireciona corretamente)
logout(); logout();
}} }}
className="w-full text-left px-4 py-2 text-sm text-destructive hover:bg-destructive/10 cursor-pointer" className="w-full text-left px-3 sm:px-4 py-2 text-xs sm:text-sm text-destructive hover:bg-destructive/10 cursor-pointer"
> >
Sair Sair
</button> </button>
</div> </div>
</div> </div>

View File

@ -49,6 +49,23 @@ const FileUploadChat = ({ onOpenVoice }: { onOpenVoice?: () => void }) => {
const chatEndRef = useRef<HTMLDivElement>(null); const chatEndRef = useRef<HTMLDivElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
// Placeholder responsivo (não quebra, adapta o texto)
const [responsivePlaceholder, setResponsivePlaceholder] = useState("Pergunte qualquer coisa para a Zoe");
const computePlaceholder = (w: number) => {
if (w < 340) return "Pergunte à Zoe"; // ultra pequeno
if (w < 400) return "Pergunte algo à Zoe"; // pequeno
if (w < 520) return "Pergunte algo para a Zoe"; // médio estreito
return "Pergunte qualquer coisa para a Zoe"; // normal
};
useEffect(() => {
const update = () => setResponsivePlaceholder(computePlaceholder(window.innerWidth));
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
}, []);
useEffect(() => { useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]); }, [messages]);
@ -511,12 +528,11 @@ const FileUploadChat = ({ onOpenVoice }: { onOpenVoice?: () => void }) => {
{/* Input unificado com ícones embutidos */} {/* Input unificado com ícones embutidos */}
<div className="flex w-full"> <div className="flex w-full">
<div className={`relative flex items-center w-full rounded-full border ${themeClasses.border} ${themeClasses.inputBg} overflow-hidden h-11`}> <div className={`flex items-center w-full rounded-full border ${themeClasses.border} ${themeClasses.inputBg} h-11 px-2 gap-2`}>
{/* Botão anexar (esquerda) */}
<button <button
onClick={() => fileInputRef.current?.click()} onClick={() => fileInputRef.current?.click()}
type="button" type="button"
className={`absolute left-2 flex items-center justify-center h-7 w-7 rounded-full transition-colors hover:bg-primary/20 ${themeClasses.text}`} className={`flex items-center justify-center h-7 w-7 rounded-full transition-colors hover:bg-primary/20 flex-shrink-0 ${themeClasses.text}`}
aria-label="Anexar arquivos" aria-label="Anexar arquivos"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
@ -528,41 +544,33 @@ const FileUploadChat = ({ onOpenVoice }: { onOpenVoice?: () => void }) => {
className="hidden" className="hidden"
onChange={(e) => handleFileSelect(e.target.files)} onChange={(e) => handleFileSelect(e.target.files)}
/> />
{/* Textarea */}
<textarea <textarea
ref={textareaRef} ref={textareaRef}
value={inputValue} value={inputValue}
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
placeholder="Pergunte qualquer coisa para a Zoe" placeholder={responsivePlaceholder}
rows={1} rows={1}
className={`pl-11 pr-24 w-full h-full bg-transparent resize-none focus:outline-none text-sm leading-snug py-3 ${themeClasses.text} placeholder-gray-400`} className={`flex-1 bg-transparent resize-none focus:outline-none leading-snug py-3 pr-2 ${themeClasses.text} placeholder-gray-400 text-[13px] sm:text-sm placeholder:text-[12px] sm:placeholder:text-sm whitespace-nowrap overflow-hidden text-ellipsis placeholder:overflow-hidden placeholder:text-ellipsis`}
style={{ minHeight: 'auto', overflow: 'hidden' }} style={{ minHeight: 'auto', overflow: 'hidden' }}
/> />
{/* Ícones à direita */} <button
<div className="absolute right-2 flex items-center gap-2"> onClick={() => onOpenVoice?.()}
<button type="button"
onClick={() => onOpenVoice?.()} className={`flex items-center justify-center h-8 w-8 rounded-full border ${themeClasses.border} transition-colors hover:bg-primary/20 flex-shrink-0 ${themeClasses.text}`}
type="button" aria-label="Entrada de voz"
className={`flex items-center justify-center h-8 w-8 rounded-full border ${themeClasses.border} transition-colors hover:bg-primary/20 ${themeClasses.text}`} >
aria-label="Entrada de voz" <AudioLines className="w-4 h-4" />
> </button>
<AudioLines className="w-4 h-4" /> <button
</button> onClick={sendMessage}
<button disabled={!inputValue.trim() && uploadedFiles.length === 0}
onClick={sendMessage} type="button"
disabled={!inputValue.trim() && uploadedFiles.length === 0} className="flex items-center justify-center h-8 w-8 rounded-full bg-linear-to-r from-blue-500 to-purple-600 text-white hover:from-blue-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 disabled:cursor-not-allowed transition-colors shadow-md flex-shrink-0"
type="button" aria-label="Enviar mensagem"
className="flex items-center justify-center h-8 w-8 rounded-full bg-linear-to-r from-blue-500 to-purple-600 text-white hover:from-blue-600 hover:to-purple-700 disabled:from-gray-400 disabled:to-gray-500 disabled:cursor-not-allowed transition-colors shadow-md" >
aria-label="Enviar mensagem" <Send className="w-4 h-4" />
> </button>
<Send className="w-4 h-4" />
</button>
</div>
{/* Contador de caracteres */}
{inputValue.length > 0 && (
<span className={`absolute bottom-1 right-24 text-[10px] ${themeClasses.textSecondary}`}>{inputValue.length}</span>
)}
</div> </div>
</div> </div>
</div> </div>