feat: Implement invoice management features including data table, dialogs for viewing, editing, and deleting invoices, and server-side loading of invoice data
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
from enum import Enum
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import BigInteger, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
|
||||
from sqlalchemy import BigInteger, Boolean, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
from datetime import datetime
|
||||
@@ -11,21 +11,35 @@ class OperationType(str, Enum):
|
||||
IMP = "imp" # Importación
|
||||
EXP = "exp" # Exportación
|
||||
|
||||
|
||||
class TransportType(str, Enum):
|
||||
NONE = "none" # Ninguno
|
||||
TRANSPORT = "transport" # Transporte
|
||||
BOX = "box" # Caja
|
||||
PLATES = "licence plates" # Placas
|
||||
TRUCK = "truck" # Camión
|
||||
VESSEL = "vessel" # Buque
|
||||
BARGE = "rail barge" # Ferrobarcaza
|
||||
CONTAINER = "container" # Contenedor
|
||||
AIRPLANE = "airplane" # Avión
|
||||
GONDOLA = "gondola" # Góndola
|
||||
FLATBED = "flatbed" # Plataforma
|
||||
|
||||
# --- 1. Invoice Header (invoice_header) ---
|
||||
|
||||
|
||||
class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_header"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
|
||||
# Identifiers
|
||||
operation_type: Mapped[OperationType] = mapped_column(
|
||||
String(3)) # TIPOMOVIMIENTO / Clasifica IMP/EXP
|
||||
invoice_type: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("public.invoice_types.key")) # TIPOFACTURA
|
||||
String(3)) # TIPOMOVIMIENTO / Clasifica imp/exp
|
||||
invoice_type: Mapped[Optional[str]] = mapped_column(ForeignKey(
|
||||
"public.invoice_types.key")) # TIPOFACTURA (invoice_types)
|
||||
invoice_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION
|
||||
project_number: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -42,20 +56,22 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
|
||||
|
||||
# Status & Control
|
||||
status: Mapped[Optional[str]] = mapped_column(String(5)) # ESTATUS
|
||||
traffic_light_status: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
|
||||
process_log: Mapped[Optional[str]] = mapped_column(
|
||||
String(300)) # COMOFUEPROCESADA **<-- AÑADIDO**
|
||||
# OBSERVACIONE + OBSERVACIONI + COMENTARIOSESTATUS
|
||||
comments: Mapped[Optional[str]] = mapped_column(Text)
|
||||
is_updated: Mapped[Optional[str]] = mapped_column(Boolean) # ESTATUS
|
||||
updated_date: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=False)) # FECHAACTUALIZACION
|
||||
who_updated: Mapped[Optional[str]] = mapped_column(String(20)) # QUIENA ACTUALIZO
|
||||
|
||||
traffic_light_status: Mapped[Optional[str]] = mapped_column(String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
|
||||
process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA
|
||||
|
||||
# Comments
|
||||
observation_es: Mapped[Optional[str]] = mapped_column(Text) #OBSERVACIONE
|
||||
observation_en: Mapped[Optional[str]] = mapped_column(Text) #OBSERVACIONI
|
||||
comments_status: Mapped[Optional[str]] = mapped_column(Text) #COMENTARIOSESTATUS
|
||||
|
||||
# Digital Archive Links
|
||||
cfdi_uuid: Mapped[Optional[str]] = mapped_column(String(100)) # CFDIUUID
|
||||
path_pdf: Mapped[Optional[str]] = mapped_column(
|
||||
String(500)) # CFDIPATHPDF **<-- AÑADIDO**
|
||||
path_xml: Mapped[Optional[str]] = mapped_column(
|
||||
String(500)) # CFDIPATHXML **<-- AÑADIDO**
|
||||
path_pdf: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHPDF
|
||||
path_xml: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHXML
|
||||
|
||||
# Relationships (Para navegacion ORM)
|
||||
compliance_mx: Mapped["InvoiceComplianceMx"] = relationship(
|
||||
@@ -73,10 +89,12 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 2. Compliance MX (invoice_compliance_mx) ---
|
||||
class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_compliance_mx"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
invoice_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("invoice_header.id"), primary_key=True)
|
||||
ForeignKey("a76.invoice_header.id"), primary_key=True)
|
||||
|
||||
# Core Customs Data
|
||||
pedimento: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -84,23 +102,29 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
pedimento_code: Mapped[Optional[str]] = mapped_column(
|
||||
String(5)) # PEDIMENTOR1, K1
|
||||
remesa: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
aduana: Mapped[Optional[str]] = mapped_column(String(5)) # ADUANA_CRUCE
|
||||
customs_agent: Mapped[Optional[str]] = mapped_column(
|
||||
String(10)) # AADUANAL
|
||||
aduana: Mapped[Optional[str]] = mapped_column(ForeignKey("public.customs_sections.customs_code")) # ADUANA_CRUCE
|
||||
|
||||
# Clients & Providers
|
||||
provider_header: Mapped[Optional[str]] = mapped_column(String(20)) # PROVEEDOREXPORTADOR
|
||||
provider_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # PROVEEDOR
|
||||
sold_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # VENDIDOCONSIGNADO
|
||||
sold_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOA
|
||||
shipped_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOTRANSFERIDO
|
||||
shipped_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # ENVIADOA
|
||||
shipped_by_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOPORVENDIDOPOR
|
||||
shipped_by_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOPOR
|
||||
|
||||
customs_broker_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANAL
|
||||
|
||||
# Flags & Specific Regimes
|
||||
is_mixed: Mapped[Optional[str]] = mapped_column(
|
||||
String(2)) # ESMIXTO **<-- AÑADIDO**
|
||||
waste_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(1)) # TIPODESPERDICIO **<-- AÑADIDO**
|
||||
is_mixed: Mapped[Optional[bool]] = mapped_column(Boolean) # ESMIXTO
|
||||
waste_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODESPERDICIO
|
||||
appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17
|
||||
|
||||
# VUCEM / Digital
|
||||
edocument: Mapped[Optional[str]] = mapped_column(String(50)) # EDOCUMENT
|
||||
electronic_signature: Mapped[Optional[str]] = mapped_column(
|
||||
String(999)) # FIRMAELECTRONICA
|
||||
sem_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
|
||||
electronic_signature: Mapped[Optional[str]] = mapped_column(String(999)) # FIRMAELECTRONICA
|
||||
sem_id: Mapped[Optional[int]] = mapped_column(Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
|
||||
|
||||
# Relationship
|
||||
header: Mapped["InvoiceHeader"] = relationship(
|
||||
@@ -110,40 +134,33 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 3. Financials (invoice_financials) ---
|
||||
class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_financials"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
invoice_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("invoice_header.id"), primary_key=True)
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA
|
||||
exchange_rate: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(13, 6)) # TIPOCAMBIO
|
||||
currency_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.currency_types.code")) # TIPOCLAVEMONEDA
|
||||
exchange_rate: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIO
|
||||
|
||||
# Merchandise Values
|
||||
value_mn: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
|
||||
value_me: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0)
|
||||
customs_value_mn: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0) # VALORADUANASMN **<-- AÑADIDO**
|
||||
value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
|
||||
value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0)
|
||||
customs_value_mn: Mapped[Optional[float]] = mapped_column( Numeric(23, 8), default=0) # VALORADUANASMN
|
||||
|
||||
# Costs & Taxes
|
||||
freight: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8), default=0) # FLETE
|
||||
insurance: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
|
||||
iva_mn: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
|
||||
iva_factor: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(17, 4)) # FACTORIVA
|
||||
freight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # FLETE
|
||||
insurance: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
|
||||
iva_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
|
||||
iva_factor: Mapped[Optional[float]] = mapped_column(Numeric(17, 4)) # FACTORIVA
|
||||
|
||||
# Weights & Quantities
|
||||
total_quantity: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8)) # CANTEXPO / CANTIMPO
|
||||
gross_weight: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8)) # PESOBRUTO **<-- AÑADIDO**
|
||||
net_weight: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(19, 8)) # PESONETO **<-- AÑADIDO**
|
||||
total_quantity: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # CANTEXPO / CANTIMPO
|
||||
gross_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESOBRUTO
|
||||
net_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESONETO
|
||||
bundle_count: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
|
||||
|
||||
# Relationship
|
||||
@@ -153,17 +170,21 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 4. Logistics (invoice_logistics) ---
|
||||
class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_logistics"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
logistics_id: Mapped[int] = mapped_column(
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
# Carrier Info
|
||||
carrier_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(10)) # TRANSPORTISTA
|
||||
transport_type: Mapped[TransportType] = mapped_column(
|
||||
String(15), default="none") # TRANSPORTE
|
||||
transport_mode: Mapped[Optional[str]] = mapped_column(
|
||||
String(15)) # TRANSPORTE
|
||||
String(15)) # MODTRANS
|
||||
driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR
|
||||
is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL
|
||||
rail_id: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -189,11 +210,13 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 5. Sales Order Details (invoice_sales_details) ---
|
||||
class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_sales_details"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
detail_id: Mapped[int] = mapped_column(
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
line_number: Mapped[int] = mapped_column(Integer) # LINEA
|
||||
sales_order: Mapped[Optional[str]] = mapped_column(
|
||||
@@ -214,11 +237,13 @@ class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
|
||||
# --- 6. Collections (invoice_collections) ---
|
||||
class InvoiceCollections(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_collections"
|
||||
__tableargs__ = {'schema': 'a76'}
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
collection_id: Mapped[int] = mapped_column(
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO
|
||||
is_collected: Mapped[Optional[int]] = mapped_column(Integer) # COBRADO
|
||||
|
||||
@@ -5,24 +5,35 @@ from pydantic import BaseModel, Field
|
||||
from .models import OperationType
|
||||
|
||||
|
||||
|
||||
# --- Base Schemas ---
|
||||
class InvoiceHeaderBase(BaseModel):
|
||||
"""Base fields for Invoice Header"""
|
||||
operation_type: Optional[OperationType] = Field(None, max_length=20, description="Operation type: imp/exp")
|
||||
invoice_type: Optional[str] = Field(None, max_length=5, description="Invoice type key")
|
||||
invoice_number: Optional[str] = Field(None, max_length=20, description="Invoice number")
|
||||
project_number: Optional[str] = Field(None, max_length=14, description="Project number")
|
||||
purchase_order: Optional[str] = Field(None, max_length=50, description="Purchase order")
|
||||
related_doc_id: Optional[int] = Field(None, description="Related document ID for rectifications")
|
||||
operation_type: Optional[OperationType] = Field(
|
||||
None, max_length=20, description="Operation type: imp/exp")
|
||||
invoice_type: Optional[str] = Field(
|
||||
None, max_length=5, description="Invoice type key")
|
||||
invoice_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Invoice number")
|
||||
project_number: Optional[str] = Field(
|
||||
None, max_length=14, description="Project number")
|
||||
purchase_order: Optional[str] = Field(
|
||||
None, max_length=50, description="Purchase order")
|
||||
related_doc_id: Optional[int] = Field(
|
||||
None, description="Related document ID for rectifications")
|
||||
invoice_date: Optional[date] = Field(None, description="Invoice date")
|
||||
status: Optional[str] = Field(None, max_length=5, description="Status")
|
||||
traffic_light_status: Optional[str] = Field(None, max_length=50, description="Traffic light status (SEMAFORO)")
|
||||
process_log: Optional[str] = Field(None, max_length=300, description="Processing log")
|
||||
comments: Optional[str] = Field(None, description="Comments and observations")
|
||||
cfdi_uuid: Optional[str] = Field(None, max_length=100, description="CFDI UUID")
|
||||
path_pdf: Optional[str] = Field(None, max_length=500, description="Path to PDF file")
|
||||
path_xml: Optional[str] = Field(None, max_length=500, description="Path to XML file")
|
||||
is_updated: Optional[bool] = Field(None, description="Status")
|
||||
traffic_light_status: Optional[str] = Field(
|
||||
None, max_length=50, description="Traffic light status (SEMAFORO)")
|
||||
process_log: Optional[str] = Field(
|
||||
None, max_length=300, description="Processing log")
|
||||
comments_status: Optional[str] = Field(
|
||||
None, description="Comments and observations")
|
||||
cfdi_uuid: Optional[str] = Field(
|
||||
None, max_length=100, description="CFDI UUID")
|
||||
path_pdf: Optional[str] = Field(
|
||||
None, max_length=500, description="Path to PDF file")
|
||||
path_xml: Optional[str] = Field(
|
||||
None, max_length=500, description="Path to XML file")
|
||||
|
||||
|
||||
class InvoiceComplianceMxBase(BaseModel):
|
||||
@@ -36,8 +47,8 @@ class InvoiceComplianceMxBase(BaseModel):
|
||||
None, max_length=5, description="Customs office")
|
||||
customs_agent: Optional[str] = Field(
|
||||
None, max_length=10, description="Customs agent")
|
||||
is_mixed: Optional[str] = Field(
|
||||
None, max_length=2, description="Is mixed operation")
|
||||
is_mixed: Optional[bool] = Field(
|
||||
None, description="Is mixed operation")
|
||||
waste_type: Optional[str] = Field(
|
||||
None, max_length=1, description="Waste type")
|
||||
appendix_17: Optional[int] = Field(None, description="Appendix 17")
|
||||
|
||||
@@ -72,6 +72,18 @@
|
||||
"customs_sections": "Customs Sections",
|
||||
"anexo_22_app_31": "Anexo 22 App 3"
|
||||
},
|
||||
"import_invoices":{
|
||||
"title": "Import Invoices",
|
||||
"temporary": "Temporary",
|
||||
"definitive": "Definitive",
|
||||
"mexican_purchases": "Mexican Purchases",
|
||||
"regime_change": "Regime Change"
|
||||
},
|
||||
"export_invoices": {
|
||||
"title": "Export Invoices",
|
||||
"exportation": "Exportation",
|
||||
"repair": "Repair"
|
||||
},
|
||||
"clients_and_providers": "Clients and Providers",
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"nav_user": {
|
||||
|
||||
@@ -72,6 +72,18 @@
|
||||
"customs_sections": "Secciones Aduaneras",
|
||||
"anexo_22_app_31": "Anexo 22 App 3"
|
||||
},
|
||||
"import_invoices":{
|
||||
"title": "Facturas de importación",
|
||||
"temporary": "Temporal",
|
||||
"definitive": "Definitiva",
|
||||
"mexican_purchases": "Compras mexicanas",
|
||||
"regime_change": "Cambio de régimen"
|
||||
},
|
||||
"export_invoices": {
|
||||
"title": "Facturas de exportación",
|
||||
"exportation": "Exportación",
|
||||
"repair": "Reparación"
|
||||
},
|
||||
"clients_and_providers": "Clientes y Proveedores",
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"nav_user": {
|
||||
|
||||
318
frontend/src/lib/api/dashboard/a76/invoices.ts
Normal file
318
frontend/src/lib/api/dashboard/a76/invoices.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* API Client para Facturas (Invoices)
|
||||
* Gestiona las operaciones CRUD para facturas y sus relaciones
|
||||
*/
|
||||
import { api } from '$lib/api';
|
||||
|
||||
export type OperationType = 'imp' | 'exp';
|
||||
export type TransportType = 'none' | 'transport' | 'box' | 'licence plates' | 'truck' | 'vessel' | 'rail barge' | 'container' | 'airplane' | 'gondola' | 'flatbed';
|
||||
|
||||
// --- Interfaces ---
|
||||
|
||||
export interface InvoiceComplianceMx {
|
||||
invoice_id?: number;
|
||||
pedimento?: string | null;
|
||||
pedimento_code?: string | null;
|
||||
remesa?: number | null;
|
||||
aduana?: string | null;
|
||||
provider_header?: string | null;
|
||||
provider_id?: string | null;
|
||||
sold_to_header?: string | null;
|
||||
sold_to_id?: string | null;
|
||||
shipped_to_header?: string | null;
|
||||
shipped_to_id?: string | null;
|
||||
shipped_by_header?: string | null;
|
||||
shipped_by_id?: string | null;
|
||||
customs_broker_id?: string | null;
|
||||
is_mixed?: boolean | null;
|
||||
waste_type?: string | null;
|
||||
appendix_17?: number | null;
|
||||
edocument?: string | null;
|
||||
electronic_signature?: string | null;
|
||||
sem_id?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceFinancials {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
currency?: string | null;
|
||||
currency_type?: string | null;
|
||||
exchange_rate?: number | null;
|
||||
value_mn?: number | null;
|
||||
value_me?: number | null;
|
||||
customs_value_mn?: number | null;
|
||||
freight?: number | null;
|
||||
insurance?: number | null;
|
||||
iva_mn?: number | null;
|
||||
iva_factor?: number | null;
|
||||
total_quantity?: number | null;
|
||||
gross_weight?: number | null;
|
||||
net_weight?: number | null;
|
||||
bundle_count?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceLogistics {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
carrier_id?: string | null;
|
||||
transport_type?: TransportType | null;
|
||||
transport_mode?: string | null;
|
||||
driver_name?: string | null;
|
||||
is_rail?: string | null;
|
||||
rail_id?: string | null;
|
||||
vehicle_num?: string | null;
|
||||
license_plate?: string | null;
|
||||
seal_number?: string | null;
|
||||
guide_number?: string | null;
|
||||
entry_exit_date?: string | null;
|
||||
}
|
||||
|
||||
export interface InvoiceSalesDetails {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
line_number: number;
|
||||
sales_order?: string | null;
|
||||
colors_description?: string | null;
|
||||
square_color_code?: string | null;
|
||||
line_bundles?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceCollections {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
concept?: string | null;
|
||||
is_collected?: number | null;
|
||||
collection_date?: string | null;
|
||||
amount?: number | null;
|
||||
collector_user?: string | null;
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
capture_date: string;
|
||||
is_updated?: boolean | null;
|
||||
updated_date?: string | null;
|
||||
who_updated?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: InvoiceComplianceMx | null;
|
||||
financials?: InvoiceFinancials | null;
|
||||
logistics?: InvoiceLogistics[];
|
||||
details?: InvoiceSalesDetails[];
|
||||
collections?: InvoiceCollections[];
|
||||
}
|
||||
|
||||
export interface InvoiceListResponse {
|
||||
items: Invoice[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface CreateInvoiceData {
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: Omit<InvoiceComplianceMx, 'invoice_id'> | null;
|
||||
financials?: Omit<InvoiceFinancials, 'id' | 'invoice_id'> | null;
|
||||
logistics?: Omit<InvoiceLogistics, 'id' | 'invoice_id'>[] | null;
|
||||
details?: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>[] | null;
|
||||
collections?: Omit<InvoiceCollections, 'id' | 'invoice_id'>[] | null;
|
||||
}
|
||||
|
||||
export interface UpdateInvoiceData {
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: Partial<InvoiceComplianceMx> | null;
|
||||
financials?: Partial<InvoiceFinancials> | null;
|
||||
logistics?: Partial<InvoiceLogistics>[] | null;
|
||||
details?: Partial<InvoiceSalesDetails>[] | null;
|
||||
collections?: Partial<InvoiceCollections>[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* API para Facturas
|
||||
*/
|
||||
export const invoicesApi = {
|
||||
/**
|
||||
* Lista todas las facturas con paginación
|
||||
*/
|
||||
list: (companyId: number, page = 1, pageSize = 50, filters?: Record<string, any>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
|
||||
// Agregar filtros si existen
|
||||
if (filters) {
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
params.append(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return api.get<InvoiceListResponse>(`/v1/a76/invoices?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene una factura por ID
|
||||
*/
|
||||
get: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<Invoice>(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Crea una nueva factura
|
||||
*/
|
||||
create: (companyId: number, data: CreateInvoiceData) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<Invoice>(`/v1/a76/invoices?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Actualiza una factura existente
|
||||
*/
|
||||
update: (invoiceId: number, companyId: number, data: UpdateInvoiceData) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.put<Invoice>(`/v1/a76/invoices/${invoiceId}?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Elimina una factura
|
||||
*/
|
||||
delete: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
|
||||
},
|
||||
|
||||
// --- Nested Resources ---
|
||||
|
||||
/**
|
||||
* Logística de factura
|
||||
*/
|
||||
logistics: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceLogistics[]>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceLogistics, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceLogistics>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, logisticsId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/logistics/${logisticsId}?${params.toString()}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Detalles de venta de factura
|
||||
*/
|
||||
details: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceSalesDetails[]>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceSalesDetails>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, detailId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/details/${detailId}?${params.toString()}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cobranzas de factura
|
||||
*/
|
||||
collections: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceCollections[]>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceCollections, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceCollections>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, collectionId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}?${params.toString()}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
89
frontend/src/lib/components/dashboard/invoices/columns.ts
Normal file
89
frontend/src/lib/components/dashboard/invoices/columns.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Definición de columnas para la tabla de facturas
|
||||
*/
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns() {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
cell: (info: any) => info.getValue(),
|
||||
enableSorting: true
|
||||
},
|
||||
{
|
||||
accessorKey: 'operation_type',
|
||||
header: 'Tipo',
|
||||
cell: (info: any) => {
|
||||
const type = info.getValue();
|
||||
return type === 'imp' ? 'Importación' : type === 'exp' ? 'Exportación' : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'invoice_number',
|
||||
header: 'Número de Factura',
|
||||
cell: (info: any) => info.getValue() || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'invoice_type',
|
||||
header: 'Tipo Factura',
|
||||
cell: (info: any) => info.getValue() || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'project_number',
|
||||
header: 'Proyecto',
|
||||
cell: (info: any) => info.getValue() || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'compliance_mx.pedimento',
|
||||
header: 'Pedimento',
|
||||
cell: (info: any) => {
|
||||
const row = info.row.original;
|
||||
return row.compliance_mx?.pedimento || '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'invoice_date',
|
||||
header: 'Fecha Factura',
|
||||
cell: (info: any) => {
|
||||
const date = info.getValue();
|
||||
if (!date) return '-';
|
||||
return new Date(date).toLocaleDateString('es-MX');
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'financials.value_mn',
|
||||
header: 'Valor MN',
|
||||
cell: (info: any) => {
|
||||
const row = info.row.original;
|
||||
const value = row.financials?.value_mn;
|
||||
if (value === null || value === undefined) return '-';
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: 'MXN'
|
||||
}).format(value);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'traffic_light_status',
|
||||
header: 'Semáforo',
|
||||
cell: (info: any) => info.getValue() || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'capture_date',
|
||||
header: 'Fecha Captura',
|
||||
cell: (info: any) => {
|
||||
const date = info.getValue();
|
||||
if (!date) return '-';
|
||||
return new Date(date).toLocaleDateString('es-MX');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: (info: any) => DataTableActions,
|
||||
enableSorting: false
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import { invoicesApi, type Invoice, type CreateInvoiceData, type UpdateInvoiceData } from "$lib/api/dashboard/a76/invoices";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { LoaderCircle } from 'lucide-svelte';
|
||||
import * as Tabs from "$lib/components/ui/tabs";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = $bindable<Invoice | null>(null),
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Invoice | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let formData = $state({
|
||||
// Header fields
|
||||
operation_type: "imp" as "imp" | "exp",
|
||||
invoice_type: "",
|
||||
invoice_number: "",
|
||||
project_number: "",
|
||||
purchase_order: "",
|
||||
related_doc_id: null as number | null,
|
||||
invoice_date: "",
|
||||
traffic_light_status: "",
|
||||
observation_es: "",
|
||||
observation_en: "",
|
||||
comments_status: "",
|
||||
cfdi_uuid: "",
|
||||
path_pdf: "",
|
||||
path_xml: "",
|
||||
// Compliance MX fields
|
||||
pedimento: "",
|
||||
pedimento_code: "",
|
||||
remesa: null as number | null,
|
||||
aduana: "",
|
||||
customs_broker_id: "",
|
||||
provider_id: "",
|
||||
sold_to_id: "",
|
||||
shipped_to_id: "",
|
||||
shipped_by_id: "",
|
||||
is_mixed: false,
|
||||
waste_type: "",
|
||||
appendix_17: null as number | null,
|
||||
edocument: "",
|
||||
// Financials fields
|
||||
currency: "MXN",
|
||||
exchange_rate: null as number | null,
|
||||
value_mn: null as number | null,
|
||||
value_me: null as number | null,
|
||||
customs_value_mn: null as number | null,
|
||||
freight: null as number | null,
|
||||
insurance: null as number | null,
|
||||
iva_mn: null as number | null,
|
||||
iva_factor: null as number | null,
|
||||
total_quantity: null as number | null,
|
||||
gross_weight: null as number | null,
|
||||
net_weight: null as number | null,
|
||||
bundle_count: null as number | null
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Actualizar formData cuando item cambia
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
formData = {
|
||||
operation_type: item.operation_type || "imp",
|
||||
invoice_type: item.invoice_type || "",
|
||||
invoice_number: item.invoice_number || "",
|
||||
project_number: item.project_number || "",
|
||||
purchase_order: item.purchase_order || "",
|
||||
related_doc_id: item.related_doc_id || null,
|
||||
invoice_date: item.invoice_date || "",
|
||||
traffic_light_status: item.traffic_light_status || "",
|
||||
observation_es: item.observation_es || "",
|
||||
observation_en: item.observation_en || "",
|
||||
comments_status: item.comments_status || "",
|
||||
cfdi_uuid: item.cfdi_uuid || "",
|
||||
path_pdf: item.path_pdf || "",
|
||||
path_xml: item.path_xml || "",
|
||||
pedimento: item.compliance_mx?.pedimento || "",
|
||||
pedimento_code: item.compliance_mx?.pedimento_code || "",
|
||||
remesa: item.compliance_mx?.remesa || null,
|
||||
aduana: item.compliance_mx?.aduana || "",
|
||||
customs_broker_id: item.compliance_mx?.customs_broker_id || "",
|
||||
provider_id: item.compliance_mx?.provider_id || "",
|
||||
sold_to_id: item.compliance_mx?.sold_to_id || "",
|
||||
shipped_to_id: item.compliance_mx?.shipped_to_id || "",
|
||||
shipped_by_id: item.compliance_mx?.shipped_by_id || "",
|
||||
is_mixed: item.compliance_mx?.is_mixed || false,
|
||||
waste_type: item.compliance_mx?.waste_type || "",
|
||||
appendix_17: item.compliance_mx?.appendix_17 || null,
|
||||
edocument: item.compliance_mx?.edocument || "",
|
||||
currency: item.financials?.currency || "MXN",
|
||||
exchange_rate: item.financials?.exchange_rate || null,
|
||||
value_mn: item.financials?.value_mn || null,
|
||||
value_me: item.financials?.value_me || null,
|
||||
customs_value_mn: item.financials?.customs_value_mn || null,
|
||||
freight: item.financials?.freight || null,
|
||||
insurance: item.financials?.insurance || null,
|
||||
iva_mn: item.financials?.iva_mn || null,
|
||||
iva_factor: item.financials?.iva_factor || null,
|
||||
total_quantity: item.financials?.total_quantity || null,
|
||||
gross_weight: item.financials?.gross_weight || null,
|
||||
net_weight: item.financials?.net_weight || null,
|
||||
bundle_count: item.financials?.bundle_count || null
|
||||
};
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
});
|
||||
|
||||
const isEditing = $derived(!!item);
|
||||
|
||||
function resetForm() {
|
||||
formData = {
|
||||
operation_type: "imp",
|
||||
invoice_type: "",
|
||||
invoice_number: "",
|
||||
project_number: "",
|
||||
purchase_order: "",
|
||||
related_doc_id: null,
|
||||
invoice_date: "",
|
||||
traffic_light_status: "",
|
||||
observation_es: "",
|
||||
observation_en: "",
|
||||
comments_status: "",
|
||||
cfdi_uuid: "",
|
||||
path_pdf: "",
|
||||
path_xml: "",
|
||||
pedimento: "",
|
||||
pedimento_code: "",
|
||||
remesa: null,
|
||||
aduana: "",
|
||||
customs_broker_id: "",
|
||||
provider_id: "",
|
||||
sold_to_id: "",
|
||||
shipped_to_id: "",
|
||||
shipped_by_id: "",
|
||||
is_mixed: false,
|
||||
waste_type: "",
|
||||
appendix_17: null,
|
||||
edocument: "",
|
||||
currency: "MXN",
|
||||
exchange_rate: null,
|
||||
value_mn: null,
|
||||
value_me: null,
|
||||
customs_value_mn: null,
|
||||
freight: null,
|
||||
insurance: null,
|
||||
iva_mn: null,
|
||||
iva_factor: null,
|
||||
total_quantity: null,
|
||||
gross_weight: null,
|
||||
net_weight: null,
|
||||
bundle_count: null
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
error = "No hay compañía seleccionada";
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (isEditing && item) {
|
||||
const payload: UpdateInvoiceData = {
|
||||
operation_type: formData.operation_type,
|
||||
invoice_type: formData.invoice_type || null,
|
||||
invoice_number: formData.invoice_number || null,
|
||||
project_number: formData.project_number || null,
|
||||
purchase_order: formData.purchase_order || null,
|
||||
related_doc_id: formData.related_doc_id,
|
||||
invoice_date: formData.invoice_date || null,
|
||||
traffic_light_status: formData.traffic_light_status || null,
|
||||
observation_es: formData.observation_es || null,
|
||||
observation_en: formData.observation_en || null,
|
||||
comments_status: formData.comments_status || null,
|
||||
cfdi_uuid: formData.cfdi_uuid || null,
|
||||
path_pdf: formData.path_pdf || null,
|
||||
path_xml: formData.path_xml || null,
|
||||
compliance_mx: {
|
||||
pedimento: formData.pedimento || null,
|
||||
pedimento_code: formData.pedimento_code || null,
|
||||
remesa: formData.remesa,
|
||||
aduana: formData.aduana || null,
|
||||
customs_broker_id: formData.customs_broker_id || null,
|
||||
provider_id: formData.provider_id || null,
|
||||
sold_to_id: formData.sold_to_id || null,
|
||||
shipped_to_id: formData.shipped_to_id || null,
|
||||
shipped_by_id: formData.shipped_by_id || null,
|
||||
is_mixed: formData.is_mixed,
|
||||
waste_type: formData.waste_type || null,
|
||||
appendix_17: formData.appendix_17,
|
||||
edocument: formData.edocument || null
|
||||
},
|
||||
financials: {
|
||||
currency: formData.currency || null,
|
||||
exchange_rate: formData.exchange_rate,
|
||||
value_mn: formData.value_mn,
|
||||
value_me: formData.value_me,
|
||||
customs_value_mn: formData.customs_value_mn,
|
||||
freight: formData.freight,
|
||||
insurance: formData.insurance,
|
||||
iva_mn: formData.iva_mn,
|
||||
iva_factor: formData.iva_factor,
|
||||
total_quantity: formData.total_quantity,
|
||||
gross_weight: formData.gross_weight,
|
||||
net_weight: formData.net_weight,
|
||||
bundle_count: formData.bundle_count
|
||||
}
|
||||
};
|
||||
response = await invoicesApi.update(item.id, companyStore.activeCompany.id, payload);
|
||||
} else {
|
||||
const payload: CreateInvoiceData = {
|
||||
operation_type: formData.operation_type,
|
||||
invoice_type: formData.invoice_type || null,
|
||||
invoice_number: formData.invoice_number || null,
|
||||
project_number: formData.project_number || null,
|
||||
purchase_order: formData.purchase_order || null,
|
||||
related_doc_id: formData.related_doc_id,
|
||||
invoice_date: formData.invoice_date || null,
|
||||
traffic_light_status: formData.traffic_light_status || null,
|
||||
observation_es: formData.observation_es || null,
|
||||
observation_en: formData.observation_en || null,
|
||||
comments_status: formData.comments_status || null,
|
||||
cfdi_uuid: formData.cfdi_uuid || null,
|
||||
path_pdf: formData.path_pdf || null,
|
||||
path_xml: formData.path_xml || null,
|
||||
compliance_mx: {
|
||||
pedimento: formData.pedimento || null,
|
||||
pedimento_code: formData.pedimento_code || null,
|
||||
remesa: formData.remesa,
|
||||
aduana: formData.aduana || null,
|
||||
customs_broker_id: formData.customs_broker_id || null,
|
||||
provider_id: formData.provider_id || null,
|
||||
sold_to_id: formData.sold_to_id || null,
|
||||
shipped_to_id: formData.shipped_to_id || null,
|
||||
shipped_by_id: formData.shipped_by_id || null,
|
||||
is_mixed: formData.is_mixed,
|
||||
waste_type: formData.waste_type || null,
|
||||
appendix_17: formData.appendix_17,
|
||||
edocument: formData.edocument || null
|
||||
},
|
||||
financials: {
|
||||
currency: formData.currency || null,
|
||||
exchange_rate: formData.exchange_rate,
|
||||
value_mn: formData.value_mn,
|
||||
value_me: formData.value_me,
|
||||
customs_value_mn: formData.customs_value_mn,
|
||||
freight: formData.freight,
|
||||
insurance: formData.insurance,
|
||||
iva_mn: formData.iva_mn,
|
||||
iva_factor: formData.iva_factor,
|
||||
total_quantity: formData.total_quantity,
|
||||
gross_weight: formData.gross_weight,
|
||||
net_weight: formData.net_weight,
|
||||
bundle_count: formData.bundle_count
|
||||
}
|
||||
};
|
||||
response = await invoicesApi.create(companyStore.activeCompany.id, payload);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (response.status === 401) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
open = false;
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error al guardar";
|
||||
console.error("Error saving:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
resetForm();
|
||||
error = null;
|
||||
}
|
||||
open = newOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={handleOpenChange}>
|
||||
<Dialog.Content class="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{isEditing ? "Editar Factura" : "Nueva Factura"}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{isEditing
|
||||
? "Modifica los datos de la factura"
|
||||
: "Ingresa los datos de la nueva factura"}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-3">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="compliance">Cumplimiento</Tabs.Trigger>
|
||||
<Tabs.Trigger value="financials">Financieros</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<!-- General Tab -->
|
||||
<Tabs.Content value="general" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="operation_type">Tipo de Operación *</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.operation_type}
|
||||
onValueChange={(v: string) => {
|
||||
if (v) formData.operation_type = v as "imp" | "exp";
|
||||
}}
|
||||
>
|
||||
<Select.Trigger>
|
||||
{formData.operation_type === 'imp' ? 'Importación' : formData.operation_type === 'exp' ? 'Exportación' : 'Seleccionar tipo'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="imp">Importación</Select.Item>
|
||||
<Select.Item value="exp">Exportación</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div> <div class="space-y-2">
|
||||
<Label for="invoice_number">Número de Factura</Label>
|
||||
<Input
|
||||
id="invoice_number"
|
||||
bind:value={formData.invoice_number}
|
||||
placeholder="Número de factura"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="invoice_type">Tipo de Factura</Label>
|
||||
<Input
|
||||
id="invoice_type"
|
||||
bind:value={formData.invoice_type}
|
||||
placeholder="Tipo de factura"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="project_number">Número de Proyecto</Label>
|
||||
<Input
|
||||
id="project_number"
|
||||
bind:value={formData.project_number}
|
||||
placeholder="Número de proyecto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="purchase_order">Orden de Compra</Label>
|
||||
<Input
|
||||
id="purchase_order"
|
||||
bind:value={formData.purchase_order}
|
||||
placeholder="Orden de compra"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="invoice_date">Fecha de Factura</Label>
|
||||
<Input
|
||||
id="invoice_date"
|
||||
type="date"
|
||||
bind:value={formData.invoice_date}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="traffic_light_status">Semáforo</Label>
|
||||
<Input
|
||||
id="traffic_light_status"
|
||||
bind:value={formData.traffic_light_status}
|
||||
placeholder="Estado del semáforo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="cfdi_uuid">CFDI UUID</Label>
|
||||
<Input
|
||||
id="cfdi_uuid"
|
||||
bind:value={formData.cfdi_uuid}
|
||||
placeholder="UUID del CFDI"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="observation_es">Observaciones (Español)</Label>
|
||||
<Input
|
||||
id="observation_es"
|
||||
bind:value={formData.observation_es}
|
||||
placeholder="Observaciones en español"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="observation_en">Observaciones (Inglés)</Label>
|
||||
<Input
|
||||
id="observation_en"
|
||||
bind:value={formData.observation_en}
|
||||
placeholder="Observaciones en inglés"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Compliance Tab -->
|
||||
<Tabs.Content value="compliance" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="pedimento">Pedimento</Label>
|
||||
<Input
|
||||
id="pedimento"
|
||||
bind:value={formData.pedimento}
|
||||
placeholder="Número de pedimento"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="pedimento_code">Código de Pedimento</Label>
|
||||
<Input
|
||||
id="pedimento_code"
|
||||
bind:value={formData.pedimento_code}
|
||||
placeholder="R1, K1, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="remesa">Remesa</Label>
|
||||
<Input
|
||||
id="remesa"
|
||||
type="number"
|
||||
bind:value={formData.remesa}
|
||||
placeholder="Número de remesa"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="aduana">Aduana</Label>
|
||||
<Input
|
||||
id="aduana"
|
||||
bind:value={formData.aduana}
|
||||
placeholder="Código de aduana"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="customs_broker_id">Agente Aduanal</Label>
|
||||
<Input
|
||||
id="customs_broker_id"
|
||||
bind:value={formData.customs_broker_id}
|
||||
placeholder="ID del agente aduanal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="provider_id">Proveedor</Label>
|
||||
<Input
|
||||
id="provider_id"
|
||||
bind:value={formData.provider_id}
|
||||
placeholder="ID del proveedor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="edocument">E-Document</Label>
|
||||
<Input
|
||||
id="edocument"
|
||||
bind:value={formData.edocument}
|
||||
placeholder="Número de e-document"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 flex items-center gap-2 pt-8">
|
||||
<input
|
||||
id="is_mixed"
|
||||
type="checkbox"
|
||||
bind:checked={formData.is_mixed}
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
<Label for="is_mixed" class="!mt-0">Operación Mixta</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Financials Tab -->
|
||||
<Tabs.Content value="financials" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="currency">Moneda</Label>
|
||||
<Input
|
||||
id="currency"
|
||||
bind:value={formData.currency}
|
||||
placeholder="MXN, USD, etc."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="exchange_rate">Tipo de Cambio</Label>
|
||||
<Input
|
||||
id="exchange_rate"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={formData.exchange_rate}
|
||||
placeholder="Tipo de cambio"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="value_mn">Valor MN</Label>
|
||||
<Input
|
||||
id="value_mn"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.value_mn}
|
||||
placeholder="Valor en moneda nacional"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="value_me">Valor ME</Label>
|
||||
<Input
|
||||
id="value_me"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.value_me}
|
||||
placeholder="Valor en moneda extranjera"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="customs_value_mn">Valor Aduana MN</Label>
|
||||
<Input
|
||||
id="customs_value_mn"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.customs_value_mn}
|
||||
placeholder="Valor de aduana en MN"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="freight">Flete</Label>
|
||||
<Input
|
||||
id="freight"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.freight}
|
||||
placeholder="Costo de flete"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="insurance">Seguro</Label>
|
||||
<Input
|
||||
id="insurance"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.insurance}
|
||||
placeholder="Costo de seguro"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="iva_mn">IVA MN</Label>
|
||||
<Input
|
||||
id="iva_mn"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.iva_mn}
|
||||
placeholder="IVA en MN"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="total_quantity">Cantidad Total</Label>
|
||||
<Input
|
||||
id="total_quantity"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.total_quantity}
|
||||
placeholder="Cantidad total"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="gross_weight">Peso Bruto</Label>
|
||||
<Input
|
||||
id="gross_weight"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.gross_weight}
|
||||
placeholder="Peso bruto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="net_weight">Peso Neto</Label>
|
||||
<Input
|
||||
id="net_weight"
|
||||
type="number"
|
||||
step="0.01"
|
||||
bind:value={formData.net_weight}
|
||||
placeholder="Peso neto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="bundle_count">Número de Bultos</Label>
|
||||
<Input
|
||||
id="bundle_count"
|
||||
type="number"
|
||||
bind:value={formData.bundle_count}
|
||||
placeholder="Número de bultos"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
{#if error}
|
||||
<div class="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onclick={() => (open = false)}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
{isEditing ? "Actualizar" : "Crear"}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { Ellipsis, Eye, Pencil, Trash2 } from 'lucide-svelte';
|
||||
|
||||
interface Props {
|
||||
invoice: Invoice;
|
||||
}
|
||||
|
||||
let { invoice }: Props = $props();
|
||||
|
||||
function dispatchView() {
|
||||
window.dispatchEvent(new CustomEvent('invoiceView', { detail: invoice }));
|
||||
}
|
||||
|
||||
function dispatchEdit() {
|
||||
window.dispatchEvent(new CustomEvent('invoiceEdit', { detail: invoice }));
|
||||
}
|
||||
|
||||
function dispatchDelete() {
|
||||
window.dispatchEvent(new CustomEvent('invoiceDelete', { detail: invoice }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button {...props} variant="ghost" size="icon" class="h-8 w-8 p-0">
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<Ellipsis class="h-4 w-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={dispatchView}>
|
||||
<Eye class="mr-2 h-4 w-4" />
|
||||
Ver Detalles
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={dispatchEdit}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item onclick={dispatchDelete} class="text-destructive">
|
||||
<Trash2 class="mr-2 h-4 w-4" />
|
||||
Eliminar
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
131
frontend/src/lib/components/dashboard/invoices/data-table.svelte
Normal file
131
frontend/src/lib/components/dashboard/invoices/data-table.svelte
Normal file
@@ -0,0 +1,131 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel
|
||||
} from "@tanstack/table-core";
|
||||
import { createSvelteTable, FlexRender } from "$lib/components/ui/data-table/index.js";
|
||||
import * as Table from "$lib/components/ui/table/index.js";
|
||||
|
||||
type DataTableProps<TData, TValue> = {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
loading: boolean;
|
||||
hasMore: boolean;
|
||||
loadMore: () => void;
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns,
|
||||
loading,
|
||||
hasMore,
|
||||
loadMore
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
});
|
||||
|
||||
let scrollContainer = $state<HTMLDivElement>();
|
||||
let loadingTrigger = $state<HTMLDivElement>();
|
||||
|
||||
// Intersection Observer para detectar cuando el usuario llega al final
|
||||
onMount(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const [entry] = entries;
|
||||
if (entry.isIntersecting && hasMore && !loading) {
|
||||
loadMore();
|
||||
}
|
||||
},
|
||||
{
|
||||
root: scrollContainer,
|
||||
threshold: 0.1
|
||||
}
|
||||
);
|
||||
|
||||
if (loadingTrigger) {
|
||||
observer.observe(loadingTrigger);
|
||||
}
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class="rounded-md border max-h-[600px] overflow-y-auto" bind:this={scrollContainer}>
|
||||
<Table.Root>
|
||||
<Table.Header class="sticky top-0 bg-background z-10">
|
||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||
<Table.Row>
|
||||
{#each headerGroup.headers as header (header.id)}
|
||||
<Table.Head>
|
||||
{#if !header.isPlaceholder}
|
||||
<FlexRender
|
||||
content={header.column.columnDef.header}
|
||||
context={header.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Head>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
{#if cell.column.id === 'actions'}
|
||||
{@const cellDef = cell.column.columnDef.cell}
|
||||
{#if cellDef && typeof cellDef === 'function'}
|
||||
{@const Component = cellDef(cell.getContext())}
|
||||
<Component invoice={cell.row.original} />
|
||||
{/if}
|
||||
{:else}
|
||||
<FlexRender
|
||||
content={cell.column.columnDef.cell}
|
||||
context={cell.getContext()}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
{/each}
|
||||
</Table.Row>
|
||||
{:else}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-24 text-center">
|
||||
No hay resultados.
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
|
||||
<!-- Loading Trigger - Se activa cuando es visible -->
|
||||
{#if hasMore}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={columns.length} class="h-20 text-center">
|
||||
<div bind:this={loadingTrigger}>
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
<span class="text-muted-foreground text-sm">Cargando más...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-muted-foreground text-sm">
|
||||
Desplázate para cargar más
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,110 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as AlertDialog from "$lib/components/ui/alert-dialog";
|
||||
import { invoicesApi, type Invoice } from "$lib/api/dashboard/a76/invoices";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { LoaderCircle } from 'lucide-svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item: Invoice | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!item || !companyStore.activeCompany) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await invoicesApi.delete(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
error = response.error;
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
open = false;
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error al eliminar";
|
||||
console.error("Error deleting:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
error = null;
|
||||
}
|
||||
open = newOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<AlertDialog.Root bind:open onOpenChange={handleOpenChange}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>¿Estás seguro?</AlertDialog.Title>
|
||||
<AlertDialog.Description class="space-y-2">
|
||||
<p>Esta acción no se puede deshacer. Se eliminará permanentemente esta factura:</p>
|
||||
{#if item}
|
||||
<div class="mt-2 rounded-lg bg-muted p-3 space-y-2">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="font-medium">ID:</span>
|
||||
<span class="font-semibold">{item.id}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="font-medium">Número de Factura:</span>
|
||||
<code class="font-mono font-semibold">{item.invoice_number || 'N/A'}</code>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="font-medium">Tipo:</span>
|
||||
<span class="text-xs">
|
||||
{item.operation_type === 'imp' ? 'Importación' :
|
||||
item.operation_type === 'exp' ? 'Exportación' : 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="font-medium">Proyecto:</span>
|
||||
<span class="text-xs">{item.project_number || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="font-medium">Pedimento:</span>
|
||||
<span class="text-xs">{item.compliance_mx?.pedimento || 'N/A'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if error}
|
||||
<div class="mt-2 rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel disabled={loading}>Cancelar</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
onclick={handleDelete}
|
||||
disabled={loading}
|
||||
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
Eliminar
|
||||
</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
@@ -0,0 +1,435 @@
|
||||
<script lang="ts">
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
invoice
|
||||
}: {
|
||||
open: boolean;
|
||||
invoice: Invoice | null;
|
||||
} = $props();
|
||||
|
||||
function formatDate(dateString: string | null | undefined): string {
|
||||
if (!dateString) return '-';
|
||||
return new Date(dateString).toLocaleDateString('es-MX');
|
||||
}
|
||||
|
||||
function formatCurrency(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return '-';
|
||||
return new Intl.NumberFormat('es-MX', {
|
||||
style: 'currency',
|
||||
currency: 'MXN'
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function formatNumber(value: number | null | undefined): string {
|
||||
if (value === null || value === undefined) return '-';
|
||||
return new Intl.NumberFormat('es-MX').format(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={(v) => (open = v)}>
|
||||
<Dialog.Content class="max-w-5xl max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Detalles de Factura #{invoice?.id}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Información completa de la factura
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if invoice}
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.List class="grid w-full grid-cols-5">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="compliance">Cumplimiento</Tabs.Trigger>
|
||||
<Tabs.Trigger value="financials">Financieros</Tabs.Trigger>
|
||||
<Tabs.Trigger value="logistics">Logística</Tabs.Trigger>
|
||||
<Tabs.Trigger value="details">Detalles</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<!-- General Tab -->
|
||||
<Tabs.Content value="general" class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Tipo de Operación</p>
|
||||
<p class="text-base">
|
||||
{#if invoice.operation_type === 'imp'}
|
||||
<Badge>Importación</Badge>
|
||||
{:else if invoice.operation_type === 'exp'}
|
||||
<Badge variant="secondary">Exportación</Badge>
|
||||
{:else}
|
||||
-
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Factura</p>
|
||||
<p class="text-base">{invoice.invoice_number || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Tipo de Factura</p>
|
||||
<p class="text-base">{invoice.invoice_type || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Proyecto</p>
|
||||
<p class="text-base">{invoice.project_number || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Orden de Compra</p>
|
||||
<p class="text-base">{invoice.purchase_order || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Fecha de Factura</p>
|
||||
<p class="text-base">{formatDate(invoice.invoice_date)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Fecha de Captura</p>
|
||||
<p class="text-base">{formatDate(invoice.capture_date)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Semáforo</p>
|
||||
<p class="text-base">{invoice.traffic_light_status || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">CFDI UUID</p>
|
||||
<p class="text-xs break-all">{invoice.cfdi_uuid || '-'}</p>
|
||||
</div> <div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Actualizado</p>
|
||||
<p class="text-base">{invoice.is_updated ? 'Sí' : 'No'}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">Observaciones (ES)</p>
|
||||
<p class="text-base">{invoice.observation_es || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">Observaciones (EN)</p>
|
||||
<p class="text-base">{invoice.observation_en || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">Log de Proceso</p>
|
||||
<p class="text-base">{invoice.process_log || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Compliance Tab -->
|
||||
<Tabs.Content value="compliance" class="space-y-4">
|
||||
{#if invoice.compliance_mx}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Pedimento</p>
|
||||
<p class="text-base font-semibold">{invoice.compliance_mx.pedimento || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Código de Pedimento</p>
|
||||
<p class="text-base">{invoice.compliance_mx.pedimento_code || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Remesa</p>
|
||||
<p class="text-base">{invoice.compliance_mx.remesa || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Aduana</p>
|
||||
<p class="text-base">{invoice.compliance_mx.aduana || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Agente Aduanal ID</p>
|
||||
<p class="text-base">{invoice.compliance_mx.customs_broker_id || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Proveedor</p>
|
||||
<p class="text-base">{invoice.compliance_mx.provider_id || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Vendido A</p>
|
||||
<p class="text-base">{invoice.compliance_mx.sold_to_id || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Enviado A</p>
|
||||
<p class="text-base">{invoice.compliance_mx.shipped_to_id || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Enviado Por</p>
|
||||
<p class="text-base">{invoice.compliance_mx.shipped_by_id || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Operación Mixta</p>
|
||||
<p class="text-base">{invoice.compliance_mx.is_mixed ? 'Sí' : 'No'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Tipo de Desperdicio</p>
|
||||
<p class="text-base">{invoice.compliance_mx.waste_type || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Apéndice 17</p>
|
||||
<p class="text-base">{invoice.compliance_mx.appendix_17 || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">E-Document</p>
|
||||
<p class="text-base">{invoice.compliance_mx.edocument || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2">
|
||||
<p class="text-sm font-medium text-muted-foreground">Firma Electrónica</p>
|
||||
<p class="text-xs break-all">{invoice.compliance_mx.electronic_signature || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">No hay información de cumplimiento disponible.</p>
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Financials Tab -->
|
||||
<Tabs.Content value="financials" class="space-y-4">
|
||||
{#if invoice.financials}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Moneda</p>
|
||||
<p class="text-base">{invoice.financials.currency || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Tipo de Cambio</p>
|
||||
<p class="text-base">{formatNumber(invoice.financials.exchange_rate)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Valor MN</p>
|
||||
<p class="text-base font-semibold">{formatCurrency(invoice.financials.value_mn)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Valor ME</p>
|
||||
<p class="text-base font-semibold">{formatNumber(invoice.financials.value_me)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Valor Aduana MN</p>
|
||||
<p class="text-base">{formatCurrency(invoice.financials.customs_value_mn)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Flete</p>
|
||||
<p class="text-base">{formatCurrency(invoice.financials.freight)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Seguro</p>
|
||||
<p class="text-base">{formatCurrency(invoice.financials.insurance)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">IVA MN</p>
|
||||
<p class="text-base">{formatCurrency(invoice.financials.iva_mn)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Factor IVA</p>
|
||||
<p class="text-base">{formatNumber(invoice.financials.iva_factor)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Cantidad Total</p>
|
||||
<p class="text-base">{formatNumber(invoice.financials.total_quantity)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Peso Bruto</p>
|
||||
<p class="text-base">{formatNumber(invoice.financials.gross_weight)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Peso Neto</p>
|
||||
<p class="text-base">{formatNumber(invoice.financials.net_weight)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Bultos</p>
|
||||
<p class="text-base">{invoice.financials.bundle_count || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">No hay información financiera disponible.</p>
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Logistics Tab -->
|
||||
<Tabs.Content value="logistics" class="space-y-4">
|
||||
{#if invoice.logistics && invoice.logistics.length > 0}
|
||||
<div class="space-y-6">
|
||||
{#each invoice.logistics as logistics, index}
|
||||
<div class="border rounded-lg p-4">
|
||||
<h4 class="font-semibold mb-3">Logística #{index + 1}</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Transportista</p>
|
||||
<p class="text-base">{logistics.carrier_id || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Tipo de Transporte</p>
|
||||
<p class="text-base">{logistics.transport_type || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Modo de Transporte</p>
|
||||
<p class="text-base">{logistics.transport_mode || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Conductor</p>
|
||||
<p class="text-base">{logistics.driver_name || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Vehículo</p>
|
||||
<p class="text-base">{logistics.vehicle_num || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Placa</p>
|
||||
<p class="text-base">{logistics.license_plate || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Sello</p>
|
||||
<p class="text-base">{logistics.seal_number || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Guía</p>
|
||||
<p class="text-base">{logistics.guide_number || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Fecha Entrada/Salida</p>
|
||||
<p class="text-base">{formatDate(logistics.entry_exit_date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">No hay información de logística disponible.</p>
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Details Tab -->
|
||||
<Tabs.Content value="details" class="space-y-4">
|
||||
{#if invoice.details && invoice.details.length > 0}
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3">Detalles de Venta</h4>
|
||||
<div class="space-y-3">
|
||||
{#each invoice.details as detail}
|
||||
<div class="border rounded-lg p-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Línea</p>
|
||||
<p class="text-base">{detail.line_number}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Orden de Venta</p>
|
||||
<p class="text-base">{detail.sales_order || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Descripción de Colores</p>
|
||||
<p class="text-base">{detail.colors_description || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Código de Color</p>
|
||||
<p class="text-base">{detail.square_color_code || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Bultos</p>
|
||||
<p class="text-base">{detail.line_bundles || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if invoice.collections && invoice.collections.length > 0}
|
||||
<div>
|
||||
<h4 class="font-semibold mb-3">Cobranzas</h4>
|
||||
<div class="space-y-3">
|
||||
{#each invoice.collections as collection}
|
||||
<div class="border rounded-lg p-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Concepto</p>
|
||||
<p class="text-base">{collection.concept || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Monto</p>
|
||||
<p class="text-base">{formatCurrency(collection.amount)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Fecha de Cobranza</p>
|
||||
<p class="text-base">{formatDate(collection.collection_date)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Cobrado</p>
|
||||
<p class="text-base">{collection.is_collected ? 'Sí' : 'No'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Cobrador</p>
|
||||
<p class="text-base">{collection.collector_user || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">No hay detalles de venta o cobranzas disponibles.</p>
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
{/if}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => (open = false)}>
|
||||
Cerrar
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,4 +1,6 @@
|
||||
import {
|
||||
ArrowDownToLine,
|
||||
ArrowUpFromLine,
|
||||
BadgeCheck,
|
||||
ChartPie,
|
||||
Database,
|
||||
@@ -314,6 +316,44 @@ export function getSidebarData(): SidebarData {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: m["sidebar.import_invoices.title"](),
|
||||
url: "#",
|
||||
icon: ArrowDownToLine,
|
||||
items: [
|
||||
{
|
||||
title: m["sidebar.import_invoices.temporary"](),
|
||||
url: "/dashboard/invoices",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.import_invoices.definitive"](),
|
||||
url: "/dashboard/invoices",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.import_invoices.mexican_purchases"](),
|
||||
url: "/dashboard/invoices",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.import_invoices.regime_change"](),
|
||||
url: "/dashboard/invoices",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
title: m["sidebar.export_invoices.title"](),
|
||||
url: "#",
|
||||
icon: ArrowUpFromLine,
|
||||
items: [
|
||||
{
|
||||
title: m["sidebar.export_invoices.exportation"](),
|
||||
url: "/dashboard/invoices",
|
||||
},
|
||||
{
|
||||
title: m["sidebar.export_invoices.repair"](),
|
||||
url: "/dashboard/invoices",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: m["sidebar.clients_and_providers"](),
|
||||
url: "/dashboard/clients_and_providers",
|
||||
|
||||
104
frontend/src/routes/dashboard/invoices/+page.server.ts
Normal file
104
frontend/src/routes/dashboard/invoices/+page.server.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import {
|
||||
getAuthTokens,
|
||||
authenticatedFetch
|
||||
} from '$lib/server/api';
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
|
||||
// Esperar a que el layout padre valide/refresque el token
|
||||
const parentData = await parent();
|
||||
|
||||
// Verificar autenticación
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
if (!accessToken) {
|
||||
throw redirect(302, '/login');
|
||||
}
|
||||
|
||||
try {
|
||||
// Obtener company_id de múltiples fuentes (en orden de prioridad):
|
||||
// 1. URL query param (permite cambiar vía navegación)
|
||||
// 2. Cookie active_company_id (setted por el team-switcher)
|
||||
// 3. Primera compañía del usuario (fallback)
|
||||
const companyIdParam = url.searchParams.get('company_id');
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
|
||||
const companyId = companyIdParam
|
||||
? parseInt(companyIdParam)
|
||||
: cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
|
||||
// Si aún no hay companyId, mostrar error
|
||||
if (!companyId) {
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
companies: parentData.companies || []
|
||||
};
|
||||
}
|
||||
|
||||
// Obtener filtro de tipo de operación
|
||||
const operationType = url.searchParams.get('operation_type');
|
||||
|
||||
// Construir parámetros de consulta
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
page: '1',
|
||||
page_size: '50'
|
||||
});
|
||||
|
||||
// Agregar filtro de tipo si existe y no es 'all'
|
||||
if (operationType && operationType !== 'all') {
|
||||
params.append('operation_type', operationType);
|
||||
}
|
||||
|
||||
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
|
||||
const response = await authenticatedFetch(
|
||||
`v1/a76/invoices?${params.toString()}`,
|
||||
{},
|
||||
cookies,
|
||||
fetch,
|
||||
'/login'
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
error: 'Error al cargar facturas',
|
||||
companies: parentData.companies || [],
|
||||
currentCompanyId: companyId,
|
||||
operationType: operationType || 'all'
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
items: data.items || [],
|
||||
total: data.total || 0,
|
||||
page: data.page || 1,
|
||||
page_size: data.page_size || 50,
|
||||
companies: parentData.companies || [],
|
||||
currentCompanyId: companyId,
|
||||
operationType: operationType || 'all'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading invoices:', error);
|
||||
return {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
error: 'Error al cargar facturas',
|
||||
companies: parentData.companies || []
|
||||
};
|
||||
}
|
||||
};
|
||||
283
frontend/src/routes/dashboard/invoices/+page.svelte
Normal file
283
frontend/src/routes/dashboard/invoices/+page.svelte
Normal file
@@ -0,0 +1,283 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import DataTable from '$lib/components/dashboard/invoices/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/invoices/columns.js';
|
||||
import CreateEditDialog from '$lib/components/dashboard/invoices/create-edit-dialog.svelte';
|
||||
import DetailsDialog from '$lib/components/dashboard/invoices/details-dialog.svelte';
|
||||
import DeleteDialog from '$lib/components/dashboard/invoices/delete-dialog.svelte';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { Plus, RefreshCw } from 'lucide-svelte';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
|
||||
// Recibir data del servidor
|
||||
interface PageData {
|
||||
items: Invoice[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
error?: string;
|
||||
companies: any[];
|
||||
currentCompanyId?: number;
|
||||
operationType?: string;
|
||||
}
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Estado para los diálogos
|
||||
let showCreateDialog = $state(false);
|
||||
let showDetailsDialog = $state(false);
|
||||
let showDeleteDialog = $state(false);
|
||||
let selectedInvoice = $state<Invoice | null>(null);
|
||||
|
||||
// Estado para el filtro de tipo (inicializado desde data del servidor)
|
||||
let selectedType = $state<string>(data.operationType || 'all');
|
||||
|
||||
// Actualizar URL cuando cambia el filtro
|
||||
function handleTypeChange(value: string) {
|
||||
selectedType = value;
|
||||
const url = new URL(window.location.href);
|
||||
if (value === 'all') {
|
||||
url.searchParams.delete('operation_type');
|
||||
} else {
|
||||
url.searchParams.set('operation_type', value);
|
||||
}
|
||||
goto(url.toString(), { keepFocus: true, noScroll: true });
|
||||
}
|
||||
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
|
||||
// Escuchar cambios de compañía
|
||||
const handleCompanyChange = (event: CustomEvent) => {
|
||||
invalidate('app:invoices');
|
||||
};
|
||||
|
||||
// Escuchar eventos de facturas
|
||||
const handleInvoiceView = (event: CustomEvent<Invoice>) => {
|
||||
handleView(event.detail);
|
||||
};
|
||||
|
||||
const handleInvoiceEdit = (event: CustomEvent<Invoice>) => {
|
||||
handleEdit(event.detail);
|
||||
};
|
||||
|
||||
const handleInvoiceDelete = (event: CustomEvent<Invoice>) => {
|
||||
handleDelete(event.detail);
|
||||
};
|
||||
|
||||
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
window.addEventListener('invoiceView', handleInvoiceView as EventListener);
|
||||
window.addEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
|
||||
window.addEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
window.removeEventListener('invoiceView', handleInvoiceView as EventListener);
|
||||
window.removeEventListener('invoiceEdit', handleInvoiceEdit as EventListener);
|
||||
window.removeEventListener('invoiceDelete', handleInvoiceDelete as EventListener);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Estado para infinite scroll - inicializar con data del servidor
|
||||
let allItems = $state<Invoice[]>(data.items || []);
|
||||
let currentPage = $state(data.page || 1);
|
||||
let pageSize = $state(data.page_size || 50);
|
||||
let totalItems = $state(data.total || 0);
|
||||
let loading = $state(false);
|
||||
let hasMore = $derived(allItems.length < totalItems);
|
||||
let error = $state<string | null>(data.error || null);
|
||||
|
||||
// Actualizar datos cuando cambia data del servidor
|
||||
$effect(() => {
|
||||
allItems = data.items || [];
|
||||
currentPage = data.page || 1;
|
||||
totalItems = data.total || 0;
|
||||
error = data.error || null;
|
||||
});
|
||||
|
||||
async function loadMore() {
|
||||
if (loading || !hasMore || !companyStore.activeCompany) return;
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const response = await invoicesApi.list(
|
||||
companyStore.activeCompany.id,
|
||||
currentPage + 1,
|
||||
pageSize,
|
||||
selectedType !== 'all' ? { operation_type: selectedType } : undefined
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
console.error('Error en loadMore:', response.error, 'Status:', response.status);
|
||||
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data?.items) {
|
||||
allItems = [...allItems, ...response.data.items];
|
||||
currentPage++;
|
||||
totalItems = response.data.total;
|
||||
}
|
||||
} catch (e) {
|
||||
error = 'Error cargando más datos';
|
||||
console.error('Error loading more:', e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
// Invalidar datos para que el servidor recargue
|
||||
await invalidate('app:invoices');
|
||||
}
|
||||
|
||||
function handleCreateClick() {
|
||||
selectedInvoice = null;
|
||||
showCreateDialog = true;
|
||||
}
|
||||
|
||||
function handleView(invoice: Invoice) {
|
||||
selectedInvoice = invoice;
|
||||
showDetailsDialog = true;
|
||||
}
|
||||
|
||||
function handleEdit(invoice: Invoice) {
|
||||
selectedInvoice = invoice;
|
||||
showCreateDialog = true;
|
||||
}
|
||||
|
||||
function handleDelete(invoice: Invoice) {
|
||||
selectedInvoice = invoice;
|
||||
showDeleteDialog = true;
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas
|
||||
const columns = createColumns();
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona las facturas de importación y exportación
|
||||
</p>
|
||||
</div>
|
||||
<Button onclick={handleCreateClick}>
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nueva Factura
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if error}
|
||||
<Card.Root class="border-destructive">
|
||||
<Card.Header>
|
||||
<Card.Title class="text-destructive">Error</Card.Title>
|
||||
<Card.Description>{error}</Card.Description>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
{/if}
|
||||
|
||||
<!-- Data Table -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>Listado de Facturas</Card.Title>
|
||||
<Card.Description>
|
||||
Mostrando {allItems.length} de {totalItems} registros
|
||||
{#if companyStore.activeCompany}
|
||||
- Compañía: {companyStore.activeCompany.name}
|
||||
{/if}
|
||||
</Card.Description>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Select.Root type="single" value={selectedType} onValueChange={handleTypeChange}>
|
||||
<Select.Trigger class="w-[180px]">
|
||||
{selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="all">Todas</Select.Item>
|
||||
<Select.Item value="imp">Importación</Select.Item>
|
||||
<Select.Item value="exp">Exportación</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<Button variant="outline" onclick={reloadData}>
|
||||
<RefreshCw class="mr-2" size={16} />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<DataTable
|
||||
data={allItems}
|
||||
{columns}
|
||||
{loading}
|
||||
{hasMore}
|
||||
{loadMore}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<!-- Diálogos -->
|
||||
<CreateEditDialog
|
||||
bind:open={showCreateDialog}
|
||||
bind:item={selectedInvoice}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
|
||||
<DetailsDialog
|
||||
bind:open={showDetailsDialog}
|
||||
invoice={selectedInvoice}
|
||||
/>
|
||||
|
||||
<DeleteDialog
|
||||
bind:open={showDeleteDialog}
|
||||
item={selectedInvoice}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
Reference in New Issue
Block a user