|
@@ -1,216 +1,52 @@
|
|
|
"use client"
|
|
"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() {
|
|
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 (
|
|
return (
|
|
|
<div className="container mx-auto max-w-4xl py-8 space-y-8">
|
|
<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>
|
|
</div>
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|