"use client"; import { RiImageAddLine } from "@remixicon/react"; import Image from "next/image"; import { useEffect, useMemo, useRef, useState, useTransition } from "react"; import { toast } from "sonner"; import { createPayerAction, updatePayerAction, } from "@/features/payers/actions"; import { Button } from "@/shared/components/ui/button"; import { Checkbox } from "@/shared/components/ui/checkbox"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/shared/components/ui/dialog"; import { Input } from "@/shared/components/ui/input"; import { Label } from "@/shared/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/shared/components/ui/select"; import { useControlledState } from "@/shared/hooks/use-controlled-state"; import { useFormState } from "@/shared/hooks/use-form-state"; import { DEFAULT_PAYER_AVATAR, PAYER_STATUS_OPTIONS, type PayerStatus, } from "@/shared/lib/payers/constants"; import { getAvatarSrc } from "@/shared/lib/payers/utils"; import { StatusSelectContent } from "./payer-select-items"; import type { Payer, PayerFormValues } from "./types"; const AVATAR_MAX_SIZE = 200; function resizeImageToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { const img = new window.Image(); img.onload = () => { let { width, height } = img; if (width > height) { if (width > AVATAR_MAX_SIZE) { height = Math.round((height * AVATAR_MAX_SIZE) / width); width = AVATAR_MAX_SIZE; } } else { if (height > AVATAR_MAX_SIZE) { width = Math.round((width * AVATAR_MAX_SIZE) / height); height = AVATAR_MAX_SIZE; } } const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); if (!ctx) { reject(new Error("Canvas não disponível")); return; } ctx.drawImage(img, 0, 0, width, height); resolve(canvas.toDataURL("image/jpeg", 0.85)); }; img.onerror = () => reject(new Error("Falha ao carregar imagem")); img.src = e.target?.result as string; }; reader.onerror = () => reject(new Error("Falha ao ler arquivo")); reader.readAsDataURL(file); }); } type PayerCreatePayload = Parameters[0]; interface PayerDialogProps { mode: "create" | "update"; trigger?: React.ReactNode; payer?: Payer; avatarOptions: string[]; open?: boolean; onOpenChange?: (open: boolean) => void; } const buildInitialValues = ({ payer, avatarOptions, }: { payer?: Payer; avatarOptions: string[]; }): PayerFormValues => { const defaultAvatar = avatarOptions[0] ?? DEFAULT_PAYER_AVATAR; return { name: payer?.name ?? "", email: payer?.email ?? "", status: (payer?.status as PayerStatus) ?? PAYER_STATUS_OPTIONS[0], avatarUrl: payer?.avatarUrl ?? defaultAvatar, note: payer?.note ?? "", isAutoSend: payer?.isAutoSend ?? false, }; }; export function PayerDialog({ mode, trigger, payer, avatarOptions, open, onOpenChange, }: PayerDialogProps) { const [errorMessage, setErrorMessage] = useState(null); const [isPending, startTransition] = useTransition(); const [uploadedAvatar, setUploadedAvatar] = useState(null); const [isProcessingImage, setIsProcessingImage] = useState(false); const fileInputRef = useRef(null); const [dialogOpen, setDialogOpen] = useControlledState( open, false, onOpenChange, ); const initialState = useMemo( () => buildInitialValues({ payer, avatarOptions }), [payer, avatarOptions], ); const { formState, resetForm, updateField } = useFormState(initialState); // Avatares da biblioteca excluem data URLs (que ficam no círculo de upload) const availableAvatars = useMemo(() => { const set = new Set([...avatarOptions, DEFAULT_PAYER_AVATAR]); if (initialState.avatarUrl && !initialState.avatarUrl.startsWith("data:")) { set.add(initialState.avatarUrl); } return Array.from(set).sort((a, b) => a.localeCompare(b, "pt-BR", { sensitivity: "base" }), ); }, [avatarOptions, initialState.avatarUrl]); const handleFileChange = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; setIsProcessingImage(true); try { const base64 = await resizeImageToBase64(file); setUploadedAvatar(base64); updateField("avatarUrl", base64); } catch { toast.error("Não foi possível processar a imagem."); } finally { setIsProcessingImage(false); if (fileInputRef.current) fileInputRef.current.value = ""; } }; useEffect(() => { if (dialogOpen) { resetForm(initialState); setErrorMessage(null); setIsProcessingImage(false); // Se o avatar atual for um upload anterior, restaura no círculo setUploadedAvatar( initialState.avatarUrl.startsWith("data:") ? initialState.avatarUrl : null, ); } }, [dialogOpen, initialState, resetForm]); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); setErrorMessage(null); const payerId = payer?.id; if (mode === "update" && !payerId) { const message = "Pagador inválido."; setErrorMessage(message); toast.error(message); return; } const emailValue = formState.email.trim(); const payload: PayerCreatePayload = { name: formState.name.trim(), status: formState.status, avatarUrl: formState.avatarUrl, email: emailValue || null, note: formState.note.trim() || null, isAutoSend: formState.isAutoSend, }; startTransition(async () => { const result = mode === "create" ? await createPayerAction(payload) : await updatePayerAction({ id: payerId ?? "", ...payload }); if (result.success) { toast.success(result.message); setDialogOpen(false); resetForm(initialState); return; } setErrorMessage(result.error); toast.error(result.error); }); }; const title = mode === "create" ? "Novo pagador" : "Editar pagador"; const description = mode === "create" ? "Selecione um avatar e informe os detalhes para criar um novo pagador." : "Atualize os detalhes do pagador selecionado."; const submitLabel = mode === "create" ? "Salvar pagador" : "Atualizar pagador"; const isUploadSelected = uploadedAvatar !== null && formState.avatarUrl === uploadedAvatar; return ( {trigger ? {trigger} : null} {title} {description}
updateField("name", event.target.value) } placeholder="Ex.: Felipe Coutinho" required />
updateField("email", event.target.value) } placeholder="Ex.: felipe@email.com" />
updateField("isAutoSend", Boolean(checked)) } aria-label="Ativar envio automático" />

Dispare cobranças e lembretes sem intervenção manual.

{availableAvatars.map((avatar) => { const isSelected = avatar === formState.avatarUrl; const src = getAvatarSrc(avatar); return ( ); })} {/* Círculo de upload — sempre o último */}
updateField("note", event.target.value)} placeholder="Observações sobre este pagador" />
{errorMessage ? (

{errorMessage}

) : null}
); }