Se cambiaron los nombres de los archivos a uno mas estandarizado, se puso el ymal como estaba y se quitaron notificaciones

This commit is contained in:
2025-12-30 13:58:56 -06:00
parent 0e3d8654b6
commit 91d68cb1ef
49 changed files with 205 additions and 717 deletions

View File

@@ -1,27 +1,40 @@
"""
DTOs for Customs Broker Concepts.
"""
from datetime import datetime
from typing import Optional
from decimal import Decimal
from pydantic import BaseModel, Field, ConfigDict
class CustomsBrokerConceptBase(BaseModel):
broker_key: str = Field(..., max_length=5, description="Customs Broker Key (CLAVEAA)")
concept: str = Field(..., max_length=15, description="Concept")
amount: Optional[Decimal] = Field(None, description="Amount")
priority: Optional[int] = Field(None, description="Priority")
concept: str = Field(..., max_length=15, description="Concept/Code")
amount: Optional[float] = Field(None, description="Amount (IMPORTE)")
priority: Optional[int] = Field(None, description="Priority (PRIORIDAD)")
class CustomsBrokerConceptCreate(CustomsBrokerConceptBase):
"""Schema for creating a concept"""
pass
class CustomsBrokerConceptUpdate(BaseModel):
broker_key: Optional[str] = Field(None, max_length=5)
concept: Optional[str] = Field(None, max_length=15)
amount: Optional[Decimal] = None
priority: Optional[int] = None
"""Schema for updating a concept"""
broker_key: Optional[str] = Field(None, max_length=5, description="Customs Broker Key (CLAVEAA)")
concept: Optional[str] = Field(None, max_length=15, description="Concept/Code")
amount: Optional[float] = Field(None, description="Amount (IMPORTE)")
priority: Optional[int] = Field(None, description="Priority (PRIORIDAD)")
class CustomsBrokerConceptResponse(CustomsBrokerConceptBase):
"""Schema for concept response"""
id: int
company_id: int
tenant_id: int
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,14 +1,26 @@
"""
Routes for managing Customs Broker Concepts.
"""
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from .dto import CustomsBrokerConceptCreate, CustomsBrokerConceptResponse, CustomsBrokerConceptUpdate
from .service import CustomsBrokerConceptService
# Create router using TenantCRUDRoutes factory
router = TenantCRUDRoutes(
service=CustomsBrokerConceptService,
create_schema=CustomsBrokerConceptCreate,
update_schema=CustomsBrokerConceptUpdate,
response_schema=CustomsBrokerConceptResponse,
prefix="/customs-broker-concepts",
tags=["a76.general_catalogs.customs_broker_concepts"],
prefix="/customs-broker-concepts",
tags=["a76 / customs_broker_concepts"],
resource_name="Customs Broker Concept",
enable_list=True,
id_name="concept_id",
enable_list=True, # Enable GET /customs-broker-concepts with pagination
enable_filters=True, # Enable filtering
default_page_size=50,
max_page_size=100,
).router

View File

@@ -1,94 +1,125 @@
from typing import List, Optional, Tuple, Dict, Any
"""
Service layer for Customs Broker Concepts.
"""
from typing import Optional, Tuple, List, Dict, Any
import logging
from sqlalchemy.orm import Session
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
from .models import CustomsBrokerConcept
from .dto import CustomsBrokerConceptCreate, CustomsBrokerConceptUpdate
from . import dto, models
logger = logging.getLogger(__name__)
class CustomsBrokerConceptService:
"""Service for Customs Broker Concept CRUD operations with tenant support"""
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
limit: int = 50,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[CustomsBrokerConcept], int]:
query = db.query(CustomsBrokerConcept).filter(
CustomsBrokerConcept.tenant_id == tenant_id,
CustomsBrokerConcept.company_id == company_id
) -> Tuple[List[models.CustomsBrokerConcept], int]:
"""Get all customs broker concepts for a tenant/company with pagination"""
query = db.query(models.CustomsBrokerConcept).filter(
models.CustomsBrokerConcept.tenant_id == tenant_id,
models.CustomsBrokerConcept.company_id == company_id,
)
total = query.count()
items = query.offset(skip).limit(limit).all()
# Apply filters if provided
if filters:
if filters.get("broker_key"):
query = query.filter(
models.CustomsBrokerConcept.broker_key.ilike(f"%{filters['broker_key']}%")
)
if filters.get("concept"):
query = query.filter(
models.CustomsBrokerConcept.concept.ilike(
f"%{filters['concept']}%")
)
return items, total
total = query.count()
concepts = query.offset(skip).limit(limit).all()
return concepts, total
@staticmethod
def get_by_id(
db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[CustomsBrokerConcept]:
return db.query(CustomsBrokerConcept).filter(
CustomsBrokerConcept.id == id,
CustomsBrokerConcept.tenant_id == tenant_id,
CustomsBrokerConcept.company_id == company_id
).first()
db: Session, concept_id: int, tenant_id: int, company_id: int
) -> Optional[models.CustomsBrokerConcept]:
"""Get customs broker concept by ID"""
return (
db.query(models.CustomsBrokerConcept)
.filter(
models.CustomsBrokerConcept.id == concept_id,
models.CustomsBrokerConcept.tenant_id == tenant_id,
models.CustomsBrokerConcept.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session, data: CustomsBrokerConceptCreate, tenant_id: int, company_id: int
) -> CustomsBrokerConcept:
db_obj = CustomsBrokerConcept(
**data.model_dump(),
tenant_id=tenant_id,
company_id=company_id
db: Session,
concept_data: dto.CustomsBrokerConceptCreate,
tenant_id: int,
company_id: int,
) -> models.CustomsBrokerConcept:
"""Create a new customs broker concept"""
new_concept = models.CustomsBrokerConcept(
**concept_data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
db.add(db_obj)
db.add(new_concept)
db.commit()
db.refresh(db_obj)
return db_obj
db.refresh(new_concept)
return new_concept
@staticmethod
def update(
db: Session, id: int, tenant_id: int, data: CustomsBrokerConceptUpdate, company_id: int
) -> Optional[CustomsBrokerConcept]:
db_obj = CustomsBrokerConceptService.get_by_id(
db, id, tenant_id, company_id)
if not db_obj:
db: Session,
concept_id: int,
tenant_id: int,
concept_data: dto.CustomsBrokerConceptUpdate,
company_id: int,
) -> Optional[models.CustomsBrokerConcept]:
"""Update a customs broker concept"""
concept = CustomsBrokerConceptService.get_by_id(
db, concept_id, tenant_id, company_id)
if not concept:
return None
update_dict = data.model_dump(exclude_unset=True)
for key, value in update_dict.items():
setattr(db_obj, key, value)
# Update fields
update_data = concept_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(concept, field, value)
db.commit()
db.refresh(db_obj)
return db_obj
db.refresh(concept)
return concept
@staticmethod
def delete(
db: Session, id: int, tenant_id: int, company_id: int
db: Session, concept_id: int, tenant_id: int, company_id: int
) -> bool:
db_obj = CustomsBrokerConceptService.get_by_id(
db, id, tenant_id, company_id)
if not db_obj:
"""Delete a customs broker concept"""
concept = CustomsBrokerConceptService.get_by_id(
db, concept_id, tenant_id, company_id)
if not concept:
return False
try:
db.delete(db_obj)
db.delete(concept)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError deleting customs broker concept {id}: {str(e)}")
logger.error(f"IntegrityError deleting concept {concept_id}: {str(e)}")
if "foreign key constraint" in str(e).lower():
raise HTTPException(
status_code=400,
@@ -97,5 +128,5 @@ class CustomsBrokerConceptService:
raise HTTPException(status_code=400, detail="Error al eliminar el concepto")
except Exception as e:
db.rollback()
logger.error(f"Error deleting customs broker concept {id}: {str(e)}")
logger.error(f"Error deleting concept {concept_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error al eliminar el concepto")

View File

@@ -3,33 +3,21 @@ import type { ApiResponse } from '$lib/api';
export interface CustomsBrokerConcept {
id: number;
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
tenant_id: string;
company_id?: string;
broker_key: string;
concept: string;
amount?: number | null;
priority?: number | null;
company_id: number;
tenant_id: number;
created_at: string;
updated_at?: string;
updated_at?: string | null;
}
export interface CustomsBrokerConceptCreate {
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
broker_key: string;
concept: string;
amount?: number | null;
priority?: number | null;
}
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}

View File

@@ -1,75 +0,0 @@
import { api } from '$lib/api';
import type { ApiResponse } from '$lib/api';
export interface CustomsBrokerConcept {
id: number;
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
tenant_id: string;
company_id?: string;
created_at: string;
updated_at?: string;
}
export interface CustomsBrokerConceptCreate {
code: string;
description?: string;
description_en?: string;
detailed_description?: string;
priority?: number;
first_total?: boolean;
type?: string;
is_printed?: boolean;
section?: number;
classification?: string;
}
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}
export interface CustomsBrokerConceptListResponse {
items: CustomsBrokerConcept[];
total: number;
page: number;
page_size: number;
pages: number;
}
export async function getCustomsBrokerConcepts(
page: number = 1,
pageSize: number = 50,
companyId: number,
filters: Record<string, any> = {},
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
const params = new URLSearchParams({
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId.toString(),
...filters
});
return await api.get(`/v1/a76/customs-broker-concepts?${params.toString()}`);
}
export async function getCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
return await api.get(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
}
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
return await api.post(`/v1/a76/customs-broker-concepts?company_id=${companyId}`, data);
}
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
return await api.put(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`, data);
}
export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
return await api.delete(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
}

View File

@@ -24,7 +24,6 @@
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
@@ -35,20 +34,17 @@
const response = await deleteClassificationConcept(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Clasificación "${item.classification}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}

View File

@@ -30,13 +30,11 @@
// Si hay error en la respuesta
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito (status 204 o 200)
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Empresa "${item.name}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
@@ -44,7 +42,6 @@
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}

View File

@@ -24,7 +24,6 @@
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
@@ -35,20 +34,17 @@
const response = await deleteConcept(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Concepto "${item.code}" eliminado correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}

View File

@@ -6,24 +6,24 @@ import DataTableActions from './data-table-actions.svelte';
export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBrokerConcept>[] {
return [
{
accessorKey: 'code',
header: 'Código',
cell: ({ row }) => row.original.code || '-'
accessorKey: 'broker_key',
header: 'Clave Agente',
cell: ({ row }) => row.original.broker_key || '-'
},
{
accessorKey: 'description',
header: 'Descripción',
cell: ({ row }) => row.original.description || '-'
accessorKey: 'concept',
header: 'Concepto',
cell: ({ row }) => row.original.concept || '-'
},
{
accessorKey: 'type',
header: 'Tipo',
cell: ({ row }) => row.original.type || '-'
accessorKey: 'amount',
header: 'Importe',
cell: ({ row }) => row.original.amount ? `$${row.original.amount.toFixed(2)}` : '-'
},
{
accessorKey: 'section',
header: 'Sección',
cell: ({ row }) => row.original.section?.toString() || '-'
accessorKey: 'priority',
header: 'Prioridad',
cell: ({ row }) => row.original.priority?.toString() || '-'
},
{
id: 'actions',

View File

@@ -6,49 +6,51 @@
import {
createCustomsBrokerConcept,
updateCustomsBrokerConcept,
type CustomsBrokerConcept
type CustomsBrokerConcept,
type CustomsBrokerConceptCreate
} from "$lib/api/dashboard/a76/general_catalogs/customs-broker-concepts";
import { companyStore } from "$lib/stores/company.svelte";
let {
open = $bindable(false),
item = null,
companyId,
onSuccess
}: {
open: boolean;
item?: CustomsBrokerConcept | null;
item?: CustomsBrokerConcept | null;
companyId: number;
onSuccess?: () => void;
} = $props();
const isEdit = $derived(!!item);
const title = $derived(isEdit ? "Editar Concepto AA" : "Nuevo Concepto AA");
// 3. Estado alineado al modelo de BD
let formData = $state({
// Estado alineado a CustomsBrokerConceptCreate
let formData = $state<CustomsBrokerConceptCreate>({
broker_key: '',
concept: '',
amount: null as number | null,
priority: null as number | null
amount: undefined,
priority: undefined
});
let loading = $state(false);
let error = $state<string | null>(null);
// 4. Cargar datos al editar
// Cargar datos al editar
$effect(() => {
if (item) {
formData = {
broker_key: item.broker_key || '',
concept: item.concept || '',
amount: item.amount || null,
priority: item.priority || null
amount: item.amount,
priority: item.priority
};
} else {
formData = {
broker_key: '',
concept: '',
amount: null,
priority: null
amount: undefined,
priority: undefined
};
}
});
@@ -58,34 +60,30 @@
loading = true;
try {
const companyId = companyStore.activeCompany?.id;
if (!companyId) throw new Error('No hay una compañía seleccionada');
// Validaciones
if (!formData.broker_key.trim()) throw new Error('La Clave AA es requerida');
if (!formData.broker_key.trim()) throw new Error('La Clave del Agente es requerida');
if (!formData.concept.trim()) throw new Error('El Concepto es requerido');
// 5. Preparar datos con los tipos correctos (Números)
const dataToSend = {
// Limpiar y enviar datos
const dataToSend: CustomsBrokerConceptCreate = {
broker_key: formData.broker_key.trim(),
concept: formData.concept.trim(),
amount: formData.amount ? Number(formData.amount) : undefined,
priority: formData.priority ? Number(formData.priority) : undefined
};
// 6. Corregida la sintaxis de llamada a la API
if (isEdit && item) {
// UPDATE: (id, data, companyId)
await updateCustomsBrokerConcept(item.id, dataToSend, companyId);
} else {
// CREATE: (data, companyId)
await createCustomsBrokerConcept(dataToSend, companyId);
}
open = false;
if (onSuccess) onSuccess();
} catch (e) {
error = e instanceof Error ? e.message : 'Error al guardar';
} catch (e: any) {
error = e.message || 'Error al guardar';
} finally {
loading = false;
}
@@ -93,11 +91,11 @@
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-md max-h-[90vh] overflow-y-auto">
<Dialog.Content class="max-w-lg max-h-[90vh] overflow-y-auto">
<Dialog.Header>
<Dialog.Title>{title}</Dialog.Title>
<Dialog.Description>
{isEdit ? 'Modifica el concepto del agente aduanal' : 'Crea un nuevo concepto'}
{isEdit ? 'Modifica los detalles del concepto' : 'Ingresa los datos para el nuevo concepto'}
</Dialog.Description>
</Dialog.Header>
@@ -108,29 +106,30 @@
</div>
{/if}
<div class="grid gap-4">
<div class="grid gap-2">
<Label for="broker_key">Clave AA <span class="text-destructive">*</span></Label>
<div class="grid grid-cols-2 gap-4">
<div class="grid gap-2 col-span-1">
<Label for="broker_key">Clave del Agente <span class="text-destructive">*</span></Label>
<Input
id="broker_key"
bind:value={formData.broker_key}
placeholder="Ej: 550"
maxlength={5}
placeholder="Ej: 01001"
maxlength="5"
disabled={isEdit}
/>
</div>
<div class="grid gap-2">
<div class="grid gap-2 col-span-1">
<Label for="concept">Concepto <span class="text-destructive">*</span></Label>
<Input
id="concept"
bind:value={formData.concept}
placeholder="Ej: FLETE"
maxlength={15}
placeholder="Ej: 001"
maxlength="15"
disabled={isEdit}
/>
</div>
<div class="grid gap-2">
<div class="grid gap-2 col-span-1">
<Label for="amount">Importe</Label>
<Input
id="amount"
@@ -141,13 +140,13 @@
/>
</div>
<div class="grid gap-2">
<div class="grid gap-2 col-span-1">
<Label for="priority">Prioridad</Label>
<Input
id="priority"
type="number"
bind:value={formData.priority}
placeholder="Ej: 1"
placeholder="0"
/>
</div>
</div>

View File

@@ -24,7 +24,6 @@
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
@@ -35,20 +34,17 @@
const response = await deleteCustomsBrokerConcept(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Concepto "${item.code}" eliminado correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}

View File

@@ -27,7 +27,6 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('❌ Error: No hay compañía seleccionada');
return;
}
@@ -35,11 +34,9 @@
try {
await deleteDoda(item.id, companyId);
alert('✅ Registro eliminado correctamente');
if (onSuccess) onSuccess();
} catch (err: any) {
error = err.message || 'Error al eliminar el registro';
alert(`❌ Error: ${error}`);
console.error('Error deleting doda:', err);
} finally {
loading = false;

View File

@@ -27,7 +27,6 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('❌ Error: No hay compañía seleccionada');
return;
}
@@ -35,11 +34,9 @@
try {
await deleteElectronicNotice(item.id, companyId);
alert('✅ Aviso electrónico eliminado correctamente');
if (onSuccess) onSuccess();
} catch (err: any) {
error = err.message || 'Error al eliminar el aviso electrónico';
alert(`❌ Error: ${error}`);
console.error('Error deleting electronic notice:', err);
} finally {
loading = false;

View File

@@ -27,7 +27,6 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('❌ Error: No hay compañía seleccionada');
return;
}
@@ -40,11 +39,9 @@
throw new Error(response.error);
}
alert('✅ Equivalencia eliminada correctamente');
if (onSuccess) onSuccess();
} catch (err: any) {
error = err.message || 'Error al eliminar la equivalencia';
alert(`❌ Error: ${error}`);
console.error('Error deleting equivalency:', err);
} finally {
loading = false;

View File

@@ -83,14 +83,12 @@
if (isEdit && item) {
await updateErrorCatalog(item.id, basePayload, companyId);
alert("✅ Error actualizado correctamente");
} else {
const createPayload = {
code: formData.code.trim(),
...basePayload
};
await createErrorCatalog(createPayload, companyId);
alert("✅ Error creado correctamente");
}
open = false;

View File

@@ -27,7 +27,6 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('❌ Error: No hay compañía seleccionada');
return;
}
@@ -35,13 +34,11 @@
try {
await deleteErrorCatalog(item.id, companyId);
alert('✅ Error eliminado correctamente');
if (onSuccess) {
onSuccess();
}
} catch (err: any) {
error = err.message || 'Error al eliminar el error';
alert(`❌ Error: ${error}`);
console.error('Error deleting:', err);
} finally {
loading = false;

View File

@@ -27,7 +27,6 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('❌ Error: No hay compañía seleccionada');
return;
}
@@ -35,11 +34,9 @@
try {
await deleteIdentifier(item.id, companyId);
alert('✅ Identificador eliminado correctamente');
if (onSuccess) onSuccess();
} catch (err: any) {
error = err.message || 'Error al eliminar el identificador';
alert(`❌ Error: ${error}`);
console.error('Error deleting identifier:', err);
} finally {
loading = false;

View File

@@ -27,7 +27,6 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('❌ Error: No hay compañía seleccionada');
return;
}
@@ -35,11 +34,9 @@
try {
await deleteINPC(item.id, companyId);
alert('✅ Registro eliminado correctamente');
if (onSuccess) onSuccess();
} catch (err: any) {
error = err.message || 'Error al eliminar el registro';
alert(`❌ Error: ${error}`);
console.error('Error deleting INPC:', err);
} finally {
loading = false;

View File

@@ -25,7 +25,6 @@
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
@@ -36,20 +35,17 @@
const response = await deleteLegend(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
// Éxito
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Leyenda "${item.code}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}

View File

@@ -60,14 +60,12 @@
if (isEdit && item) {
await updateLocation(item.id, basePayload, companyId);
alert("✅ Ubicación actualizada correctamente");
} else {
const createPayload = {
location_code: formData.location_code.trim(),
...basePayload
};
await createLocation(createPayload, companyId);
alert("✅ Ubicación creada correctamente");
}
open = false;

View File

@@ -93,10 +93,8 @@
if (isEdit && item) {
await updateMultiCurrencyType(item.id, dataToSend, companyId);
alert(`✅ Tipo de moneda múltiple actualizado correctamente`);
} else {
await createMultiCurrencyType(dataToSend, companyId);
alert(`✅ Tipo de moneda múltiple creado correctamente`);
}
open = false;

View File

@@ -27,7 +27,6 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('❌ Error: No hay compañía seleccionada');
return;
}
@@ -35,11 +34,9 @@
try {
await deletePrevalidator(item.id, companyId);
alert('✅ Prevalidador eliminado correctamente');
if (onSuccess) onSuccess();
} catch (err: any) {
error = err.message || 'Error al eliminar el prevalidador';
alert(`❌ Error: ${error}`);
console.error('Error deleting prevalidator:', err);
} finally {
loading = false;

View File

@@ -27,7 +27,6 @@
const companyId = companyStore.activeCompany?.id;
if (!companyId) {
alert('❌ Error: No hay compañía seleccionada');
return;
}
@@ -35,11 +34,9 @@
try {
await deleteSignature(item.id, companyId);
alert('✅ Firma eliminada correctamente');
if (onSuccess) onSuccess();
} catch (err: any) {
error = err.message || 'Error al eliminar la firma';
alert(`❌ Error: ${error}`);
console.error('Error deleting signature:', err);
} finally {
loading = false;

View File

@@ -70,10 +70,8 @@
if (isEdit && conversion) {
await updateUnitConversion(conversion.id, dataToSend, companyId);
alert(`✅ Conversión "${dataToSend.from_unit_code}${dataToSend.to_unit_code}" actualizada correctamente`);
} else {
await createUnitConversion(dataToSend, companyId);
alert(`✅ Conversión "${dataToSend.from_unit_code}${dataToSend.to_unit_code}" creada correctamente`);
}
open = false;

View File

@@ -23,7 +23,6 @@
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
@@ -31,13 +30,11 @@
try {
await deleteUnitConversion(conversion.id, companyStore.activeCompany.id);
alert(`✅ Conversión "${conversion.from_unit_code}${conversion.to_unit_code}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}

View File

@@ -23,7 +23,6 @@
}
if (!companyStore.activeCompany) {
alert('❌ Error: No hay una compañía seleccionada');
return;
}
@@ -33,19 +32,16 @@
const response = await deleteUnitOfMeasureACE(item.id, companyStore.activeCompany.id);
if (response.error) {
alert(`❌ Error al eliminar:\n\n${response.error}`);
return;
}
if (response.status === 204 || response.status === 200 || !response.error) {
alert(`✅ Unidad ACE "${item.code}" eliminada correctamente`);
if (onSuccess) {
onSuccess();
}
}
} catch (e) {
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
} finally {
loading = false;
}

View File

@@ -45,7 +45,6 @@
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
@@ -59,9 +58,7 @@
: await createUnitOfMeasureAmerican(data, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al guardar');
} else {
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
open = false;
onSuccess?.();
}

View File

@@ -20,15 +20,12 @@
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const response = await deleteUnitOfMeasureAmerican(unit.id, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al eliminar');
} else if (response.status === 204 || response.status === 200) {
alert('Unidad eliminada');
onSuccess?.();
}
}

View File

@@ -47,7 +47,6 @@
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
@@ -62,9 +61,7 @@
: await createUnitOfMeasureCustoms(data, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al guardar');
} else {
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
open = false;
onSuccess?.();
}

View File

@@ -20,15 +20,12 @@
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const response = await deleteUnitOfMeasureCustoms(unit.id, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al eliminar');
} else if (response.status === 204 || response.status === 200) {
alert('Unidad eliminada');
onSuccess?.();
}
}

View File

@@ -45,7 +45,6 @@
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
@@ -59,9 +58,7 @@
: await createUnitOfMeasureGeneral(data, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al guardar');
} else {
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
open = false;
onSuccess?.();
}

View File

@@ -20,15 +20,12 @@
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const response = await deleteUnitOfMeasureGeneral(unit.id, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al eliminar');
} else if (response.status === 204 || response.status === 200) {
alert('Unidad eliminada');
onSuccess?.();
}
}

View File

@@ -45,7 +45,6 @@
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
@@ -59,9 +58,7 @@
: await createUnitOfMeasureOMA(data, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al guardar');
} else {
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
open = false;
onSuccess?.();
}

View File

@@ -20,15 +20,12 @@
const activeCompanyId = companyStore.activeCompany?.id;
if (!activeCompanyId) {
alert('No hay una compañía activa seleccionada');
return;
}
const response = await deleteUnitOfMeasureOMA(unit.id, activeCompanyId);
if (response.error) {
alert(response.error.detail || 'Error al eliminar');
} else if (response.status === 204 || response.status === 200) {
alert('Unidad eliminada');
onSuccess?.();
}
}

View File

@@ -14,7 +14,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
companies: parentData.companies || []
};
}
/////HOLAAA
try {
// Obtener company_id de múltiples fuentes (en orden de prioridad):
// 1. URL query param (permite cambiar vía navegación)

View File

@@ -12,7 +12,7 @@
import { browser } from '$app/environment';
import { companyStore } from '$lib/stores/company.svelte';
import { Plus, Search, Trash2 } from 'lucide-svelte';
//HOLAA
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();

View File

@@ -1,395 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import * as Card from "$lib/components/ui/card";
import { customsBrokersApi, type CreateCustomsBrokerData } from "$lib/api/dashboard/a76/customs-brokers";
import { companyStore } from "$lib/stores/company.svelte";
import { goto } from "$app/navigation";
import { ArrowLeft } from "lucide-svelte";
// Eliminar props de modal ya que ahora es una página
// let { open = $bindable(false), onSuccess }: { open: boolean; onSuccess?: () => void; } = $props();
let formData = $state({
broker_key: "",
name: "",
type: "",
address: "",
postal_code: "",
city: "",
state: "",
phone: "",
fax: "",
email: "",
country: "",
tax_id: "",
personal_id: "",
position: "",
license: "",
company: "",
contact: ""
});
let loading = $state(false);
let error = $state<string | null>(null);
async function handleSubmit(e: Event) {
e.preventDefault();
if (!companyStore.activeCompany) {
error = "No hay compañía seleccionada";
return;
}
loading = true;
error = null;
try {
const payload: CreateCustomsBrokerData = {
broker_key: formData.broker_key,
name: formData.name || null,
type: formData.type || null,
address: formData.address || null,
postal_code: formData.postal_code || null,
city: formData.city || null,
state: formData.state || null,
phone: formData.phone || null,
fax: formData.fax || null,
email: formData.email || null,
country: formData.country || null,
tax_id: formData.tax_id || null,
personal_id: formData.personal_id || null,
position: formData.position || null,
license: formData.license || null,
company: formData.company || null,
contact: formData.contact || null,
tenant_id: "1", // TODO: Get from user context
company_id: companyStore.activeCompany.id.toString()
};
const response = await customsBrokersApi.create(payload);
if (response.error) {
if (response.status === 409) {
error = 'Ya existe un agente aduanal con la clave proporcionada.';
} else
if (response.status === 401) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
error = response.error;
}
return;
}
// Éxito - redirigir a la lista
goto('/dashboard/customs_brokers');
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error saving:", e);
} finally {
loading = false;
}
}
function resetForm() {
// Limpiar formulario
formData = {
broker_key: "",
name: "",
type: "",
address: "",
postal_code: "",
city: "",
state: "",
phone: "",
fax: "",
email: "",
country: "",
tax_id: "",
personal_id: "",
position: "",
license: "",
company: "",
contact: ""
};
error = null;
}
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center gap-4">
<Button variant="ghost" size="icon" onclick={() => goto('/dashboard/customs_brokers')}>
<ArrowLeft class="h-4 w-4" />
</Button>
<div>
<h1 class="text-3xl font-bold tracking-tight">Nuevo Agente Aduanal</h1>
<p class="text-muted-foreground">
Completa los datos para crear un nuevo agente aduanal
</p>
</div>
</div>
<!-- Formulario -->
<Card.Root>
<Card.Header>
<Card.Title>Información del Agente Aduanal</Card.Title>
<Card.Description>
Todos los campos marcados con * son obligatorios
</Card.Description>
</Card.Header>
<Card.Content>
<form onsubmit={handleSubmit} class="space-y-6">
{#if error}
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Información básica -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información Básica</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="broker_key">Clave *</Label>
<Input
id="broker_key"
bind:value={formData.broker_key}
placeholder="Ej: 12345"
maxlength={5}
required
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="type">Tipo</Label>
<Input
id="type"
bind:value={formData.type}
placeholder="Tipo de agente"
maxlength={9}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="name">Nombre</Label>
<Input
id="name"
bind:value={formData.name}
placeholder="Nombre del agente aduanal"
maxlength={80}
disabled={loading}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="license">Patente</Label>
<Input
id="license"
bind:value={formData.license}
placeholder="Número de patente"
maxlength={4}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="company">Empresa</Label>
<Input
id="company"
bind:value={formData.company}
placeholder="Empresa del agente"
maxlength={200}
disabled={loading}
/>
</div>
</div>
</div>
<!-- Información de contacto -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información de Contacto</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="phone">Teléfono</Label>
<Input
id="phone"
bind:value={formData.phone}
placeholder="Número telefónico"
maxlength={30}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="fax">Fax</Label>
<Input
id="fax"
bind:value={formData.fax}
placeholder="Número de fax"
maxlength={30}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
type="email"
bind:value={formData.email}
placeholder="correo@ejemplo.com"
maxlength={100}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="contact">Contacto</Label>
<Input
id="contact"
bind:value={formData.contact}
placeholder="Nombre del contacto"
maxlength={80}
disabled={loading}
/>
</div>
</div>
<!-- Dirección -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Dirección</h3>
<div class="space-y-2">
<Label for="address">Dirección</Label>
<Input
id="address"
bind:value={formData.address}
placeholder="Calle y número"
maxlength={1500}
disabled={loading}
/>
</div>
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="postal_code">Código Postal</Label>
<Input
id="postal_code"
bind:value={formData.postal_code}
placeholder="C.P."
maxlength={15}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="city">Ciudad</Label>
<Input
id="city"
bind:value={formData.city}
placeholder="Ciudad"
maxlength={30}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="state">Estado</Label>
<Input
id="state"
bind:value={formData.state}
placeholder="Estado"
maxlength={30}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="country">País</Label>
<Input
id="country"
bind:value={formData.country}
placeholder="País"
maxlength={3}
disabled={loading}
/>
</div>
</div>
<!-- Información fiscal -->
<div class="space-y-4">
<h3 class="text-sm font-semibold">Información Fiscal</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="tax_id">RFC</Label>
<Input
id="tax_id"
bind:value={formData.tax_id}
placeholder="RFC"
maxlength={30}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="personal_id">CURP</Label>
<Input
id="personal_id"
bind:value={formData.personal_id}
placeholder="CURP"
maxlength={20}
disabled={loading}
/>
</div>
</div>
<div class="space-y-2">
<Label for="position">Posición</Label>
<Input
id="position"
bind:value={formData.position}
placeholder="Cargo o posición"
maxlength={30}
disabled={loading}
/>
</div>
</div>
<div class="fixed bottom-0 right-0 left-0 md:left-64 py-2 px-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
<Button type="button" variant="outline" onclick={() => goto('/dashboard/customs_brokers')} disabled={loading}>
Cancelar
</Button>
<Button type="button" variant="outline" onclick={resetForm} disabled={loading}>
Limpiar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<div class="flex items-center gap-2">
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"></div>
Guardando...
</div>
{:else}
Crear Agente Aduanal
{/if}
</Button>
</div>
</form>
</Card.Content>
</Card.Root>
</div>

View File

@@ -6,48 +6,44 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
const { accessToken } = getAuthTokens(cookies);
if (!accessToken) {
return { error: 'No authenticated', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
return { error: 'No authenticated', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
}
try {
const page = Number(url.searchParams.get('page')) || 1;
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
const filters: Record<string, string> = {};
const code = url.searchParams.get('code');
const description = url.searchParams.get('description');
if (code) filters.code = code;
if (description) filters.description = description;
// Obtener company_id de la cookie o usar el primero disponible
const parentData = await parent();
const cookieCompanyId = cookies.get('active_company_id');
const companyId = cookieCompanyId
? parseInt(cookieCompanyId)
: parentData.companies?.[0]?.id;
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
if (!companyId) {
return {
error: 'No se encontró una compañía seleccionada',
concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }
};
return { error: 'No company selected', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
}
const filters: Record<string, string> = {};
const brokerKey = url.searchParams.get('broker_key');
const concept = url.searchParams.get('concept');
if (brokerKey) filters.broker_key = brokerKey;
if (concept) filters.concept = concept;
const queryParams = new URLSearchParams({
company_id: companyId.toString(),
page: page.toString(),
page_size: pageSize.toString(),
company_id: companyId,
...filters
});
const response = await authenticatedFetch(`v1/a76/customs-broker-concepts?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
if (!response.ok) {
return { error: 'Failed to load', concepts: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
return { error: 'Failed to load', concepts: { items: [], total: 0, page, page_size: pageSize, pages: 0 }, activeCompanyId: companyId };
}
return { concepts: await response.json() };
const data = await response.json();
// Calculate pages if not provided by API
const pages = data.pages || Math.ceil(data.total / pageSize);
return { concepts: { ...data, pages }, activeCompanyId: parseInt(companyId) };
} catch (error) {
console.error('Error loading customs broker concepts:', error);
return { error: 'Error loading', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
return { error: 'Error loading', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
}
};

View File

@@ -13,8 +13,8 @@
let dialogOpen = $state(false);
// Filtros
let searchCode = $state($page.url.searchParams.get('code') || '');
let searchDesc = $state($page.url.searchParams.get('description') || '');
let searchBrokerKey = $state($page.url.searchParams.get('broker_key') || '');
let searchConcept = $state($page.url.searchParams.get('concept') || '');
let timeout: ReturnType<typeof setTimeout>;
// Solo necesitamos una declaración de handleSuccess
@@ -31,11 +31,11 @@
clearTimeout(timeout);
timeout = setTimeout(() => {
const url = new URL($page.url);
if (searchCode) url.searchParams.set('code', searchCode);
else url.searchParams.delete('code');
if (searchBrokerKey) url.searchParams.set('broker_key', searchBrokerKey);
else url.searchParams.delete('broker_key');
if (searchDesc) url.searchParams.set('description', searchDesc);
else url.searchParams.delete('description');
if (searchConcept) url.searchParams.set('concept', searchConcept);
else url.searchParams.delete('concept');
url.searchParams.set('page', '1');
goto(url, { keepFocus: true, noScroll: true });
@@ -60,15 +60,15 @@
<div class="flex gap-4 items-end">
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por código..."
bind:value={searchCode}
placeholder="Buscar por clave de agente..."
bind:value={searchBrokerKey}
oninput={handleSearch}
/>
</div>
<div class="grid w-full max-w-sm items-center gap-1.5">
<Input
placeholder="Buscar por descripción..."
bind:value={searchDesc}
placeholder="Buscar por concepto..."
bind:value={searchConcept}
oninput={handleSearch}
/>
</div>
@@ -85,6 +85,7 @@
<CreateEditDialog
bind:open={dialogOpen}
companyId={data.activeCompanyId}
onSuccess={handleSuccess}
/>
</div>

View File

@@ -2,9 +2,9 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import DataTable from '$lib/components/dashboard/general_catalogs/electronic-notices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/electronic-notices/columns';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/electronic-notices/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/general_catalogs/electronic_notices/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/general_catalogs/electronic_notices/columns.js';
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/electronic_notices/create-edit-dialog.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';

View File

@@ -2,9 +2,9 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import { createColumns } from '$lib/components/dashboard/exchange-rate/columns';
import CreateEditDialog from '$lib/components/dashboard/exchange-rate/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/exchange-rate/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/exchange_rate/columns';
import CreateEditDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
import DataTable from '$lib/components/dashboard/exchange_rate/data-table.svelte';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Plus } from 'lucide-svelte';