diff --git a/src/components/commander-csv/CommanderCsvView.tsx b/src/components/commander-csv/CommanderCsvView.tsx new file mode 100644 index 0000000..971b442 --- /dev/null +++ b/src/components/commander-csv/CommanderCsvView.tsx @@ -0,0 +1,449 @@ +import { useMemo, useState, type ChangeEvent } from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Textarea } from "@/components/ui/textarea"; +import { Copy, Download, FileText, Loader2, Plus, Upload, X } from "lucide-react"; +import { InteractiveHoverButton } from "@/components/ui/interactive-hover-button"; +import { toast } from "sonner"; +import { commanderCsvService, type CommanderCsvMode } from "@/services/commanderCsv"; + +type HeaderItem = { + original: string; + slug: string; +}; + +const toSlug = (value: string) => + value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/_+/g, "_") + .replace(/^_+|_+$/g, ""); + +export function CommanderCsvView() { + const [mode, setMode] = useState("pdf"); + const [headerInput, setHeaderInput] = useState(""); + const [headers, setHeaders] = useState([]); + const [selectedFile, setSelectedFile] = useState(null); + const [content, setContent] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isHealthLoading, setIsHealthLoading] = useState(false); + const [healthStatus, setHealthStatus] = useState<"idle" | "ok" | "error">("idle"); + const [responseData, setResponseData] = useState(null); + const [isClearHeadersOpen, setIsClearHeadersOpen] = useState(false); + const [lastSentAt, setLastSentAt] = useState(null); + const [lastSentMode, setLastSentMode] = useState(null); + + const headersCsv = useMemo(() => headers.map((header) => header.slug).join(","), [headers]); + + const addHeader = () => { + const original = headerInput.trim(); + if (!original) return; + + const slug = toSlug(original); + if (!slug) { + toast.error("Cabecalho invalido. Digite letras ou numeros."); + return; + } + + if (headers.some((header) => header.slug === slug)) { + toast.error("Esse cabecalho ja foi adicionado."); + return; + } + + setHeaders((prev) => [...prev, { original, slug }]); + setHeaderInput(""); + }; + + const removeHeader = (slug: string) => { + setHeaders((prev) => prev.filter((header) => header.slug !== slug)); + }; + + const handleFileChange = (event: ChangeEvent) => { + const file = event.target.files?.[0] ?? null; + if (!file) return; + + const fileName = file.name.toLowerCase(); + if (!fileName.endsWith(".pdf")) { + toast.error("Envie apenas arquivo PDF."); + return; + } + + setSelectedFile(file); + }; + + const runHealthcheck = async () => { + setIsHealthLoading(true); + try { + await commanderCsvService.healthcheck(); + setHealthStatus("ok"); + toast.success("API online."); + } catch (error: unknown) { + setHealthStatus("error"); + const message = error && typeof error === "object" && "message" in error + ? (error as { message: string }).message + : "Falha no healthcheck."; + toast.error(message); + } finally { + setIsHealthLoading(false); + } + }; + + const submit = async () => { + if (headers.length === 0) { + toast.error("Adicione pelo menos um cabecalho."); + return; + } + + if (mode === "pdf" && !selectedFile) { + toast.error("Selecione um PDF para enviar."); + return; + } + + if (mode === "text" && !content.trim()) { + toast.error("Preencha o conteudo de texto."); + return; + } + + setIsSubmitting(true); + try { + const result = mode === "pdf" + ? await commanderCsvService.pdf2csv(selectedFile as File, headersCsv) + : await commanderCsvService.txt2csv(content.trim(), headersCsv); + + setResponseData(result); + setLastSentAt(new Date()); + setLastSentMode(mode); + toast.success("Processamento concluido."); + } catch (error: unknown) { + const message = error && typeof error === "object" && "message" in error + ? (error as { message: string }).message + : "Erro ao enviar dados para API."; + toast.error(message); + } finally { + setIsSubmitting(false); + } + }; + + const responseContent = useMemo(() => { + if (responseData && typeof responseData === "object" && "content" in responseData) { + const content = (responseData as { content?: unknown }).content; + if (typeof content === "string") return content; + } + return null; + }, [responseData]); + + const copyResponse = async () => { + const textToCopy = responseContent ?? (responseData ? JSON.stringify(responseData, null, 2) : ""); + if (!textToCopy) { + toast.error("Nao ha resposta para copiar."); + return; + } + try { + await navigator.clipboard.writeText(textToCopy); + toast.success("Resposta copiada."); + } catch { + toast.error("Falha ao copiar resposta."); + } + }; + + const downloadCsv = () => { + if (!responseContent) { + toast.error("Nao ha CSV para baixar."); + return; + } + + const utf8Bom = "\uFEFF"; + const blob = new Blob([utf8Bom, responseContent], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `commander-csv-${new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-")}.csv`; + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + URL.revokeObjectURL(url); + toast.success("Download do CSV iniciado."); + }; + + const sendBlockReason = useMemo(() => { + if (headers.length === 0) return "Adicione pelo menos um cabecalho."; + if (mode === "pdf" && !selectedFile) return "Selecione um PDF para envio."; + if (mode === "text" && !content.trim()) return "Preencha o conteudo de texto."; + return null; + }, [headers.length, mode, selectedFile, content]); + + return ( +
+
+
+
+

Commander CSV

+

+ Configure os cabecalhos, escolha o modo de envio e gere o CSV com mais controle. +

+
+
+
+ + {healthStatus === "ok" ? "API online" : healthStatus === "error" ? "API indisponivel" : "Status da API"} +
+ +
+
+
+ +
+
+ + + Cabecalhos + + Defina os campos do CSV. O sistema gera slug automaticamente em minusculo com underline. + + + +
+ setHeaderInput(event.target.value)} + placeholder="Ex: Data de Aniversario" + className="min-w-[220px] flex-1" + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + addHeader(); + } + }} + /> +
+ + +
+
+ +
+ {headers.length === 0 ? ( +

+ Adicione seu primeiro cabecalho para habilitar o envio. +

+ ) : ( + headers.map((header) => ( +
+

{header.slug}

+ +
+ )) + )} +
+ +
+
+ + + + Envio + + Alterne entre PDF ou Texto com os mesmos cabecalhos configurados. + + + + setMode(value as CommanderCsvMode)} + className="grid gap-3 sm:grid-cols-2" + > + + + + + {mode === "pdf" ? ( +
+ + + +
+ ) : ( +
+ +