forked from RiseUP/riseup-squad18
- 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
308 lines
9.5 KiB
TypeScript
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>
|
|
);
|
|
}
|
|
|
|
|