Merge pull request 'feature/funcionalidades-click-derecho-facturas' (#451) from feature/funcionalidades-click-derecho-facturas into development
Reviewed-on: ADUANASOFT/anexo76#451
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
from typing import Dict, Any, Literal, Optional
|
||||
|
||||
from core.config import settings
|
||||
from core.database import get_core_db
|
||||
@@ -255,6 +255,252 @@ def delete_invoice(
|
||||
success = services.InvoiceService.delete(db, invoice_id, tenant_id, company_id)
|
||||
return {"success": success}
|
||||
|
||||
@router.post("/invoices/{invoice_id}/copy", response_model=schemas.InvoiceHeaderResponse)
|
||||
def copy_invoice(
|
||||
invoice_id: int = Path(..., description="ID de la factura a copiar"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
original = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not original:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
|
||||
perm_base = get_invoice_permission_base(original.operation_type, original.invoice_type)
|
||||
validate_access_to_resource(
|
||||
db, company_id, current_user, required_permissions=[f"{perm_base}.create"]
|
||||
)
|
||||
|
||||
try:
|
||||
return services.InvoiceService.copy(db, invoice_id, tenant_id, company_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("copy_invoice failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al copiar factura: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/invoices/{invoice_id}/copy-header", response_model=schemas.InvoiceHeaderResponse)
|
||||
def copy_invoice_header(
|
||||
invoice_id: int = Path(..., description="ID de la factura"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
original = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not original:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(original.operation_type, original.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.create"])
|
||||
try:
|
||||
return services.InvoiceService.copy_header_only(db, invoice_id, tenant_id, company_id)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("copy_invoice_header failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al copiar encabezado: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/export")
|
||||
def export_invoice(
|
||||
invoice_id: int = Path(..., description="ID de la factura"),
|
||||
format: Literal['csv', 'xlsx', 'txt'] = Query(..., description="Formato: csv | xlsx | txt"),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_items(db, invoice, format)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("export_invoice failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al exportar factura: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/interfaces/gm-transport")
|
||||
def interface_gm_transport(
|
||||
invoice_id: int = Path(..., description="ID de la factura"),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_gm_transport(db, invoice)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("interface_gm_transport failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al generar interfaz GM Transport: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/interfaces/carta-porte")
|
||||
def interface_carta_porte(
|
||||
invoice_id: int = Path(..., description="ID de la factura"),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_carta_porte(db, invoice)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("interface_carta_porte failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al generar interfaz Carta Porte: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/interfaces/tfc")
|
||||
def interface_tfc(
|
||||
invoice_id: int = Path(..., description="ID de la factura"),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_tfc(db, invoice)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("interface_tfc failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al generar interfaz TFC: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/interfaces/carta-porte-consolidada")
|
||||
def interface_carta_porte_consolidada(
|
||||
invoice_id: int = Path(..., description="ID de la factura"),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_carta_porte_consolidada(db, invoice)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("interface_carta_porte_consolidada failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al generar interfaz Carta Porte Consolidada: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/interfaces/aviso-cruce")
|
||||
def interface_aviso_cruce(
|
||||
invoice_id: int = Path(..., description="ID de la factura"),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_aviso_cruce(db, invoice)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("interface_aviso_cruce failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al generar interfaz Aviso Cruce: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/interfaces/aaduanal-rs")
|
||||
def interface_aaduanal_rs(
|
||||
invoice_id: int = Path(..., description="ID de la factura"),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_aaduanal_rs(db, invoice)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("interface_aaduanal_rs failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al generar interfaz AAduanal_RS: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/interfaces/caaarem")
|
||||
def interface_caaarem(
|
||||
invoice_id: int = Path(...),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_caaarem(db, invoice)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("interface_caaarem failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al generar interfaz CAAAREM: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/invoices/{invoice_id}/interfaces/cp-genesis")
|
||||
def interface_cp_genesis(
|
||||
invoice_id: int = Path(...),
|
||||
company_id: int = Query(...),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
invoice = services.InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Factura no encontrada")
|
||||
perm_base = get_invoice_permission_base(invoice.operation_type, invoice.invoice_type)
|
||||
validate_access_to_resource(db, company_id, current_user, required_permissions=[f"{perm_base}.view"])
|
||||
try:
|
||||
return services.InvoiceService.export_cp_genesis(db, invoice)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception("interface_cp_genesis failed: %s", e)
|
||||
raise HTTPException(status_code=500, detail=f"Error al generar interfaz Carta Porte Genesis: {str(e)}")
|
||||
|
||||
|
||||
# --- RUTA DE LISTADO (FILTROS) ---
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@ pylint==4.0.2
|
||||
# reportes
|
||||
Jinja2==3.1.6
|
||||
pdfkit==1.0.0
|
||||
openpyxl==3.1.5
|
||||
|
||||
# Desarrollo en seguno plano
|
||||
celery==5.3.6
|
||||
|
||||
@@ -596,5 +596,50 @@ export const invoicesApi = {
|
||||
return api.getBlob(
|
||||
`/v1/a76/factura-cove/invoices/${invoiceId}/cove/acuse?${params.toString()}`
|
||||
);
|
||||
},
|
||||
|
||||
copyInvoice: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.post<Invoice>(`/v1/a76/invoices/${invoiceId}/copy?${params.toString()}`, {});
|
||||
},
|
||||
copyInvoiceHeader: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.post<Invoice>(`/v1/a76/invoices/${invoiceId}/copy-header?${params.toString()}`, {});
|
||||
},
|
||||
exportItems: (invoiceId: number, companyId: number, format: 'csv' | 'xlsx' | 'txt'): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString(), format });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/export?${params.toString()}`);
|
||||
},
|
||||
downloadGmTransport: (invoiceId: number, companyId: number): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/interfaces/gm-transport?${params.toString()}`);
|
||||
},
|
||||
downloadCartaPorte: (invoiceId: number, companyId: number): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/interfaces/carta-porte?${params.toString()}`);
|
||||
},
|
||||
downloadTfc: (invoiceId: number, companyId: number): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/interfaces/tfc?${params.toString()}`);
|
||||
},
|
||||
downloadCartaPorteConsolidada: (invoiceId: number, companyId: number): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/interfaces/carta-porte-consolidada?${params.toString()}`);
|
||||
},
|
||||
downloadAvisoCruce: (invoiceId: number, companyId: number): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/interfaces/aviso-cruce?${params.toString()}`);
|
||||
},
|
||||
downloadAaduanalRs: (invoiceId: number, companyId: number): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/interfaces/aaduanal-rs?${params.toString()}`);
|
||||
},
|
||||
downloadCaaarem: (invoiceId: number, companyId: number): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/interfaces/caaarem?${params.toString()}`);
|
||||
},
|
||||
downloadCpGenesis: (invoiceId: number, companyId: number): Promise<Blob> => {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.getBlob(`/v1/a76/invoices/${invoiceId}/interfaces/cp-genesis?${params.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
// Props para selección
|
||||
selectedIds?: number[];
|
||||
onRowClick?: (row: TData) => void;
|
||||
onContextMenu?: (event: MouseEvent, row: TData) => void;
|
||||
compact?: boolean;
|
||||
sorting?: import("@tanstack/table-core").SortingState;
|
||||
onSortingChange?: (sorting: import("@tanstack/table-core").SortingState) => void;
|
||||
@@ -27,6 +28,7 @@
|
||||
loadMore,
|
||||
selectedIds = [],
|
||||
onRowClick,
|
||||
onContextMenu,
|
||||
compact = false,
|
||||
sorting = [],
|
||||
onSortingChange
|
||||
@@ -211,6 +213,7 @@
|
||||
class="group/inv-list cursor-pointer {row.getIsSelected() ? 'catalog-table-row-selected' : 'catalog-table-row'}"
|
||||
onclick={() => onRowClick && onRowClick(row.original)}
|
||||
ondblclick={() => handleRowDoubleClick(row)}
|
||||
oncontextmenu={(e) => { e.preventDefault(); onContextMenu?.(e, row.original); }}
|
||||
>
|
||||
{#each visibleCells as cell (cell.id)}
|
||||
{@const colId = cell.column.id}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
portalProps,
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.ContentProps & {
|
||||
portalProps?: ContextMenuPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Portal {...portalProps}>
|
||||
<ContextMenuPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="context-menu-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 max-h-(--bits-context-menu-content-available-height) origin-(--bits-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border p-1 shadow-md outline-none",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: ContextMenuPrimitive.GroupProps = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Group data-slot="context-menu-group" {...restProps} />
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.ItemProps & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Item
|
||||
bind:ref
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
class={cn(
|
||||
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/10 dark:data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground outline-hidden relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.GroupHeadingProps & { inset?: boolean } = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.GroupHeading
|
||||
bind:ref
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
class={cn("text-muted-foreground px-2 py-1.5 text-xs font-medium data-[inset]:pl-8", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: ContextMenuPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Root {...restProps} />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SeparatorProps = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Separator
|
||||
bind:ref
|
||||
data-slot="context-menu-separator"
|
||||
class={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SubContentProps = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.SubContent
|
||||
bind:ref
|
||||
data-slot="context-menu-sub-content"
|
||||
class={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--bits-context-menu-content-transform-origin) z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { cn } from "$lib/utils.js";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import ChevronRightIcon from "@lucide/svelte/icons/chevron-right";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
inset,
|
||||
children,
|
||||
...restProps
|
||||
}: ContextMenuPrimitive.SubTriggerProps & { inset?: boolean } = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
bind:ref
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
class={cn(
|
||||
"data-highlighted:bg-accent data-highlighted:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground outline-hidden [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm data-[disabled]:pointer-events-none data-[inset]:pl-8 data-[disabled]:opacity-50 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
<ChevronRightIcon class="ml-auto size-4" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: ContextMenuPrimitive.SubProps = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Sub {...restProps} />
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
|
||||
let { ...restProps }: ContextMenuPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...restProps} />
|
||||
31
frontend/src/lib/components/ui/context-menu/index.ts
Normal file
31
frontend/src/lib/components/ui/context-menu/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||
import Content from "./context-menu-content.svelte";
|
||||
import Group from "./context-menu-group.svelte";
|
||||
import Item from "./context-menu-item.svelte";
|
||||
import Label from "./context-menu-label.svelte";
|
||||
import Separator from "./context-menu-separator.svelte";
|
||||
import Sub from "./context-menu-sub.svelte";
|
||||
import SubContent from "./context-menu-sub-content.svelte";
|
||||
import SubTrigger from "./context-menu-sub-trigger.svelte";
|
||||
import Trigger from "./context-menu-trigger.svelte";
|
||||
import Root from "./context-menu-root.svelte";
|
||||
|
||||
const CheckboxItem = ContextMenuPrimitive.CheckboxItem;
|
||||
const RadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
const RadioItem = ContextMenuPrimitive.RadioItem;
|
||||
|
||||
export {
|
||||
Root,
|
||||
Trigger,
|
||||
Content,
|
||||
Item,
|
||||
Label,
|
||||
Separator,
|
||||
Group,
|
||||
Sub,
|
||||
SubTrigger,
|
||||
SubContent,
|
||||
CheckboxItem,
|
||||
RadioGroup,
|
||||
RadioItem,
|
||||
};
|
||||
@@ -20,6 +20,7 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { getAccessTokenFromDocument } from '$lib/access-token-cookie-browser';
|
||||
@@ -54,7 +55,14 @@
|
||||
ArrowRightLeft,
|
||||
Database,
|
||||
ChevronUp,
|
||||
Mail
|
||||
Mail,
|
||||
Copy,
|
||||
Clipboard,
|
||||
FileOutput,
|
||||
Truck,
|
||||
Route,
|
||||
FileSpreadsheet,
|
||||
Building2
|
||||
} from 'lucide-svelte';
|
||||
import DetailsDialog from '$lib/components/dashboard/invoices/details-dialog.svelte';
|
||||
import DeleteDialog from '$lib/components/dashboard/invoices/delete-dialog.svelte';
|
||||
@@ -95,6 +103,9 @@
|
||||
let isDownloadModalOpen = $state(false);
|
||||
let isTransferenciaModalOpen = $state(false);
|
||||
let isRevertConfirmOpen = $state(false);
|
||||
let exportItemsDialogOpen = $state(false);
|
||||
let exportItemsInvoice = $state<Invoice | null>(null);
|
||||
let exportItemsFormat = $state<'csv' | 'xlsx' | 'txt'>('csv');
|
||||
|
||||
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
|
||||
$effect(() => {
|
||||
@@ -241,17 +252,27 @@
|
||||
|
||||
// Estado para selección de filas (múltiple)
|
||||
let selectedInvoiceIds = $state<number[]>([]);
|
||||
|
||||
// Estado para menú contextual (clic derecho en fila)
|
||||
let contextMenuOpen = $state(false);
|
||||
let contextMenuPosition = $state({ x: 0, y: 0 });
|
||||
let contextMenuInvoice = $state<Invoice | null>(null);
|
||||
let contextMenuEl = $state<HTMLDivElement | null>(null);
|
||||
// Estado para los diálogos de acciones
|
||||
let showDetailsDialog = $state(false);
|
||||
let showDeleteDialog = $state(false);
|
||||
let reportesMenuOpen = $state(false);
|
||||
let masAccionesMenuOpen = $state(false);
|
||||
let copiasInterfacesMenuOpen = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (reportesMenuOpen) masAccionesMenuOpen = false;
|
||||
if (reportesMenuOpen) { masAccionesMenuOpen = false; copiasInterfacesMenuOpen = false; }
|
||||
});
|
||||
$effect(() => {
|
||||
if (masAccionesMenuOpen) reportesMenuOpen = false;
|
||||
if (masAccionesMenuOpen) { reportesMenuOpen = false; copiasInterfacesMenuOpen = false; }
|
||||
});
|
||||
$effect(() => {
|
||||
if (copiasInterfacesMenuOpen) { reportesMenuOpen = false; masAccionesMenuOpen = false; }
|
||||
});
|
||||
|
||||
function handleRowClick(invoice: Invoice) {
|
||||
@@ -265,6 +286,72 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRowContextMenu(event: MouseEvent, invoice: Invoice) {
|
||||
if (!selectedInvoiceIds.includes(invoice.id)) {
|
||||
selectedInvoiceIds = [invoice.id];
|
||||
}
|
||||
contextMenuInvoice = invoice;
|
||||
contextMenuPosition = { x: event.clientX, y: event.clientY };
|
||||
contextMenuOpen = true;
|
||||
await tick();
|
||||
if (contextMenuEl) {
|
||||
const { innerWidth, innerHeight } = window;
|
||||
const rect = contextMenuEl.getBoundingClientRect();
|
||||
let { x, y } = contextMenuPosition;
|
||||
if (x + rect.width > innerWidth) x = innerWidth - rect.width - 8;
|
||||
if (y + rect.height > innerHeight) y = innerHeight - rect.height - 8;
|
||||
if (x < 8) x = 8;
|
||||
if (y < 8) y = 8;
|
||||
contextMenuPosition = { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyToClipboard(invoice: Invoice, fromContextMenu = true) {
|
||||
const parts = [
|
||||
invoice.invoice_number,
|
||||
invoice.invoice_date,
|
||||
invoice.operation_type?.toUpperCase(),
|
||||
invoice.compliance_mx?.pedimento?.pedimento_number
|
||||
].filter(Boolean);
|
||||
await navigator.clipboard.writeText(parts.join(' | '));
|
||||
toast.success('Factura copiada al portapapeles');
|
||||
if (fromContextMenu) contextMenuOpen = false;
|
||||
}
|
||||
|
||||
function openExportItemsDialog(inv: Invoice) {
|
||||
exportItemsInvoice = inv;
|
||||
exportItemsFormat = 'csv';
|
||||
exportItemsDialogOpen = true;
|
||||
}
|
||||
|
||||
function handleExportItemsConfirm() {
|
||||
if (!exportItemsInvoice) return;
|
||||
const inv = exportItemsInvoice;
|
||||
const ext = exportItemsFormat;
|
||||
downloadBlob(
|
||||
invoicesApi.exportItems(inv.id, companyStore.activeCompany!.id, ext),
|
||||
`${inv.invoice_number ?? 'factura'}_export.${ext}`
|
||||
);
|
||||
exportItemsDialogOpen = false;
|
||||
exportItemsInvoice = null;
|
||||
}
|
||||
|
||||
async function downloadBlob(blobPromise: Promise<Blob>, filename: string) {
|
||||
try {
|
||||
const blob = await blobPromise;
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
} catch {
|
||||
toast.error('Error al descargar el archivo');
|
||||
}
|
||||
}
|
||||
|
||||
const selectedInvoice = $derived(
|
||||
selectedInvoiceIds.length === 1 ? allItems.find((i) => i.id === selectedInvoiceIds[0]) : null
|
||||
);
|
||||
@@ -1614,6 +1701,7 @@
|
||||
{loadMore}
|
||||
selectedIds={selectedInvoiceIds}
|
||||
onRowClick={handleRowClick}
|
||||
onContextMenu={handleRowContextMenu}
|
||||
{sorting}
|
||||
onSortingChange={(newSorting) => (sorting = newSorting)}
|
||||
/>
|
||||
@@ -1810,6 +1898,121 @@
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
|
||||
{#if contextMenuOpen && contextMenuInvoice}
|
||||
<!-- Capa de cierre del menú contextual -->
|
||||
<button
|
||||
type="button"
|
||||
class="fixed inset-0 z-40 cursor-default border-0 bg-transparent p-0 [&:focus-visible]:outline-none"
|
||||
aria-label="Cerrar menú"
|
||||
onclick={() => { contextMenuOpen = false; contextMenuInvoice = null; }}
|
||||
oncontextmenu={(e) => { e.preventDefault(); contextMenuOpen = false; contextMenuInvoice = null; }}
|
||||
></button>
|
||||
<!-- Menú contextual de fila -->
|
||||
<div
|
||||
bind:this={contextMenuEl}
|
||||
class="fixed z-50 min-w-56 rounded-md border bg-popover p-1 text-sm shadow-md"
|
||||
style={`top: ${contextMenuPosition.y}px; left: ${contextMenuPosition.x}px;`}
|
||||
>
|
||||
<p class="truncate px-2 py-1 text-xs font-medium text-muted-foreground">{contextMenuInvoice.invoice_number ?? 'Factura'}</p>
|
||||
<div class="-mx-1 my-1 h-px bg-border"></div>
|
||||
{#snippet cmItem(icon: any, label: string, action: () => void)}
|
||||
{@const Icon = icon}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground"
|
||||
onclick={() => {
|
||||
// Capturar antes de nullificar el estado
|
||||
action();
|
||||
contextMenuOpen = false;
|
||||
contextMenuInvoice = null;
|
||||
}}
|
||||
>
|
||||
<Icon class="h-4 w-4 shrink-0" />
|
||||
<span>{label}</span>
|
||||
</button>
|
||||
{/snippet}
|
||||
{@render cmItem(Copy, 'Copiar Factura', () => {
|
||||
invoicesApi.copyInvoice(contextMenuInvoice!.id, companyStore.activeCompany!.id)
|
||||
.then((res) => {
|
||||
if (res.error) { toast.error(res.error); return; }
|
||||
toast.success('Factura copiada');
|
||||
reloadData();
|
||||
});
|
||||
})}
|
||||
{@render cmItem(Download, 'Exportar Partidas', () => {
|
||||
openExportItemsDialog(contextMenuInvoice!);
|
||||
})}
|
||||
{@render cmItem(ArrowRightLeft, 'Copiar Encabezado a SCAII', () => {
|
||||
const invId = contextMenuInvoice!.id;
|
||||
invoicesApi.copyInvoiceHeader(invId, companyStore.activeCompany!.id)
|
||||
.then((res) => {
|
||||
if (res.error) { toast.error(res.error); return; }
|
||||
toast.success('Encabezado copiado');
|
||||
reloadData();
|
||||
});
|
||||
})}
|
||||
<div class="-mx-1 my-1 h-px bg-border"></div>
|
||||
{@render cmItem(Route, 'Interfaz Carta Porte', () => {
|
||||
const inv = contextMenuInvoice!;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadCartaPorte(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_carta_porte.csv`
|
||||
);
|
||||
})}
|
||||
{@render cmItem(Truck, 'Interfaz GM Transport', () => {
|
||||
const inv = contextMenuInvoice!;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadGmTransport(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_gm_transport.csv`
|
||||
);
|
||||
})}
|
||||
{@render cmItem(Truck, 'Interfaz TFC', () => {
|
||||
const inv = contextMenuInvoice!;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadTfc(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_tfc.csv`
|
||||
);
|
||||
})}
|
||||
{@render cmItem(FileText, 'Interfaz Aviso Traslado', () => {
|
||||
const inv = contextMenuInvoice!;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadAvisoCruce(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_aviso_cruce.csv`
|
||||
);
|
||||
})}
|
||||
{@render cmItem(Route, 'Interfaz Carta Porte Consolidada', () => {
|
||||
const inv = contextMenuInvoice!;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadCartaPorteConsolidada(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_carta_porte_consolidada.csv`
|
||||
);
|
||||
})}
|
||||
{@render cmItem(Building2, 'Interfaz CAAAREM', () => {
|
||||
const inv = contextMenuInvoice!;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadCaaarem(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_caaarem.csv`
|
||||
);
|
||||
})}
|
||||
{@render cmItem(ArrowRightLeft, 'Interfaz AAduanal_RS', () => {
|
||||
const inv = contextMenuInvoice!;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadAaduanalRs(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_aaduanal_rs.csv`
|
||||
);
|
||||
})}
|
||||
{@render cmItem(Route, 'Interfaz Carta Porte Genesis', () => {
|
||||
const inv = contextMenuInvoice!;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadCpGenesis(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_cp_genesis.csv`
|
||||
);
|
||||
})}
|
||||
<div class="-mx-1 my-1 h-px bg-border"></div>
|
||||
{@render cmItem(Clipboard, 'Copiar Factura (Portapapeles)', () => handleCopyToClipboard(contextMenuInvoice!))}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showVuSubmenu}
|
||||
<!-- Capa para cerrar el submenú al hacer click fuera -->
|
||||
<button
|
||||
@@ -2088,6 +2291,186 @@
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<!-- Dropdown: Copias e Interfaces -->
|
||||
<DropdownMenu.Root
|
||||
open={copiasInterfacesMenuOpen}
|
||||
onOpenChange={(v) => { copiasInterfacesMenuOpen = v; }}
|
||||
>
|
||||
<DropdownMenu.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Button
|
||||
{...props}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={selectedInvoiceIds.length !== 1}
|
||||
data-footer-action="copias-interfaces"
|
||||
>
|
||||
<Copy class="mr-2 h-4 w-4" />
|
||||
Copias / Interfaces
|
||||
<ChevronUp class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
align="end"
|
||||
class="max-h-[480px] w-60 overflow-y-auto"
|
||||
data-invoice-footer-dropdown="copias-interfaces"
|
||||
>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Copias</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const invId = selectedInvoice.id;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
invoicesApi.copyInvoice(invId, companyStore.activeCompany!.id)
|
||||
.then((res) => {
|
||||
if (res.error) { toast.error(res.error); return; }
|
||||
toast.success('Factura copiada');
|
||||
reloadData();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Copy class="mr-2 h-4 w-4" />
|
||||
Copiar Factura
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
openExportItemsDialog(selectedInvoice);
|
||||
}}
|
||||
>
|
||||
<Download class="mr-2 h-4 w-4" />
|
||||
Exportar Partidas
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const invId = selectedInvoice.id;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
invoicesApi.copyInvoiceHeader(invId, companyStore.activeCompany!.id)
|
||||
.then((res) => {
|
||||
if (res.error) { toast.error(res.error); return; }
|
||||
toast.success('Encabezado copiado');
|
||||
reloadData();
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ArrowRightLeft class="mr-2 h-4 w-4" />
|
||||
Copiar Encabezado a SCAII
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onclick={() => { copiasInterfacesMenuOpen = false; if (selectedInvoice) handleCopyToClipboard(selectedInvoice, false); }}
|
||||
>
|
||||
<Clipboard class="mr-2 h-4 w-4" />
|
||||
Copiar Factura (Portapapeles)
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Interfaces</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={async () => {
|
||||
if (!selectedInvoice) return;
|
||||
const inv = selectedInvoice;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadCartaPorte(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_carta_porte.csv`
|
||||
);
|
||||
}}>
|
||||
<Route class="mr-2 h-4 w-4" />
|
||||
Carta Porte
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const inv = selectedInvoice;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadGmTransport(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_gm_transport.csv`
|
||||
);
|
||||
}}>
|
||||
<Truck class="mr-2 h-4 w-4" />
|
||||
GM Transport
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const inv = selectedInvoice;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadTfc(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_tfc.csv`
|
||||
);
|
||||
}}>
|
||||
<Truck class="mr-2 h-4 w-4" />
|
||||
TFC
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const inv = selectedInvoice;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadAvisoCruce(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_aviso_cruce.csv`
|
||||
);
|
||||
}}>
|
||||
<FileText class="mr-2 h-4 w-4" />
|
||||
Aviso Traslado
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const inv = selectedInvoice;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadCartaPorteConsolidada(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_carta_porte_consolidada.csv`
|
||||
);
|
||||
}}>
|
||||
<Route class="mr-2 h-4 w-4" />
|
||||
Carta Porte Consolidada
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const inv = selectedInvoice;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadCaaarem(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_caaarem.csv`
|
||||
);
|
||||
}}>
|
||||
<Building2 class="mr-2 h-4 w-4" />
|
||||
CAAAREM
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const inv = selectedInvoice;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadAaduanalRs(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_aaduanal_rs.csv`
|
||||
);
|
||||
}}>
|
||||
<ArrowRightLeft class="mr-2 h-4 w-4" />
|
||||
AAduanal_RS
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={() => {
|
||||
if (!selectedInvoice) return;
|
||||
const inv = selectedInvoice;
|
||||
copiasInterfacesMenuOpen = false;
|
||||
downloadBlob(
|
||||
invoicesApi.downloadCpGenesis(inv.id, companyStore.activeCompany!.id),
|
||||
`${inv.invoice_number ?? 'factura'}_cp_genesis.csv`
|
||||
);
|
||||
}}>
|
||||
<Route class="mr-2 h-4 w-4" />
|
||||
Carta Porte Genesis
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Group>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<div class="h-6 w-px bg-border"></div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -2166,6 +2549,53 @@
|
||||
<TransferenciaElectronicaModal bind:open={isTransferenciaModalOpen} invoice={selectedInvoice} />
|
||||
{/if}
|
||||
|
||||
<Dialog.Root bind:open={exportItemsDialogOpen}>
|
||||
<Dialog.Content class="sm:max-w-[320px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Exportar a</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Selecciona el formato para
|
||||
<span class="font-medium">{exportItemsInvoice?.invoice_number ?? 'la factura'}</span>.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<div class="py-4">
|
||||
<RadioGroup.Root
|
||||
value={exportItemsFormat}
|
||||
onValueChange={(v) => (exportItemsFormat = v as 'csv' | 'xlsx' | 'txt')}
|
||||
class="flex flex-col gap-3"
|
||||
>
|
||||
<div class="flex cursor-pointer items-center space-x-2">
|
||||
<RadioGroup.Item value="csv" id="fmt-csv" />
|
||||
<Label for="fmt-csv" class="cursor-pointer">CSV</Label>
|
||||
</div>
|
||||
<div class="flex cursor-pointer items-center space-x-2">
|
||||
<RadioGroup.Item value="xlsx" id="fmt-xlsx" />
|
||||
<Label for="fmt-xlsx" class="cursor-pointer">Excel (.xlsx)</Label>
|
||||
</div>
|
||||
<div class="flex cursor-pointer items-center space-x-2">
|
||||
<RadioGroup.Item value="txt" id="fmt-txt" />
|
||||
<Label for="fmt-txt" class="cursor-pointer">Texto plano (.txt)</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
exportItemsDialogOpen = false;
|
||||
exportItemsInvoice = null;
|
||||
}}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onclick={handleExportItemsConfirm}>
|
||||
<Download class="mr-2 h-4 w-4" />
|
||||
Descargar
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
{#if showDetailsDialog && selectedInvoice}
|
||||
<DetailsDialog invoice={selectedInvoice} onClose={() => (showDetailsDialog = false)} />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user