riseup-squad18/src/components/ui/AvatarUpload.tsx
Fernando Pirichowski Aguiar 389a191f20 fix: corrige persistência de avatar, agendamento de consulta e download de PDF
- Avatar do paciente agora persiste após reload (adiciona timestamp para evitar cache)
- Agendamento usa patient_id correto ao invés de user_id
- Botão de download de PDF desbloqueado com logs detalhados
2025-11-15 08:36:41 -03:00

308 lines
9.5 KiB
TypeScript

import { useState, useRef, useEffect } from "react";
import { Camera, Upload, X, Trash2 } from "lucide-react";
import { avatarService, patientService, doctorService } from "../../services";
import toast from "react-hot-toast";
import { Avatar } from "./Avatar";
interface AvatarUploadProps {
/** ID do usuário */
userId?: string;
/** URL atual do avatar */
currentAvatarUrl?: string;
/** Nome para gerar iniciais */
name?: string;
/** Cor do avatar */
color?:
| "blue"
| "green"
| "purple"
| "orange"
| "pink"
| "teal"
| "indigo"
| "red";
/** Tamanho do avatar */
size?: "lg" | "xl";
/** Callback quando o avatar é atualizado */
onAvatarUpdate?: (avatarUrl: string | null) => void;
/** Se está em modo de edição */
editable?: boolean;
/** Tipo de usuário (paciente ou médico) */
userType?: "patient" | "doctor";
}
export function AvatarUpload({
userId,
currentAvatarUrl,
name = "",
color = "blue",
size = "xl",
onAvatarUpdate,
editable = true,
userType = "patient",
}: AvatarUploadProps) {
const [isUploading, setIsUploading] = useState(false);
const [showMenu, setShowMenu] = useState(false);
const [displayUrl, setDisplayUrl] = useState<string | undefined>(
currentAvatarUrl
);
const fileInputRef = useRef<HTMLInputElement>(null);
// Atualiza displayUrl quando currentAvatarUrl muda externamente
useEffect(() => {
console.log("[AvatarUpload] currentAvatarUrl:", currentAvatarUrl);
console.log("[AvatarUpload] userId:", userId);
console.log("[AvatarUpload] editable:", editable);
setDisplayUrl(currentAvatarUrl);
}, [currentAvatarUrl, userId, editable]);
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
console.log("[AvatarUpload] Arquivo selecionado:", {
file: file?.name,
userId,
hasUserId: !!userId,
userIdType: typeof userId,
userIdValue: userId,
});
if (!file) {
console.warn("[AvatarUpload] Nenhum arquivo selecionado");
return;
}
if (!userId) {
console.error("[AvatarUpload] ❌ userId não está definido!", {
userId,
hasUserId: !!userId,
});
toast.error(
"Não foi possível identificar o usuário. Por favor, recarregue a página."
);
return;
}
// Validação adicional: userId não pode ser string vazia
if (typeof userId === "string" && userId.trim() === "") {
console.error("[AvatarUpload] ❌ userId está vazio!", { userId });
toast.error(
"ID do usuário está vazio. Por favor, recarregue a página."
);
return;
}
// Validação de tamanho (max 2MB)
if (file.size > 2 * 1024 * 1024) {
toast.error("Arquivo muito grande! Tamanho máximo: 2MB");
return;
}
// Validação de tipo
if (!["image/jpeg", "image/png", "image/webp"].includes(file.type)) {
toast.error("Formato inválido! Use JPG, PNG ou WebP");
return;
}
setIsUploading(true);
setShowMenu(false);
try {
console.log("[AvatarUpload] 🚀 Iniciando upload...", {
userId,
fileName: file.name,
fileSize: file.size,
fileType: file.type,
});
// Upload do avatar
const uploadResult = await avatarService.upload({
userId,
file,
});
console.log("[AvatarUpload] ✅ Upload retornou:", uploadResult);
// Gera URL pública com cache-busting
const ext = file.name.split(".").pop()?.toLowerCase();
const avatarExt =
ext === "jpg" || ext === "png" || ext === "webp" ? ext : "jpg";
const baseUrl = avatarService.getPublicUrl({
userId,
ext: avatarExt,
});
// Adiciona timestamp para forçar reload da imagem
const publicUrl = `${baseUrl}?t=${Date.now()}`;
console.log("[AvatarUpload] Upload concluído, atualizando paciente...", {
baseUrl,
});
// Atualiza avatar_url na tabela apropriada (patients ou doctors)
try {
if (userType === "doctor") {
await doctorService.updateByUserId(userId, { avatar_url: baseUrl });
console.log("[AvatarUpload] ✅ Avatar atualizado na tabela doctors");
} else {
await patientService.updateByUserId(userId, { avatar_url: baseUrl });
console.log("[AvatarUpload] ✅ Avatar atualizado na tabela patients");
}
} catch (error) {
console.warn(
`[AvatarUpload] ⚠️ Não foi possível atualizar tabela ${userType === "doctor" ? "doctors" : "patients"}:`,
error
);
// Não bloqueia o fluxo, avatar já está no Storage
}
// Atualiza estado local com timestamp
setDisplayUrl(publicUrl);
// Callback com timestamp para forçar reload imediato no componente
onAvatarUpdate?.(publicUrl);
toast.success("Avatar atualizado com sucesso!");
console.log("[AvatarUpload] ✅ Processo concluído com sucesso");
} catch (error) {
console.error("❌ [AvatarUpload] Erro ao fazer upload:", error);
toast.error("Erro ao fazer upload do avatar");
} finally {
setIsUploading(false);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
const handleRemove = async () => {
if (!userId) return;
if (!confirm("Tem certeza que deseja remover o avatar?")) {
setShowMenu(false);
return;
}
setIsUploading(true);
setShowMenu(false);
try {
await avatarService.delete({ userId });
// Remove avatar_url da tabela apropriada (patients ou doctors)
try {
if (userType === "doctor") {
await doctorService.updateByUserId(userId, { avatar_url: null });
console.log("[AvatarUpload] ✅ Avatar removido da tabela doctors");
} else {
await patientService.updateByUserId(userId, { avatar_url: null });
console.log("[AvatarUpload] ✅ Avatar removido da tabela patients");
}
} catch (error) {
console.warn(
`[AvatarUpload] ⚠️ Não foi possível remover da tabela ${userType === "doctor" ? "doctors" : "patients"}:`,
error
);
}
// Atualiza estado local
setDisplayUrl(undefined);
onAvatarUpdate?.(null);
toast.success("Avatar removido com sucesso!");
} catch (error) {
console.error("Erro ao remover avatar:", error);
toast.error("Erro ao remover avatar");
} finally {
setIsUploading(false);
}
};
return (
<div className="relative inline-block">
{/* Avatar */}
<div className="relative">
<Avatar
src={displayUrl || (userId ? { id: userId } : undefined)}
name={name}
size={size}
color={color}
border
/>
{/* Loading overlay */}
{isUploading && (
<div className="absolute inset-0 bg-black bg-opacity-50 rounded-full flex items-center justify-center">
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
</div>
)}
{/* Edit button */}
{editable && !isUploading && (
<button
type="button"
onClick={() => setShowMenu(!showMenu)}
className="absolute bottom-0 right-0 bg-white rounded-full p-1.5 shadow-lg hover:bg-gray-100 transition-colors border-2 border-white"
title="Editar avatar"
>
<Camera className="w-3 h-3 text-gray-700" />
</button>
)}
</div>
{/* Menu dropdown */}
{showMenu && editable && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40"
onClick={() => setShowMenu(false)}
/>
{/* Menu */}
<div className="absolute top-full left-0 mt-2 bg-white rounded-lg shadow-xl border border-gray-200 py-2 z-50 min-w-[200px]">
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2"
>
<Upload className="w-4 h-4" />
{currentAvatarUrl ? "Trocar foto" : "Adicionar foto"}
</button>
{currentAvatarUrl && (
<button
type="button"
onClick={handleRemove}
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
>
<Trash2 className="w-4 h-4" />
Remover foto
</button>
)}
<button
type="button"
onClick={() => setShowMenu(false)}
className="w-full px-4 py-2 text-left text-sm text-gray-500 hover:bg-gray-100 flex items-center gap-2 border-t border-gray-200 mt-1 pt-2"
>
<X className="w-4 h-4" />
Cancelar
</button>
</div>
</>
)}
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
accept="image/jpeg,image/png,image/webp"
onChange={handleFileSelect}
className="hidden"
/>
</div>
);
}