Bladeren bron

fix sign page

Matthew Trejo 1 maand geleden
bovenliggende
commit
b068a3ef26

+ 42 - 206
src/app/firmar/page.tsx

@@ -1,216 +1,52 @@
 "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"
+import { FirmarHeader } from "@/components/firmar/FirmarHeader"
+import { FileUploadSection } from "@/components/firmar/FileUploadSection"
+import { SignedXmlPreview } from "@/components/firmar/SignedXmlPreview"
+import { SignActions } from "@/components/firmar/SignActions"
+import { useXmlSigning } from "@/hooks/firmar/useXmlSigning"
 
 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)
-  }
+  const {
+    xmlFile,
+    p12File,
+    password,
+    isLoading,
+    signedXml,
+    setXmlFile,
+    setP12File,
+    setPassword,
+    handleSign,
+    handleDownload,
+    handleReset,
+  } = useXmlSigning()
 
   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>
-      )}
+      <FirmarHeader />
+
+      <FileUploadSection
+        xmlFile={xmlFile}
+        p12File={p12File}
+        password={password}
+        isLoading={isLoading}
+        onXmlFileChange={setXmlFile}
+        onP12FileChange={setP12File}
+        onPasswordChange={setPassword}
+      />
+
+      <SignActions
+        xmlFile={xmlFile}
+        p12File={p12File}
+        password={password}
+        isLoading={isLoading}
+        signedXml={signedXml}
+        onSign={handleSign}
+        onDownload={handleDownload}
+        onReset={handleReset}
+      />
+
+      {signedXml && <SignedXmlPreview signedXml={signedXml} />}
     </div>
   )
 }

+ 121 - 0
src/components/firmar/FileUploadSection.tsx

@@ -0,0 +1,121 @@
+import { FileText, FileKey, Check } from "lucide-react"
+import { Label } from "@/components/ui/label"
+import { Input } from "@/components/ui/input"
+import { Dropzone } from "@/components/ui/shadcn-io/dropzone"
+import { toast } from "sonner"
+
+interface FileUploadSectionProps {
+  xmlFile: File | null
+  p12File: File | null
+  password: string
+  isLoading: boolean
+  onXmlFileChange: (file: File | null) => void
+  onP12FileChange: (file: File | null) => void
+  onPasswordChange: (password: string) => void
+}
+
+export function FileUploadSection({
+  xmlFile,
+  p12File,
+  password,
+  isLoading,
+  onXmlFileChange,
+  onP12FileChange,
+  onPasswordChange,
+}: FileUploadSectionProps) {
+  return (
+    <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) {
+              onXmlFileChange(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) {
+              onP12FileChange(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) => onPasswordChange(e.target.value)}
+          disabled={isLoading}
+        />
+      </div>
+    </div>
+  )
+}

+ 10 - 0
src/components/firmar/FirmarHeader.tsx

@@ -0,0 +1,10 @@
+export function FirmarHeader() {
+  return (
+    <div className="mb-6">
+      <h2 className="text-2xl font-bold">Firmar Factura Electrónica</h2>
+      <p className="mt-2 text-muted-foreground">
+        Sube tu archivo XML y tu certificado digital para firmar electrónicamente tu factura
+      </p>
+    </div>
+  )
+}

+ 55 - 0
src/components/firmar/SignActions.tsx

@@ -0,0 +1,55 @@
+import { Loader2, Download } from "lucide-react"
+import { Button } from "@/components/ui/button"
+
+interface SignActionsProps {
+  xmlFile: File | null
+  p12File: File | null
+  password: string
+  isLoading: boolean
+  signedXml: string | null
+  onSign: () => void
+  onDownload: () => void
+  onReset: () => void
+}
+
+export function SignActions({
+  xmlFile,
+  p12File,
+  password,
+  isLoading,
+  signedXml,
+  onSign,
+  onDownload,
+  onReset,
+}: SignActionsProps) {
+  return (
+    <div className="flex gap-4">
+      <Button
+        onClick={onSign}
+        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={onDownload} variant="outline">
+            <Download className="mr-2 h-4 w-4" />
+            Descargar XML Firmado
+          </Button>
+          <Button onClick={onReset} variant="ghost">
+            Limpiar
+          </Button>
+        </>
+      )}
+    </div>
+  )
+}

+ 18 - 0
src/components/firmar/SignedXmlPreview.tsx

@@ -0,0 +1,18 @@
+import { Label } from "@/components/ui/label"
+
+interface SignedXmlPreviewProps {
+  signedXml: string
+}
+
+export function SignedXmlPreview({ signedXml }: SignedXmlPreviewProps) {
+  return (
+    <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>
+  )
+}

+ 87 - 0
src/hooks/firmar/useXmlSigning.ts

@@ -0,0 +1,87 @@
+import { useState } from "react"
+import { toast } from "sonner"
+
+export function useXmlSigning() {
+  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 {
+    // State
+    xmlFile,
+    p12File,
+    password,
+    isLoading,
+    signedXml,
+
+    // Setters
+    setXmlFile,
+    setP12File,
+    setPassword,
+
+    // Actions
+    handleSign,
+    handleDownload,
+    handleReset,
+  }
+}