|
|
@@ -0,0 +1,216 @@
|
|
|
+"use client"
|
|
|
+
|
|
|
+import { useState } from "react"
|
|
|
+import { toast } from "sonner"
|
|
|
+import { FileText, FileKey, Loader2, Download, Check } from "lucide-react"
|
|
|
+import { Button } from "@/components/ui/button"
|
|
|
+import { Input } from "@/components/ui/input"
|
|
|
+import { Label } from "@/components/ui/label"
|
|
|
+import { Dropzone } from "@/components/ui/shadcn-io/dropzone"
|
|
|
+
|
|
|
+export default function FirmarPage() {
|
|
|
+ const [xmlFile, setXmlFile] = useState<File | null>(null)
|
|
|
+ const [p12File, setP12File] = useState<File | null>(null)
|
|
|
+ const [password, setPassword] = useState("")
|
|
|
+ const [isLoading, setIsLoading] = useState(false)
|
|
|
+ const [signedXml, setSignedXml] = useState<string | null>(null)
|
|
|
+
|
|
|
+ const handleSign = async () => {
|
|
|
+ if (!xmlFile || !p12File || !password) {
|
|
|
+ toast.error("Por favor completa todos los campos")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ setIsLoading(true)
|
|
|
+ setSignedXml(null)
|
|
|
+
|
|
|
+ try {
|
|
|
+ const formData = new FormData()
|
|
|
+ formData.append("xml", xmlFile)
|
|
|
+ formData.append("p12", p12File)
|
|
|
+ formData.append("password", password)
|
|
|
+
|
|
|
+ const response = await fetch("/api/sign-invoice", {
|
|
|
+ method: "POST",
|
|
|
+ body: formData,
|
|
|
+ })
|
|
|
+
|
|
|
+ const data = await response.json()
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(data.details || data.error || "Error al firmar")
|
|
|
+ }
|
|
|
+
|
|
|
+ setSignedXml(data.signedXml)
|
|
|
+ toast.success("XML firmado exitosamente")
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error)
|
|
|
+ toast.error(error instanceof Error ? error.message : "Error al firmar el XML")
|
|
|
+ } finally {
|
|
|
+ setIsLoading(false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleDownload = () => {
|
|
|
+ if (!signedXml) return
|
|
|
+
|
|
|
+ const blob = new Blob([signedXml], { type: "application/xml" })
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
+ const a = document.createElement("a")
|
|
|
+ a.href = url
|
|
|
+ a.download = `factura_firmada_${new Date().getTime()}.xml`
|
|
|
+ document.body.appendChild(a)
|
|
|
+ a.click()
|
|
|
+ document.body.removeChild(a)
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+ toast.success("XML firmado descargado")
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleReset = () => {
|
|
|
+ setXmlFile(null)
|
|
|
+ setP12File(null)
|
|
|
+ setPassword("")
|
|
|
+ setSignedXml(null)
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="container mx-auto max-w-4xl py-8 space-y-8">
|
|
|
+ {/* Upload Section */}
|
|
|
+ <div className="grid gap-6">
|
|
|
+ {/* XML File */}
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>Archivo XML de la Factura</Label>
|
|
|
+ <Dropzone
|
|
|
+ accept={{
|
|
|
+ "text/xml": [".xml"],
|
|
|
+ "application/xml": [".xml"],
|
|
|
+ }}
|
|
|
+ maxFiles={1}
|
|
|
+ onDrop={(acceptedFiles) => {
|
|
|
+ if (acceptedFiles.length > 0) {
|
|
|
+ setXmlFile(acceptedFiles[0])
|
|
|
+ toast.success(`XML cargado: ${acceptedFiles[0].name}`)
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ className="border-2 border-dashed rounded-lg p-8"
|
|
|
+ >
|
|
|
+ <div className="flex flex-col items-center gap-2 text-center">
|
|
|
+ <div className="rounded-full bg-primary/10 p-3">
|
|
|
+ <FileText className="h-6 w-6 text-primary" />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-1">
|
|
|
+ <p className="text-sm font-medium">
|
|
|
+ {xmlFile ? (
|
|
|
+ <span className="flex items-center gap-2">
|
|
|
+ <Check className="h-4 w-4 text-green-500" />
|
|
|
+ {xmlFile.name}
|
|
|
+ </span>
|
|
|
+ ) : (
|
|
|
+ "Arrastra tu archivo XML aquí"
|
|
|
+ )}
|
|
|
+ </p>
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
+ o haz clic para seleccionar
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Dropzone>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* P12 Certificate */}
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>Certificado Digital (.p12)</Label>
|
|
|
+ <Dropzone
|
|
|
+ accept={{
|
|
|
+ "application/x-pkcs12": [".p12", ".pfx"],
|
|
|
+ }}
|
|
|
+ maxFiles={1}
|
|
|
+ onDrop={(acceptedFiles) => {
|
|
|
+ if (acceptedFiles.length > 0) {
|
|
|
+ setP12File(acceptedFiles[0])
|
|
|
+ toast.success(`Certificado cargado: ${acceptedFiles[0].name}`)
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ className="border-2 border-dashed rounded-lg p-8"
|
|
|
+ >
|
|
|
+ <div className="flex flex-col items-center gap-2 text-center">
|
|
|
+ <div className="rounded-full bg-primary/10 p-3">
|
|
|
+ <FileKey className="h-6 w-6 text-primary" />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-1">
|
|
|
+ <p className="text-sm font-medium">
|
|
|
+ {p12File ? (
|
|
|
+ <span className="flex items-center gap-2">
|
|
|
+ <Check className="h-4 w-4 text-green-500" />
|
|
|
+ {p12File.name}
|
|
|
+ </span>
|
|
|
+ ) : (
|
|
|
+ "Arrastra tu certificado .p12 aquí"
|
|
|
+ )}
|
|
|
+ </p>
|
|
|
+ <p className="text-xs text-muted-foreground">
|
|
|
+ o haz clic para seleccionar
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Dropzone>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Password */}
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="password">Contraseña del Certificado</Label>
|
|
|
+ <Input
|
|
|
+ id="password"
|
|
|
+ type="password"
|
|
|
+ placeholder="Ingresa la contraseña de tu certificado"
|
|
|
+ value={password}
|
|
|
+ onChange={(e) => setPassword(e.target.value)}
|
|
|
+ disabled={isLoading}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Actions */}
|
|
|
+ <div className="flex gap-4">
|
|
|
+ <Button
|
|
|
+ onClick={handleSign}
|
|
|
+ disabled={!xmlFile || !p12File || !password || isLoading}
|
|
|
+ className="flex-1"
|
|
|
+ >
|
|
|
+ {isLoading ? (
|
|
|
+ <>
|
|
|
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
+ Firmando...
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ "Firmar XML"
|
|
|
+ )}
|
|
|
+ </Button>
|
|
|
+
|
|
|
+ {signedXml && (
|
|
|
+ <>
|
|
|
+ <Button onClick={handleDownload} variant="outline">
|
|
|
+ <Download className="mr-2 h-4 w-4" />
|
|
|
+ Descargar XML Firmado
|
|
|
+ </Button>
|
|
|
+ <Button onClick={handleReset} variant="ghost">
|
|
|
+ Limpiar
|
|
|
+ </Button>
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Result Preview */}
|
|
|
+ {signedXml && (
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>XML Firmado (Vista Previa)</Label>
|
|
|
+ <div className="rounded-lg border bg-muted p-4 max-h-96 overflow-auto">
|
|
|
+ <pre className="text-xs font-mono whitespace-pre-wrap break-all">
|
|
|
+ {signedXml}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|