page.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. "use client"
  2. import { useState, useCallback, useMemo } from "react"
  3. import { Button } from "@/components/ui/button"
  4. import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
  5. import { Input } from "@/components/ui/input"
  6. import { Label } from "@/components/ui/label"
  7. import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
  8. import { Textarea } from "@/components/ui/textarea"
  9. import { toast } from "sonner"
  10. import { Trash2, Plus, Download } from "lucide-react"
  11. // Interfaces TypeScript para tipos estrictos
  12. interface InfoTributaria {
  13. ambiente: string
  14. tipoEmision: string
  15. razonSocial: string
  16. nombreComercial: string
  17. ruc: string
  18. claveAcceso: string
  19. estab: string
  20. ptoEmi: string
  21. secuencial: string
  22. dirMatriz: string
  23. }
  24. interface InfoFactura {
  25. fechaEmision: string
  26. dirEstablecimiento: string
  27. contribuyenteEspecial: string
  28. obligadoContabilidad: string
  29. tipoIdentificacionComprador: string
  30. razonSocialComprador: string
  31. identificacionComprador: string
  32. direccionComprador: string
  33. totalSinImpuestos: string
  34. totalDescuento: string
  35. codigoPorcentaje: string
  36. baseImponible: string
  37. valorImpuesto: string
  38. importeTotal: string
  39. formaPago: string
  40. emailComprador: string
  41. }
  42. interface DetalleItem {
  43. id: string
  44. codigoPrincipal: string
  45. descripcion: string
  46. cantidad: string
  47. precioUnitario: string
  48. descuento: string
  49. precioTotalSinImpuesto: string
  50. codigoPorcentaje: string
  51. tarifa: string
  52. baseImponible: string
  53. valorImpuesto: string
  54. }
  55. interface FacturaData {
  56. infoTributaria: InfoTributaria
  57. infoFactura: InfoFactura
  58. detalles: DetalleItem[]
  59. }
  60. // Componente para item de detalle
  61. function DetalleItemForm({
  62. item,
  63. onChange,
  64. onRemove
  65. }: {
  66. item: DetalleItem
  67. onChange: (item: DetalleItem) => void
  68. onRemove: () => void
  69. }) {
  70. const handleChange = (field: keyof DetalleItem, value: string) => {
  71. const updatedItem = { ...item, [field]: value }
  72. // Cálculos automáticos
  73. if (field === 'cantidad' || field === 'precioUnitario' || field === 'descuento') {
  74. const cantidad = parseFloat(updatedItem.cantidad) || 0
  75. const precioUnitario = parseFloat(updatedItem.precioUnitario) || 0
  76. const descuento = parseFloat(updatedItem.descuento) || 0
  77. const subtotal = cantidad * precioUnitario
  78. const precioTotalSinImpuesto = Math.max(0, subtotal - descuento)
  79. updatedItem.precioTotalSinImpuesto = precioTotalSinImpuesto.toFixed(2)
  80. updatedItem.baseImponible = precioTotalSinImpuesto.toFixed(2)
  81. // Calcular IVA (12% por defecto)
  82. const tarifa = parseFloat(updatedItem.tarifa) || 12
  83. updatedItem.valorImpuesto = (precioTotalSinImpuesto * tarifa / 100).toFixed(2)
  84. }
  85. if (field === 'tarifa') {
  86. const baseImponible = parseFloat(updatedItem.baseImponible) || 0
  87. const tarifa = parseFloat(value) || 12
  88. updatedItem.valorImpuesto = (baseImponible * tarifa / 100).toFixed(2)
  89. }
  90. onChange(updatedItem)
  91. }
  92. return (
  93. <Card className="p-4">
  94. <div className="flex justify-between items-center mb-4">
  95. <h4 className="text-sm font-medium">Detalle del Producto</h4>
  96. <Button
  97. type="button"
  98. variant="destructive"
  99. size="sm"
  100. onClick={onRemove}
  101. >
  102. <Trash2 className="h-4 w-4" />
  103. </Button>
  104. </div>
  105. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  106. <div className="space-y-2">
  107. <Label htmlFor={`codigo-${item.id}`}>Código Principal</Label>
  108. <Input
  109. id={`codigo-${item.id}`}
  110. value={item.codigoPrincipal}
  111. onChange={(e) => handleChange('codigoPrincipal', e.target.value)}
  112. placeholder="PROD001"
  113. />
  114. </div>
  115. <div className="space-y-2">
  116. <Label htmlFor={`descripcion-${item.id}`}>Descripción</Label>
  117. <Input
  118. id={`descripcion-${item.id}`}
  119. value={item.descripcion}
  120. onChange={(e) => handleChange('descripcion', e.target.value)}
  121. placeholder="Descripción del producto"
  122. />
  123. </div>
  124. <div className="space-y-2">
  125. <Label htmlFor={`cantidad-${item.id}`}>Cantidad</Label>
  126. <Input
  127. id={`cantidad-${item.id}`}
  128. type="number"
  129. step="0.01"
  130. value={item.cantidad}
  131. onChange={(e) => handleChange('cantidad', e.target.value)}
  132. placeholder="1"
  133. />
  134. </div>
  135. <div className="space-y-2">
  136. <Label htmlFor={`precio-unitario-${item.id}`}>Precio Unitario</Label>
  137. <Input
  138. id={`precio-unitario-${item.id}`}
  139. type="number"
  140. step="0.01"
  141. value={item.precioUnitario}
  142. onChange={(e) => handleChange('precioUnitario', e.target.value)}
  143. placeholder="10.00"
  144. />
  145. </div>
  146. <div className="space-y-2">
  147. <Label htmlFor={`descuento-${item.id}`}>Descuento</Label>
  148. <Input
  149. id={`descuento-${item.id}`}
  150. type="number"
  151. step="0.01"
  152. value={item.descuento}
  153. onChange={(e) => handleChange('descuento', e.target.value)}
  154. placeholder="0.00"
  155. />
  156. </div>
  157. <div className="space-y-2">
  158. <Label htmlFor={`precio-total-${item.id}`}>Total sin Impuesto</Label>
  159. <Input
  160. id={`precio-total-${item.id}`}
  161. value={item.precioTotalSinImpuesto}
  162. readOnly
  163. className="bg-muted"
  164. />
  165. </div>
  166. <div className="space-y-2">
  167. <Label htmlFor={`codigo-porcentaje-${item.id}`}>Código IVA</Label>
  168. <Select value={item.codigoPorcentaje} onValueChange={(value) => handleChange('codigoPorcentaje', value)}>
  169. <SelectTrigger>
  170. <SelectValue placeholder="Seleccionar" />
  171. </SelectTrigger>
  172. <SelectContent>
  173. <SelectItem value="2">2 - IVA</SelectItem>
  174. <SelectItem value="0">0 - No objeto de IVA</SelectItem>
  175. </SelectContent>
  176. </Select>
  177. </div>
  178. <div className="space-y-2">
  179. <Label htmlFor={`tarifa-${item.id}`}>Tarifa IVA (%)</Label>
  180. <Select value={item.tarifa} onValueChange={(value) => handleChange('tarifa', value)}>
  181. <SelectTrigger>
  182. <SelectValue placeholder="Seleccionar" />
  183. </SelectTrigger>
  184. <SelectContent>
  185. <SelectItem value="12">12%</SelectItem>
  186. <SelectItem value="0">0%</SelectItem>
  187. <SelectItem value="14">14%</SelectItem>
  188. </SelectContent>
  189. </Select>
  190. </div>
  191. <div className="space-y-2">
  192. <Label htmlFor={`valor-impuesto-${item.id}`}>Valor Impuesto</Label>
  193. <Input
  194. id={`valor-impuesto-${item.id}`}
  195. value={item.valorImpuesto}
  196. readOnly
  197. className="bg-muted"
  198. />
  199. </div>
  200. </div>
  201. </Card>
  202. )
  203. }
  204. // Funciones de validación específicas para Ecuador
  205. const validateRUC = (ruc: string): boolean => {
  206. // Validación básica de RUC ecuatoriano (13 dígitos)
  207. return /^\d{13}$/.test(ruc)
  208. }
  209. const validateCedula = (cedula: string): boolean => {
  210. // Validación básica de cédula ecuatoriana (10 dígitos)
  211. return /^\d{10}$/.test(cedula)
  212. }
  213. const validateClaveAcceso = (clave: string): boolean => {
  214. // Validación básica de clave de acceso SRI (49 dígitos)
  215. return /^\d{49}$/.test(clave)
  216. }
  217. export default function FacturaPage() {
  218. // Estado inicial
  219. const [infoTributaria, setInfoTributaria] = useState<InfoTributaria>({
  220. ambiente: '1',
  221. tipoEmision: '1',
  222. razonSocial: '',
  223. nombreComercial: '',
  224. ruc: '',
  225. claveAcceso: '',
  226. estab: '001',
  227. ptoEmi: '001',
  228. secuencial: '000000001',
  229. dirMatriz: ''
  230. })
  231. const [infoFactura, setInfoFactura] = useState<InfoFactura>({
  232. fechaEmision: new Date().toISOString().split('T')[0],
  233. dirEstablecimiento: '',
  234. contribuyenteEspecial: '',
  235. obligadoContabilidad: 'SI',
  236. tipoIdentificacionComprador: '04',
  237. razonSocialComprador: '',
  238. identificacionComprador: '',
  239. direccionComprador: '',
  240. totalSinImpuestos: '0.00',
  241. totalDescuento: '0.00',
  242. codigoPorcentaje: '2',
  243. baseImponible: '0.00',
  244. valorImpuesto: '0.00',
  245. importeTotal: '0.00',
  246. formaPago: '01',
  247. emailComprador: ''
  248. })
  249. const [detalles, setDetalles] = useState<DetalleItem[]>([
  250. {
  251. id: '1',
  252. codigoPrincipal: '',
  253. descripcion: '',
  254. cantidad: '1',
  255. precioUnitario: '0.00',
  256. descuento: '0.00',
  257. precioTotalSinImpuesto: '0.00',
  258. codigoPorcentaje: '2',
  259. tarifa: '12',
  260. baseImponible: '0.00',
  261. valorImpuesto: '0.00'
  262. }
  263. ])
  264. const [xmlGenerado, setXmlGenerado] = useState('')
  265. // Calcular totales automáticamente
  266. const calcularTotales = useCallback(() => {
  267. let totalSinImpuestos = 0
  268. let totalDescuento = 0
  269. let baseImponibleIVA = 0
  270. let valorIVA = 0
  271. detalles.forEach(item => {
  272. const subtotal = parseFloat(item.precioTotalSinImpuesto) || 0
  273. const descuento = parseFloat(item.descuento) || 0
  274. const impuesto = parseFloat(item.valorImpuesto) || 0
  275. totalSinImpuestos += subtotal
  276. totalDescuento += descuento
  277. baseImponibleIVA += subtotal
  278. valorIVA += impuesto
  279. })
  280. const importeTotal = totalSinImpuestos + valorIVA
  281. setInfoFactura(prev => ({
  282. ...prev,
  283. totalSinImpuestos: totalSinImpuestos.toFixed(2),
  284. totalDescuento: totalDescuento.toFixed(2),
  285. baseImponible: baseImponibleIVA.toFixed(2),
  286. valorImpuesto: valorIVA.toFixed(2),
  287. importeTotal: importeTotal.toFixed(2)
  288. }))
  289. }, [detalles])
  290. // Recalcular totales cuando cambian los detalles
  291. useMemo(() => {
  292. calcularTotales()
  293. }, [detalles, calcularTotales])
  294. // Manejar cambios en info tributaria
  295. const handleInfoTributariaChange = (field: keyof InfoTributaria, value: string) => {
  296. setInfoTributaria(prev => ({ ...prev, [field]: value }))
  297. }
  298. // Manejar cambios en info factura
  299. const handleInfoFacturaChange = (field: keyof InfoFactura, value: string) => {
  300. setInfoFactura(prev => ({ ...prev, [field]: value }))
  301. }
  302. // Agregar nuevo detalle
  303. const agregarDetalle = () => {
  304. const nuevoDetalle: DetalleItem = {
  305. id: Date.now().toString(),
  306. codigoPrincipal: '',
  307. descripcion: '',
  308. cantidad: '1',
  309. precioUnitario: '0.00',
  310. descuento: '0.00',
  311. precioTotalSinImpuesto: '0.00',
  312. codigoPorcentaje: '2',
  313. tarifa: '12',
  314. baseImponible: '0.00',
  315. valorImpuesto: '0.00'
  316. }
  317. setDetalles(prev => [...prev, nuevoDetalle])
  318. }
  319. // Actualizar detalle
  320. const actualizarDetalle = (id: string, updatedItem: DetalleItem) => {
  321. setDetalles(prev => prev.map(item => item.id === id ? updatedItem : item))
  322. }
  323. // Eliminar detalle
  324. const eliminarDetalle = (id: string) => {
  325. if (detalles.length > 1) {
  326. setDetalles(prev => prev.filter(item => item.id !== id))
  327. toast.success('Item eliminado')
  328. } else {
  329. toast.error('Debe haber al menos un item')
  330. }
  331. }
  332. // Generar XML
  333. const generarXML = () => {
  334. // Validaciones básicas
  335. if (!validateRUC(infoTributaria.ruc)) {
  336. toast.error('RUC inválido. Debe tener 13 dígitos')
  337. return
  338. }
  339. if (!validateClaveAcceso(infoTributaria.claveAcceso)) {
  340. toast.error('Clave de acceso inválida. Debe tener 49 dígitos')
  341. return
  342. }
  343. if (!infoFactura.identificacionComprador) {
  344. toast.error('La identificación del comprador es requerida')
  345. return
  346. }
  347. if (infoFactura.tipoIdentificacionComprador === '04' && !validateRUC(infoFactura.identificacionComprador)) {
  348. toast.error('RUC del comprador inválido')
  349. return
  350. }
  351. // Generar XML (simulación del código PHP)
  352. let xml = `<?xml version="1.0" encoding="UTF-8"?>
  353. <factura id="comprobante" version="1.1.0">
  354. <infoTributaria>
  355. <ambiente>${infoTributaria.ambiente}</ambiente>
  356. <tipoEmision>${infoTributaria.tipoEmision}</tipoEmision>
  357. <razonSocial>${infoTributaria.razonSocial}</razonSocial>
  358. <nombreComercial>${infoTributaria.nombreComercial}</nombreComercial>
  359. <ruc>${infoTributaria.ruc}</ruc>
  360. <claveAcceso>${infoTributaria.claveAcceso}</claveAcceso>
  361. <codDoc>01</codDoc>
  362. <estab>${infoTributaria.estab}</estab>
  363. <ptoEmi>${infoTributaria.ptoEmi}</ptoEmi>
  364. <secuencial>${infoTributaria.secuencial}</secuencial>
  365. <dirMatriz>${infoTributaria.dirMatriz}</dirMatriz>
  366. </infoTributaria>
  367. <infoFactura>
  368. <fechaEmision>${infoFactura.fechaEmision}</fechaEmision>
  369. <dirEstablecimiento>${infoFactura.dirEstablecimiento}</dirEstablecimiento>
  370. <contribuyenteEspecial>${infoFactura.contribuyenteEspecial}</contribuyenteEspecial>
  371. <obligadoContabilidad>${infoFactura.obligadoContabilidad}</obligadoContabilidad>
  372. <tipoIdentificacionComprador>${infoFactura.tipoIdentificacionComprador}</tipoIdentificacionComprador>
  373. <razonSocialComprador>${infoFactura.razonSocialComprador}</razonSocialComprador>
  374. <identificacionComprador>${infoFactura.identificacionComprador}</identificacionComprador>
  375. <direccionComprador>${infoFactura.direccionComprador}</direccionComprador>
  376. <totalSinImpuestos>${infoFactura.totalSinImpuestos}</totalSinImpuestos>
  377. <totalDescuento>${infoFactura.totalDescuento}</totalDescuento>
  378. <totalConImpuestos>
  379. <totalImpuesto>
  380. <codigo>2</codigo>
  381. <codigoPorcentaje>${infoFactura.codigoPorcentaje}</codigoPorcentaje>
  382. <baseImponible>${infoFactura.baseImponible}</baseImponible>
  383. <valor>${infoFactura.valorImpuesto}</valor>
  384. </totalImpuesto>
  385. </totalConImpuestos>
  386. <propina>0.00</propina>
  387. <importeTotal>${infoFactura.importeTotal}</importeTotal>
  388. <moneda>DOLAR</moneda>
  389. <pagos>
  390. <pago>
  391. <formaPago>${infoFactura.formaPago}</formaPago>
  392. <total>${infoFactura.importeTotal}</total>
  393. </pago>
  394. </pagos>
  395. </infoFactura>
  396. <detalles>`
  397. detalles.forEach(item => {
  398. xml += `
  399. <detalle>
  400. <codigoPrincipal>${item.codigoPrincipal}</codigoPrincipal>
  401. <descripcion>${item.descripcion}</descripcion>
  402. <cantidad>${item.cantidad}</cantidad>
  403. <precioUnitario>${item.precioUnitario}</precioUnitario>
  404. <descuento>${item.descuento}</descuento>
  405. <precioTotalSinImpuesto>${item.precioTotalSinImpuesto}</precioTotalSinImpuesto>
  406. <impuestos>
  407. <impuesto>
  408. <codigo>2</codigo>
  409. <codigoPorcentaje>${item.codigoPorcentaje}</codigoPorcentaje>
  410. <tarifa>${item.tarifa}</tarifa>
  411. <baseImponible>${item.baseImponible}</baseImponible>
  412. <valor>${item.valorImpuesto}</valor>
  413. </impuesto>
  414. </impuestos>
  415. </detalle>`
  416. })
  417. xml += `
  418. </detalles>
  419. <infoAdicional>
  420. <campoAdicional nombre="Email">${infoFactura.emailComprador}</campoAdicional>
  421. </infoAdicional>
  422. </factura>`
  423. setXmlGenerado(xml)
  424. toast.success('XML generado exitosamente')
  425. }
  426. // Descargar XML
  427. const descargarXML = () => {
  428. if (!xmlGenerado) {
  429. toast.error('Debe generar el XML primero')
  430. return
  431. }
  432. const blob = new Blob([xmlGenerado], { type: 'application/xml' })
  433. const url = URL.createObjectURL(blob)
  434. const a = document.createElement('a')
  435. a.href = url
  436. a.download = `factura_${infoTributaria.ruc}_${new Date().getTime()}.xml`
  437. document.body.appendChild(a)
  438. a.click()
  439. document.body.removeChild(a)
  440. URL.revokeObjectURL(url)
  441. toast.success('XML descargado')
  442. }
  443. return (
  444. <div className="space-y-6">
  445. {/* Header */}
  446. <div className="text-center">
  447. <h1 className="text-4xl font-bold tracking-tight">Factura Electrónica SRI Ecuador</h1>
  448. <p className="mt-2 text-muted-foreground">
  449. Generador de XML para facturación electrónica SRI
  450. </p>
  451. </div>
  452. {/* Info Tributaria */}
  453. <Card>
  454. <CardHeader>
  455. <CardTitle>Información Tributaria</CardTitle>
  456. <CardDescription>Datos del emisor de la factura</CardDescription>
  457. </CardHeader>
  458. <CardContent>
  459. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  460. <div className="space-y-2">
  461. <Label htmlFor="ambiente">Ambiente</Label>
  462. <Select value={infoTributaria.ambiente} onValueChange={(value) => handleInfoTributariaChange('ambiente', value)}>
  463. <SelectTrigger>
  464. <SelectValue />
  465. </SelectTrigger>
  466. <SelectContent>
  467. <SelectItem value="1">1 - Pruebas</SelectItem>
  468. <SelectItem value="2">2 - Producción</SelectItem>
  469. </SelectContent>
  470. </Select>
  471. </div>
  472. <div className="space-y-2">
  473. <Label htmlFor="tipoEmision">Tipo Emisión</Label>
  474. <Select value={infoTributaria.tipoEmision} onValueChange={(value) => handleInfoTributariaChange('tipoEmision', value)}>
  475. <SelectTrigger>
  476. <SelectValue />
  477. </SelectTrigger>
  478. <SelectContent>
  479. <SelectItem value="1">1 - Normal</SelectItem>
  480. </SelectContent>
  481. </Select>
  482. </div>
  483. <div className="space-y-2">
  484. <Label htmlFor="ruc">RUC *</Label>
  485. <Input
  486. id="ruc"
  487. value={infoTributaria.ruc}
  488. onChange={(e) => handleInfoTributariaChange('ruc', e.target.value)}
  489. placeholder="13 dígitos"
  490. maxLength={13}
  491. />
  492. </div>
  493. <div className="space-y-2">
  494. <Label htmlFor="razonSocial">Razón Social *</Label>
  495. <Input
  496. id="razonSocial"
  497. value={infoTributaria.razonSocial}
  498. onChange={(e) => handleInfoTributariaChange('razonSocial', e.target.value)}
  499. placeholder="Empresa S.A."
  500. />
  501. </div>
  502. <div className="space-y-2">
  503. <Label htmlFor="nombreComercial">Nombre Comercial *</Label>
  504. <Input
  505. id="nombreComercial"
  506. value={infoTributaria.nombreComercial}
  507. onChange={(e) => handleInfoTributariaChange('nombreComercial', e.target.value)}
  508. placeholder="Nombre Comercial"
  509. />
  510. </div>
  511. <div className="space-y-2">
  512. <Label htmlFor="claveAcceso">Clave de Acceso *</Label>
  513. <Input
  514. id="claveAcceso"
  515. value={infoTributaria.claveAcceso}
  516. onChange={(e) => handleInfoTributariaChange('claveAcceso', e.target.value)}
  517. placeholder="49 dígitos"
  518. maxLength={49}
  519. />
  520. </div>
  521. <div className="space-y-2">
  522. <Label htmlFor="estab">Establecimiento</Label>
  523. <Input
  524. id="estab"
  525. value={infoTributaria.estab}
  526. onChange={(e) => handleInfoTributariaChange('estab', e.target.value)}
  527. placeholder="001"
  528. maxLength={3}
  529. />
  530. </div>
  531. <div className="space-y-2">
  532. <Label htmlFor="ptoEmi">Punto Emisión</Label>
  533. <Input
  534. id="ptoEmi"
  535. value={infoTributaria.ptoEmi}
  536. onChange={(e) => handleInfoTributariaChange('ptoEmi', e.target.value)}
  537. placeholder="001"
  538. maxLength={3}
  539. />
  540. </div>
  541. <div className="space-y-2">
  542. <Label htmlFor="secuencial">Secuencial</Label>
  543. <Input
  544. id="secuencial"
  545. value={infoTributaria.secuencial}
  546. onChange={(e) => handleInfoTributariaChange('secuencial', e.target.value)}
  547. placeholder="000000001"
  548. maxLength={9}
  549. />
  550. </div>
  551. <div className="space-y-2 lg:col-span-3">
  552. <Label htmlFor="dirMatriz">Dirección Matriz *</Label>
  553. <Input
  554. id="dirMatriz"
  555. value={infoTributaria.dirMatriz}
  556. onChange={(e) => handleInfoTributariaChange('dirMatriz', e.target.value)}
  557. placeholder="Av. Principal 123 y Secundaria"
  558. />
  559. </div>
  560. </div>
  561. </CardContent>
  562. </Card>
  563. {/* Info Factura */}
  564. <Card>
  565. <CardHeader>
  566. <CardTitle>Información de Factura</CardTitle>
  567. <CardDescription>Datos de la factura y comprador</CardDescription>
  568. </CardHeader>
  569. <CardContent>
  570. <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  571. <div className="space-y-2">
  572. <Label htmlFor="fechaEmision">Fecha Emisión *</Label>
  573. <Input
  574. id="fechaEmision"
  575. type="date"
  576. value={infoFactura.fechaEmision}
  577. onChange={(e) => handleInfoFacturaChange('fechaEmision', e.target.value)}
  578. />
  579. </div>
  580. <div className="space-y-2">
  581. <Label htmlFor="dirEstablecimiento">Dirección Establecimiento *</Label>
  582. <Input
  583. id="dirEstablecimiento"
  584. value={infoFactura.dirEstablecimiento}
  585. onChange={(e) => handleInfoFacturaChange('dirEstablecimiento', e.target.value)}
  586. placeholder="Av. Secundaria 456"
  587. />
  588. </div>
  589. <div className="space-y-2">
  590. <Label htmlFor="contribuyenteEspecial">Contribuyente Especial</Label>
  591. <Input
  592. id="contribuyenteEspecial"
  593. value={infoFactura.contribuyenteEspecial}
  594. onChange={(e) => handleInfoFacturaChange('contribuyenteEspecial', e.target.value)}
  595. placeholder="Opcional"
  596. />
  597. </div>
  598. <div className="space-y-2">
  599. <Label htmlFor="obligadoContabilidad">Obligado Contabilidad</Label>
  600. <Select value={infoFactura.obligadoContabilidad} onValueChange={(value) => handleInfoFacturaChange('obligadoContabilidad', value)}>
  601. <SelectTrigger>
  602. <SelectValue />
  603. </SelectTrigger>
  604. <SelectContent>
  605. <SelectItem value="SI">SI</SelectItem>
  606. <SelectItem value="NO">NO</SelectItem>
  607. </SelectContent>
  608. </Select>
  609. </div>
  610. <div className="space-y-2">
  611. <Label htmlFor="tipoIdentificacionComprador">Tipo Identificación Comprador</Label>
  612. <Select value={infoFactura.tipoIdentificacionComprador} onValueChange={(value) => handleInfoFacturaChange('tipoIdentificacionComprador', value)}>
  613. <SelectTrigger>
  614. <SelectValue />
  615. </SelectTrigger>
  616. <SelectContent>
  617. <SelectItem value="04">04 - RUC</SelectItem>
  618. <SelectItem value="05">05 - Cédula</SelectItem>
  619. <SelectItem value="06">06 - Pasaporte</SelectItem>
  620. <SelectItem value="07">07 - Consumidor Final</SelectItem>
  621. </SelectContent>
  622. </Select>
  623. </div>
  624. <div className="space-y-2">
  625. <Label htmlFor="identificacionComprador">Identificación Comprador *</Label>
  626. <Input
  627. id="identificacionComprador"
  628. value={infoFactura.identificacionComprador}
  629. onChange={(e) => handleInfoFacturaChange('identificacionComprador', e.target.value)}
  630. placeholder="Según tipo seleccionado"
  631. />
  632. </div>
  633. <div className="space-y-2">
  634. <Label htmlFor="razonSocialComprador">Razón Social Comprador *</Label>
  635. <Input
  636. id="razonSocialComprador"
  637. value={infoFactura.razonSocialComprador}
  638. onChange={(e) => handleInfoFacturaChange('razonSocialComprador', e.target.value)}
  639. placeholder="Nombre del comprador"
  640. />
  641. </div>
  642. <div className="space-y-2">
  643. <Label htmlFor="direccionComprador">Dirección Comprador *</Label>
  644. <Input
  645. id="direccionComprador"
  646. value={infoFactura.direccionComprador}
  647. onChange={(e) => handleInfoFacturaChange('direccionComprador', e.target.value)}
  648. placeholder="Dirección del comprador"
  649. />
  650. </div>
  651. <div className="space-y-2">
  652. <Label htmlFor="formaPago">Forma de Pago</Label>
  653. <Select value={infoFactura.formaPago} onValueChange={(value) => handleInfoFacturaChange('formaPago', value)}>
  654. <SelectTrigger>
  655. <SelectValue />
  656. </SelectTrigger>
  657. <SelectContent>
  658. <SelectItem value="01">01 - Efectivo</SelectItem>
  659. <SelectItem value="15">15 - Transferencia</SelectItem>
  660. <SelectItem value="16">16 - Tarjeta Crédito</SelectItem>
  661. <SelectItem value="17">17 - Tarjeta Débito</SelectItem>
  662. <SelectItem value="20">20 - Otros</SelectItem>
  663. </SelectContent>
  664. </Select>
  665. </div>
  666. <div className="space-y-2">
  667. <Label htmlFor="emailComprador">Email Comprador</Label>
  668. <Input
  669. id="emailComprador"
  670. type="email"
  671. value={infoFactura.emailComprador}
  672. onChange={(e) => handleInfoFacturaChange('emailComprador', e.target.value)}
  673. placeholder="email@ejemplo.com"
  674. />
  675. </div>
  676. </div>
  677. </CardContent>
  678. </Card>
  679. {/* Detalles */}
  680. <Card>
  681. <CardHeader>
  682. <div className="flex justify-between items-center">
  683. <div>
  684. <CardTitle>Detalles de Productos/Servicios</CardTitle>
  685. <CardDescription>Agregar los productos o servicios de la factura</CardDescription>
  686. </div>
  687. <Button type="button" onClick={agregarDetalle} className="flex items-center gap-2">
  688. <Plus className="h-4 w-4" />
  689. Agregar Item
  690. </Button>
  691. </div>
  692. </CardHeader>
  693. <CardContent className="space-y-4">
  694. {detalles.map((item) => (
  695. <DetalleItemForm
  696. key={item.id}
  697. item={item}
  698. onChange={(updatedItem) => actualizarDetalle(item.id, updatedItem)}
  699. onRemove={() => eliminarDetalle(item.id)}
  700. />
  701. ))}
  702. </CardContent>
  703. </Card>
  704. {/* Totales */}
  705. <Card>
  706. <CardHeader>
  707. <CardTitle>Resumen de Totales</CardTitle>
  708. </CardHeader>
  709. <CardContent>
  710. <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
  711. <div className="space-y-2">
  712. <Label>Total Sin Impuestos</Label>
  713. <Input value={infoFactura.totalSinImpuestos} readOnly className="bg-muted" />
  714. </div>
  715. <div className="space-y-2">
  716. <Label>Valor IVA</Label>
  717. <Input value={infoFactura.valorImpuesto} readOnly className="bg-muted" />
  718. </div>
  719. <div className="space-y-2">
  720. <Label>Importe Total</Label>
  721. <Input value={infoFactura.importeTotal} readOnly className="bg-muted font-bold text-lg" />
  722. </div>
  723. </div>
  724. </CardContent>
  725. </Card>
  726. {/* Acciones */}
  727. <div className="flex gap-4">
  728. <Button onClick={generarXML} className="flex-1">
  729. Generar XML
  730. </Button>
  731. {xmlGenerado && (
  732. <Button variant="outline" onClick={descargarXML} className="flex items-center gap-2">
  733. <Download className="h-4 w-4" />
  734. Descargar XML
  735. </Button>
  736. )}
  737. </div>
  738. {/* XML Generado */}
  739. {xmlGenerado && (
  740. <Card>
  741. <CardHeader>
  742. <CardTitle>XML Generado</CardTitle>
  743. <CardDescription>XML generado para el SRI</CardDescription>
  744. </CardHeader>
  745. <CardContent>
  746. <Textarea
  747. value={xmlGenerado}
  748. readOnly
  749. rows={20}
  750. className="font-mono text-sm"
  751. />
  752. </CardContent>
  753. </Card>
  754. )}
  755. </div>
  756. )
  757. }