feat: add country fetching API endpoint and implement dashboard contributions management

- Implemented a new API endpoint for fetching countries with pagination support.
- Added a new Svelte component for managing contributions in the dashboard, including CRUD operations for contributions and general contributions.
- Introduced dynamic tab navigation for different contribution types (DTA, PREV, ECI, MULT, REC).
- Enhanced user interface with dialogs for adding and editing contributions and general contributions.
This commit is contained in:
Galindo97
2025-12-31 13:51:17 -06:00
parent 7e72b559ef
commit 57b7e6dcbf
5 changed files with 1795 additions and 147 deletions

View File

@@ -16,8 +16,8 @@ async def list_countries(
page: int = Query(1, ge=1, description="Número de página"),
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Endpoint público para obtener lista de países - no requiere autenticación"""
skip = (page - 1) * page_size
query = db.query(Country)
items = query.offset(skip).limit(page_size).all()

View File

@@ -51,6 +51,14 @@
abreviacion: string;
forma_pago_2: string;
importe_2: number;
contribuciones_generales?: Array<{
pedimento_sys_id: string;
clave_contribucion: string;
clave_tasa: string;
tasa_contribucion: number;
clave_forma_pago: string;
importe_contribucion: number;
}>;
}
interface ContribucionGeneral {
@@ -132,6 +140,16 @@
// Estados para contribuciones generales (dentro del diálogo)
let isContribGenDialogOpen = $state(false);
let editingContribGenIndex = $state<number | null>(null);
// Estado temporal para contribución general dentro del diálogo de contribución
let tempContribGen = $state({
pedimento_sys_id: '',
clave_contribucion: '',
clave_tasa: '',
tasa_contribucion: 0,
clave_forma_pago: '',
importe_contribucion: 0
});
let currentContribucion = $state<Contribucion>({
contribucion: '',
@@ -142,7 +160,8 @@
gravamen: '',
abreviacion: '',
forma_pago_2: '',
importe_2: 0
importe_2: 0,
contribuciones_generales: []
});
let currentContribGen = $state<ContribucionGeneral>({
@@ -165,7 +184,8 @@
gravamen: '',
abreviacion: '',
forma_pago_2: '',
importe_2: 0
importe_2: 0,
contribuciones_generales: []
};
isDialogOpen = true;
}
@@ -194,6 +214,34 @@
}
}
// Funciones para Contribuciones Generales (Registro 510) dentro del diálogo de contribución
function openNewContribGenInDialog() {
tempContribGen = {
pedimento_sys_id: '',
clave_contribucion: '',
clave_tasa: '',
tasa_contribucion: 0,
clave_forma_pago: '',
importe_contribucion: 0
};
isContribGenDialogOpen = true;
}
function saveContribGenInDialog() {
if (!currentContribucion.contribuciones_generales) {
currentContribucion.contribuciones_generales = [];
}
currentContribucion.contribuciones_generales.push({
pedimento_sys_id: tempContribGen.pedimento_sys_id,
clave_contribucion: tempContribGen.clave_contribucion,
clave_tasa: tempContribGen.clave_tasa,
tasa_contribucion: tempContribGen.tasa_contribucion,
clave_forma_pago: tempContribGen.clave_forma_pago,
importe_contribucion: tempContribGen.importe_contribucion
});
isContribGenDialogOpen = false;
}
// Funciones para Contribuciones Generales (Registro 510)
function openNewContribGen() {
editingContribGenIndex = null;
@@ -527,24 +575,27 @@
<!-- Dialog para Contribuciones -->
<Dialog bind:open={isDialogOpen}>
<DialogContent class="max-w-3xl">
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editingIndex !== null ? 'Editar Contribución' : 'Nueva Contribución'}
</DialogTitle>
</DialogHeader>
<div class="grid gap-4 py-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-6 py-4">
<!-- Tasas Pedimento - Registro 509 -->
<div class="space-y-4">
<h3 class="font-semibold text-sm">Tasas Pedimento - Registro 509</h3>
<div class="space-y-2">
<Label for="contribucion">Contribución *</Label>
<Label for="contribucion">Contribución:</Label>
<Select.Root
type="single"
value={currentContribucion.contribucion}
onValueChange={(v: string | undefined) => v && (currentContribucion.contribucion = v)}
>
<Select.Trigger id="contribucion">
<span class="truncate">{currentContribucion.contribucion || 'Seleccionar contribución'}</span>
<span class="truncate">{currentContribucion.contribucion || 'Seleccionar'}</span>
</Select.Trigger>
<Select.Content>
{#each opcionesContribuciones as opcion}
@@ -553,150 +604,169 @@
</Select.Content>
</Select.Root>
</div>
<div class="space-y-2">
<Label for="tipo_tasa">T.T. (Tipo de Tasa) *</Label>
<Input id="tipo_tasa" bind:value={currentContribucion.tipo_tasa} placeholder="Tipo" />
<Label for="tipo_tasa">Tipo de Tasa:</Label>
<div class="flex gap-2">
<Select.Root
type="single"
value={currentContribucion.tipo_tasa}
onValueChange={(v: string | undefined) => v && (currentContribucion.tipo_tasa = v)}
>
<Select.Trigger id="tipo_tasa" class="flex-1">
<span class="truncate">{currentContribucion.tipo_tasa || 'Seleccionar'}</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="Porcentual">Porcentual</Select.Item>
<Select.Item value="Especifico">Específico</Select.Item>
<Select.Item value="CuotaMinimaDTA">Cuota minima (DTA)</Select.Item>
<Select.Item value="CuotaFijaDTA">Cuota fija (DTA)</Select.Item>
<Select.Item value="TasaDescuentoAdValorem">Tasa de descuento sobre ad valorem</Select.Item>
<Select.Item value="FactorAplicacionTIGIE">Factor de aplicación sobre tigie.</Select.Item>
<Select.Item value="AlMillarDTA">Al millar (DTA)</Select.Item>
<Select.Item value="TasaDescuentoArancelEspecifico">Tasa de descuento sobre el arancel específico</Select.Item>
<Select.Item value="TasaEspecificaPreciosReferencia">Tasa especifica sobre precios de referencia</Select.Item>
<Select.Item value="TasaEspecificaPreciosReferenciaUM">Tasa especifica sobre precios de referencia con UM</Select.Item>
</Select.Content>
</Select.Root>
<Button size="icon" variant="outline">
<Plus class="h-4 w-4" />
</Button>
</div>
</div>
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="tasa">Tasa *</Label>
<Input
id="tasa"
type="number"
step="0.00001"
bind:value={currentContribucion.tasa}
placeholder="0.00000"
/>
</div>
<div class="space-y-2">
<Label for="forma_pago">F.P. (Forma de Pago) *</Label>
<Input id="forma_pago" bind:value={currentContribucion.forma_pago} placeholder="FP" />
</div>
<div class="space-y-2">
<Label for="importe">Importe *</Label>
<Input
id="importe"
type="number"
step="0.01"
bind:value={currentContribucion.importe}
placeholder="0.00"
/>
</div>
</div>
<div class="space-y-2">
<Label for="tasa">Tasa:</Label>
<Input
id="tasa"
step="0.00001"
bind:value={currentContribucion.tasa}
placeholder="0.00000"
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="gravamen">Gravamen</Label>
<Input id="gravamen" bind:value={currentContribucion.gravamen} placeholder="Gravamen" />
</div>
<div class="space-y-2">
<Label for="abreviacion">Abreviación</Label>
<Input
id="abreviacion"
bind:value={currentContribucion.abreviacion}
placeholder="Abreviación"
/>
</div>
</div>
<!-- Contribuciones Generales - Registro 510 -->
<div class="space-y-4">
<h3 class="font-semibold text-sm">Contribuciones Generales - Registro 510</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="forma_pago_2">F.P. 2</Label>
<Input id="forma_pago_2" bind:value={currentContribucion.forma_pago_2} placeholder="FP" />
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Clave Contribución</TableHead>
<TableHead>Forma de Pago</TableHead>
<TableHead class="text-right">Importe</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#if !currentContribucion.contribuciones_generales || currentContribucion.contribuciones_generales.length === 0}
<TableRow>
<TableCell colspan={3} class="text-center text-muted-foreground py-4">
Sin registros
</TableCell>
</TableRow>
{:else}
{#each currentContribucion.contribuciones_generales as contribGen, index}
<TableRow>
<TableCell>{contribGen.clave_contribucion}</TableCell>
<TableCell>{contribGen.clave_forma_pago}</TableCell>
<TableCell class="text-right">
{contribGen.importe_contribucion.toLocaleString('es-MX', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}
</TableCell>
</TableRow>
{/each}
{/if}
</TableBody>
</Table>
</div>
<div class="space-y-2">
<Label for="importe_2">Importe 2</Label>
<Input
id="importe_2"
type="number"
step="0.01"
bind:value={currentContribucion.importe_2}
placeholder="0.00"
/>
<div class="flex gap-2">
<Button size="sm" variant="outline" onclick={openNewContribGenInDialog}>
<Plus class="h-4 w-4 mr-1" /> Nuevo
</Button>
<Button size="sm" variant="outline" disabled>
<Pencil class="h-4 w-4 mr-1" /> Editar
</Button>
<Button size="sm" variant="outline" disabled>
<Trash2 class="h-4 w-4 mr-1" /> Borrar
</Button>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onclick={() => (isDialogOpen = false)}>Cancelar</Button>
<Button
onclick={saveContribucion}
disabled={!currentContribucion.contribucion ||
!currentContribucion.tipo_tasa ||
!currentContribucion.forma_pago}
>
{editingIndex !== null ? 'Actualizar' : 'Guardar'}
</Button>
<Button onclick={saveContribucion}>Aceptar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Dialog para Contribución General (510) -->
<!-- Dialog para Contribución General (510) dentro del diálogo de Contribución -->
<Dialog bind:open={isContribGenDialogOpen}>
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>
{editingContribGenIndex !== null ? 'Editar Contribución General' : 'Nueva Contribución General'}
</DialogTitle>
<DialogTitle>Insertando</DialogTitle>
</DialogHeader>
<div class="space-y-4 py-4">
<div class="space-y-2">
<Label for="pedimento_sys_id">PedimentoSysID:</Label>
<Label for="pedimento_sys_id_temp">PedimentoSysID:</Label>
<Input
id="pedimento_sys_id"
bind:value={currentContribGen.pedimento_sys_id}
placeholder="39,637"
id="pedimento_sys_id_temp"
bind:value={tempContribGen.pedimento_sys_id}
placeholder="39,641"
/>
</div>
<div class="space-y-2">
<Label for="clave_contribucion_gen">Clave de Contribución:</Label>
<Label for="clave_contribucion_temp">Clave de Contribución:</Label>
<Input
id="clave_contribucion_gen"
bind:value={currentContribGen.clave_contribucion}
id="clave_contribucion_temp"
bind:value={tempContribGen.clave_contribucion}
placeholder="0"
/>
</div>
<div class="space-y-2">
<Label for="clave_tasa">Clave de Tasa:</Label>
<Label for="clave_tasa_temp">Clave de Tasa:</Label>
<Input
id="clave_tasa"
bind:value={currentContribGen.clave_tasa}
id="clave_tasa_temp"
bind:value={tempContribGen.clave_tasa}
placeholder=""
/>
</div>
<div class="space-y-2">
<Label for="tasa_contribucion">Tasa de Contribución:</Label>
<Label for="tasa_contribucion_temp">Tasa de Contribución:</Label>
<Input
id="tasa_contribucion"
id="tasa_contribucion_temp"
type="number"
step="0.00001"
bind:value={currentContribGen.tasa_contribucion}
bind:value={tempContribGen.tasa_contribucion}
placeholder="0.00000"
/>
</div>
<div class="space-y-2">
<Label for="clave_forma_pago_gen">Clave Forma de Pago:</Label>
<Label for="clave_forma_pago_temp">Clave Forma de Pago:</Label>
<Input
id="clave_forma_pago_gen"
bind:value={currentContribGen.clave_forma_pago}
id="clave_forma_pago_temp"
bind:value={tempContribGen.clave_forma_pago}
placeholder="0"
/>
</div>
<div class="space-y-2">
<Label for="importe_contribucion_gen">Importe de Contribución:</Label>
<Label for="importe_contribucion_temp">Importe de Contribución:</Label>
<Input
id="importe_contribucion_gen"
id="importe_contribucion_temp"
type="number"
step="0.01"
bind:value={currentContribGen.importe_contribucion}
bind:value={tempContribGen.importe_contribucion}
placeholder="0"
/>
</div>
@@ -704,12 +774,7 @@
<DialogFooter>
<Button variant="outline" onclick={() => (isContribGenDialogOpen = false)}>Cancelar</Button>
<Button
onclick={saveContribGen}
disabled={!currentContribGen.clave_contribucion}
>
{editingContribGenIndex !== null ? 'Actualizar' : 'Guardar'}
</Button>
<Button onclick={saveContribGenInDialog}>Aceptar</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -27,6 +27,7 @@
Copy,
Upload
} from 'lucide-svelte';
import { onMount } from 'svelte';
interface Transporte {
id?: number;
@@ -115,12 +116,276 @@
};
});
// Cargar países al montar el componente
onMount(async () => {
try {
console.log('🌍 Cargando todos los países desde la base de datos...');
const response = await fetch('/api-sveltekit/countries');
if (!response.ok) {
console.error('❌ Error al cargar países:', response.status);
return;
}
const data = await response.json();
countries = data;
console.log('✅ Países cargados:', countries.length);
} catch (error) {
console.error('❌ Error al cargar países:', error);
}
});
// Estados para diálogos
let isTransporteDialogOpen = $state(false);
let isPrecintoDialogOpen = $state(false);
let isContenedorDialogOpen = $state(false);
let isTransportesPrecintosDialogOpen = $state(false);
let isGuiasDialogOpen = $state(false);
let isTransportistasDialogOpen = $state(false);
let isTransportistaFormDialogOpen = $state(false);
let isGafeteDialogOpen = $state(false);
let editingGafeteIndex = $state<number | null>(null);
// Estado para gafete actual
let currentGafete = $state({
claveTransportista: '',
linea: '',
numeroGafeteUnico: '',
nombreTransportista: ''
});
// Catálogo de transportistas (datos del catálogo del sistema)
let transportistasCatalog = $state<any[]>([
{
id: 1,
clave: '159A',
nombre: 'MANUEL ALEJANDRO VENGA',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: 'MEX',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 2,
clave: '159T',
nombre: 'TRANSPORTES HERMA',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 3,
clave: '159V',
nombre: 'Concepción Peña Grijalva',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 4,
clave: '158A',
nombre: 'PERALTA',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 5,
clave: '15C1',
nombre: 'MOLSA MOVIMIENTO LOGISTI',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 6,
clave: '15CG',
nombre: 'UNION DE FLETEROS DE TAN',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 7,
clave: '15D5',
nombre: 'Carlos Alejandro Baeta Sánchez',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 8,
clave: '15E6',
nombre: 'JUAN SEBASTIAN MERA',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 9,
clave: '15FB',
nombre: 'FLORENTIN N HERNANDEZ',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 10,
clave: '15G0',
nombre: 'JOSE GABRIEL ZEPEDA DIAZ',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 11,
clave: '15G2',
nombre: 'PERALTA',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 12,
clave: '15I9',
nombre: 'Blanca Fabiola Figueroa Mende',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: 'MEX',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 13,
clave: '15JD',
nombre: 'José García Cruz',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
},
{
id: 14,
clave: '15JF',
nombre: 'TRANSPORTES ESPAÑA',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: '',
telefono: '',
caat_code: '',
gafetes: []
}
]);
let searchTransportista = $state('');
let currentCatalogoTransportista = $state<any | null>(null);
let editingTransportistaIndex = $state<number | null>(null);
// Filtrar transportistas según búsqueda
let filteredTransportistas = $derived(
transportistasCatalog.filter((t) => {
if (!searchTransportista) return true;
const search = searchTransportista.toLowerCase();
return (
t.clave?.toLowerCase().includes(search) ||
t.nombre?.toLowerCase().includes(search) ||
t.rfc?.toLowerCase().includes(search)
);
})
);
// Lista de países desde el backend
let countries = $state<Array<{m3_key: string; description_es: string}>>([]);
let searchCountry = $state('');
let showCountryDropdown = $state(false);
// Filtrar países según búsqueda
let filteredCountries = $derived(
countries.filter((c) => {
if (!searchCountry) return true;
const search = searchCountry.toLowerCase();
return (
c.description_es?.toLowerCase().includes(search) ||
c.m3_key?.toLowerCase().includes(search)
);
})
);
// Estados para diálogo de Transportes y Precintos separados por comas
let transportesPrecintosData = $state({
@@ -181,38 +446,38 @@
];
const estadosMexico = [
'Aguascalientes',
'Baja California',
'Baja California Sur',
'Campeche',
'Chiapas',
'Chihuahua',
'Ciudad de México',
'Coahuila',
'Colima',
'Durango',
'Guanajuato',
'Guerrero',
'Hidalgo',
'Jalisco',
'México',
'Michoacán',
'Morelos',
'Nayarit',
'Nuevo León',
'Oaxaca',
'Puebla',
'Querétaro',
'Quintana Roo',
'San Luis Potosí',
'Sinaloa',
'Sonora',
'Tabasco',
'Tamaulipas',
'Tlaxcala',
'Veracruz',
'Yucatán',
'Zacatecas'
{ clave: 'AGS', nombre: 'Aguascalientes' },
{ clave: 'BC', nombre: 'Baja California' },
{ clave: 'BCS', nombre: 'Baja California Sur' },
{ clave: 'CAM', nombre: 'Campeche' },
{ clave: 'CHS', nombre: 'Chiapas' },
{ clave: 'CHI', nombre: 'Chihuahua' },
{ clave: 'CDMX', nombre: 'Ciudad de México' },
{ clave: 'COA', nombre: 'Coahuila' },
{ clave: 'COL', nombre: 'Colima' },
{ clave: 'DGO', nombre: 'Durango' },
{ clave: 'GTO', nombre: 'Guanajuato' },
{ clave: 'GRO', nombre: 'Guerrero' },
{ clave: 'HGO', nombre: 'Hidalgo' },
{ clave: 'JAL', nombre: 'Jalisco' },
{ clave: 'MEX', nombre: 'México' },
{ clave: 'MIC', nombre: 'Michoacán' },
{ clave: 'MOR', nombre: 'Morelos' },
{ clave: 'NAY', nombre: 'Nayarit' },
{ clave: 'NL', nombre: 'Nuevo León' },
{ clave: 'OAX', nombre: 'Oaxaca' },
{ clave: 'PUE', nombre: 'Puebla' },
{ clave: 'QRO', nombre: 'Querétaro' },
{ clave: 'QR', nombre: 'Quintana Roo' },
{ clave: 'SLP', nombre: 'San Luis Potosí' },
{ clave: 'SIN', nombre: 'Sinaloa' },
{ clave: 'SON', nombre: 'Sonora' },
{ clave: 'TAB', nombre: 'Tabasco' },
{ clave: 'TAM', nombre: 'Tamaulipas' },
{ clave: 'TLX', nombre: 'Tlaxcala' },
{ clave: 'VER', nombre: 'Veracruz' },
{ clave: 'YUC', nombre: 'Yucatán' },
{ clave: 'ZAC', nombre: 'Zacatecas' }
];
const tiposContenedor = [
@@ -328,7 +593,13 @@
}
function saveGuia() {
if (!formData?.guias) return;
if (!formData) return;
// Asegurar que existe el array de guías
if (!formData.guias) {
formData.guias = [];
}
if (editingGuiaIndex !== null) {
formData.guias[editingGuiaIndex] = { ...currentGuia };
} else {
@@ -402,6 +673,119 @@
}
}
// Funciones para gestionar catálogo de transportistas
function openNewTransportistaForm() {
currentCatalogoTransportista = {
clave: '',
nombre: '',
rfc: '',
curp: '',
domicilio: '',
ciudad: '',
estado: '',
pais: ''
};
editingTransportistaIndex = null;
isTransportistaFormDialogOpen = true;
}
function editCatalogoTransportista(index: number) {
currentCatalogoTransportista = { ...transportistasCatalog[index] };
editingTransportistaIndex = index;
isTransportistaFormDialogOpen = true;
}
function saveCatalogoTransportista() {
if (editingTransportistaIndex !== null) {
transportistasCatalog[editingTransportistaIndex] = { ...currentCatalogoTransportista };
} else {
transportistasCatalog.push({ ...currentCatalogoTransportista, id: Date.now() });
}
isTransportistaFormDialogOpen = false;
currentCatalogoTransportista = null;
editingTransportistaIndex = null;
}
function deleteCatalogoTransportista(index: number) {
if (confirm('¿Está seguro de eliminar este transportista?')) {
transportistasCatalog.splice(index, 1);
}
}
function selectTransportista(transportista: any) {
currentTransporte.transportista = transportista.nombre;
currentTransporte.rfc = transportista.rfc;
currentTransporte.curp = transportista.curp || '';
isTransportistasDialogOpen = false;
}
// Funciones para gafetes
function openNewGafeteDialog() {
editingGafeteIndex = null;
currentGafete = {
claveTransportista: currentCatalogoTransportista?.clave || '',
linea: '0',
numeroGafeteUnico: '',
nombreTransportista: currentCatalogoTransportista?.nombre || ''
};
isGafeteDialogOpen = true;
}
function editGafete(index: number) {
if (!currentCatalogoTransportista?.gafetes) return;
editingGafeteIndex = index;
const gafete = currentCatalogoTransportista.gafetes[index];
currentGafete = {
claveTransportista: currentCatalogoTransportista.clave || '',
linea: gafete.linea.toString(),
numeroGafeteUnico: gafete.numeroGafeteUnico,
nombreTransportista: gafete.nombreTransportista
};
isGafeteDialogOpen = true;
}
function deleteGafete(index: number) {
if (!currentCatalogoTransportista?.gafetes) return;
if (!confirm('¿Está seguro de que desea borrar este gafete?')) {
return;
}
currentCatalogoTransportista.gafetes.splice(index, 1);
// Reordenar las líneas
currentCatalogoTransportista.gafetes.forEach((gafete: any, i: number) => {
gafete.linea = i + 1;
});
}
function saveGafete() {
if (!currentCatalogoTransportista) return;
// Asegurar que existe el array de gafetes
if (!currentCatalogoTransportista.gafetes) {
currentCatalogoTransportista.gafetes = [];
}
if (editingGafeteIndex !== null) {
// Editar gafete existente
currentCatalogoTransportista.gafetes[editingGafeteIndex] = {
linea: currentCatalogoTransportista.gafetes[editingGafeteIndex].linea,
numeroGafeteUnico: currentGafete.numeroGafeteUnico,
nombreTransportista: currentGafete.nombreTransportista
};
} else {
// Agregar nuevo gafete
currentCatalogoTransportista.gafetes.push({
linea: currentCatalogoTransportista.gafetes.length + 1,
numeroGafeteUnico: currentGafete.numeroGafeteUnico,
nombreTransportista: currentGafete.nombreTransportista
});
}
editingGafeteIndex = null;
isGafeteDialogOpen = false;
}
// Funciones para Contenedores
function openNewContenedor() {
editingContenedorIndex = null;
@@ -539,7 +923,7 @@
{:else}
{#each formData.transportes as transporte, index}
<TableRow>
<TableCell>{transporte.identificacion}</TableCell>
<TableCell>{transporte.identificacion_fiscal}</TableCell>
<TableCell>{transporte.pais}</TableCell>
<TableCell>
<div class="flex gap-1">
@@ -844,11 +1228,17 @@
<div class="grid gap-4 py-4">
<div class="space-y-2">
<Label for="t_transportista">Transportista:</Label>
<Input
id="t_transportista"
bind:value={currentTransporte.transportista}
placeholder="Transportista"
/>
<div class="flex gap-2">
<Input
id="t_transportista"
bind:value={currentTransporte.transportista}
placeholder="Transportista"
class="flex-1"
/>
<Button size="icon" variant="outline" onclick={() => (isTransportistasDialogOpen = true)}>
<FileText size={16} />
</Button>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
@@ -889,24 +1279,52 @@
onValueChange={(v: string | undefined) => v && (currentTransporte.estado = v)}
>
<Select.Trigger id="t_estado" class="w-full">
<span class="truncate">{currentTransporte.estado || 'Seleccionar estado'}</span>
<span class="truncate">{currentTransporte.estado ? estadosMexico.find(e => e.clave === currentTransporte.estado)?.nombre || currentTransporte.estado : 'Seleccionar estado'}</span>
</Select.Trigger>
<Select.Content>
{#each estadosMexico as estado}
<Select.Item value={estado}>{estado}</Select.Item>
<Select.Item value={estado.clave}>{estado.clave} - {estado.nombre}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<div class="space-y-2">
<div class="space-y-2 relative">
<Label for="t_pais">País:</Label>
<div class="flex gap-2">
<Input id="t_pais" bind:value={currentTransporte.pais} placeholder="País" class="flex-1" />
<Button size="icon" variant="outline" onclick={() => alert('País en común')}>
<FileText size={16} />
</Button>
<div class="relative">
<Input
id="t_pais"
type="text"
placeholder="Buscar país..."
value={searchCountry}
oninput={(e) => (searchCountry = (e.target as HTMLInputElement).value)}
onfocus={() => (showCountryDropdown = true)}
onblur={() => setTimeout(() => (showCountryDropdown = false), 200)}
class="w-full"
/>
{#if currentTransporte.pais && !searchCountry}
<div class="absolute inset-0 pointer-events-none flex items-center px-3 text-sm">
<span class="truncate">{countries.find(c => c.m3_key === currentTransporte.pais)?.description_es || currentTransporte.pais}</span>
</div>
{/if}
{#if showCountryDropdown && filteredCountries.length > 0}
<div class="absolute z-50 w-full mt-1 bg-popover border border-border rounded-md shadow-lg max-h-[300px] overflow-auto">
{#each filteredCountries as country}
<button
type="button"
class="w-full text-left px-3 py-2 hover:bg-accent hover:text-accent-foreground text-sm text-popover-foreground transition-colors"
onclick={() => {
currentTransporte.pais = country.m3_key;
searchCountry = '';
showCountryDropdown = false;
}}
>
{country.description_es}
</button>
{/each}
</div>
{/if}
</div>
</div>
@@ -1098,6 +1516,291 @@
</DialogContent>
</Dialog>
<!-- Diálogo Catálogo de Transportistas -->
<Dialog open={isTransportistasDialogOpen} onOpenChange={(open) => (isTransportistasDialogOpen = open)}>
<DialogContent class="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Catálogo de Transportistas</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div class="flex gap-2">
<div class="flex-1 space-y-2">
<Label for="search-transportista">Buscar:</Label>
<Input
id="search-transportista"
bind:value={searchTransportista}
placeholder="Buscar por clave, nombre o RFC..."
/>
</div>
<div class="flex items-end">
<Button onclick={openNewTransportistaForm}>
<Plus size={16} class="mr-2" />
Nuevo
</Button>
</div>
</div>
<div class="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Clave</TableHead>
<TableHead>Nombre</TableHead>
<TableHead>RFC</TableHead>
<TableHead class="text-center">Acciones</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#if filteredTransportistas.length === 0}
<TableRow>
<TableCell colspan={4} class="text-center text-muted-foreground">
{searchTransportista ? 'No se encontraron transportistas con ese criterio.' : 'No hay transportistas en el catálogo. Haz clic en "Nuevo" para agregar.'}
</TableCell>
</TableRow>
{:else}
{#each filteredTransportistas as transportista, index}
<TableRow>
<TableCell>{transportista.clave}</TableCell>
<TableCell>{transportista.nombre}</TableCell>
<TableCell>{transportista.rfc}</TableCell>
<TableCell>
<div class="flex justify-center gap-1">
<Button
size="icon"
variant="ghost"
onclick={() => selectTransportista(transportista)}
title="Seleccionar"
>
<FileText size={16} />
</Button>
<Button
size="icon"
variant="ghost"
onclick={() => editCatalogoTransportista(transportistasCatalog.indexOf(transportista))}
title="Editar"
>
<Pencil size={16} />
</Button>
<Button
size="icon"
variant="ghost"
onclick={() => deleteCatalogoTransportista(transportistasCatalog.indexOf(transportista))}
title="Eliminar"
>
<Trash2 size={16} />
</Button>
</div>
</TableCell>
</TableRow>
{/each}
{/if}
</TableBody>
</Table>
</div>
</div>
<DialogFooter class="flex justify-end gap-2 mt-6">
<Button variant="outline" onclick={() => (isTransportistasDialogOpen = false)}>
Cerrar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Diálogo Formulario de Transportista -->
<Dialog open={isTransportistaFormDialogOpen} onOpenChange={(open) => (isTransportistaFormDialogOpen = open)}>
<DialogContent class="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{editingTransportistaIndex !== null ? 'Editar' : 'Nuevo'} Transportista</DialogTitle>
</DialogHeader>
{#if currentCatalogoTransportista}
<div class="grid gap-4 py-4">
<!-- Clave y Nombre -->
<div class="space-y-2">
<Label for="ct_clave">Clave:</Label>
<Input
id="ct_clave"
bind:value={currentCatalogoTransportista.clave}
placeholder="Clave"
/>
</div>
<div class="space-y-2">
<Label for="ct_nombre">Nombre:</Label>
<Input
id="ct_nombre"
bind:value={currentCatalogoTransportista.nombre}
placeholder="Nombre completo"
/>
</div>
<!-- RFC y CURP -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="ct_rfc">RFC:</Label>
<Input
id="ct_rfc"
bind:value={currentCatalogoTransportista.rfc}
placeholder="RFC"
/>
</div>
<div class="space-y-2">
<Label for="ct_curp">CURP:</Label>
<Input
id="ct_curp"
bind:value={currentCatalogoTransportista.curp}
placeholder="CURP"
/>
</div>
</div>
<!-- Domicilio -->
<div class="space-y-2">
<Label for="ct_domicilio">Domicilio:</Label>
<Input
id="ct_domicilio"
bind:value={currentCatalogoTransportista.domicilio}
placeholder="Domicilio"
/>
</div>
<!-- País y Entidad Federativa -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2 relative">
<Label for="ct_pais">País:</Label>
<Select.Root
type="single"
value={currentCatalogoTransportista.pais}
onValueChange={(v: string | undefined) => v && (currentCatalogoTransportista.pais = v)}
>
<Select.Trigger id="ct_pais" class="w-full">
<span class="truncate">{currentCatalogoTransportista.pais ? countries.find(c => c.m3_key === currentCatalogoTransportista.pais)?.description_es || currentCatalogoTransportista.pais : 'Seleccionar país'}</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#if countries.length === 0}
<Select.Item value="" disabled>Cargando países...</Select.Item>
{:else}
{#each countries as country}
<Select.Item value={country.m3_key}>{country.description_es}</Select.Item>
{/each}
{/if}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-2">
<Label for="ct_estado">Entidad Federativa:</Label>
<Select.Root
type="single"
value={currentCatalogoTransportista.estado}
onValueChange={(v: string | undefined) => v && (currentCatalogoTransportista.estado = v)}
>
<Select.Trigger id="ct_estado" class="w-full">
<span class="truncate">{currentCatalogoTransportista.estado ? estadosMexico.find(e => e.clave === currentCatalogoTransportista.estado)?.nombre || currentCatalogoTransportista.estado : 'Seleccionar estado'}</span>
</Select.Trigger>
<Select.Content class="max-h-[300px]">
{#each estadosMexico as estado}
<Select.Item value={estado.clave}>{estado.clave} - {estado.nombre}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
<!-- Ciudad -->
<div class="space-y-2">
<Label for="ct_ciudad">Ciudad:</Label>
<Input
id="ct_ciudad"
bind:value={currentCatalogoTransportista.ciudad}
placeholder="Ciudad"
/>
</div>
<!-- Teléfono y CAAT -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="ct_telefono">Teléfono:</Label>
<Input
id="ct_telefono"
bind:value={currentCatalogoTransportista.telefono}
placeholder="Teléfono"
/>
</div>
<div class="space-y-2">
<Label for="ct_caat">CAAT:</Label>
<Input
id="ct_caat"
bind:value={currentCatalogoTransportista.caat_code}
placeholder="(Código Alfanumérico Armonizado de Transportista)"
/>
</div>
</div>
<!-- Sección DE GAFETES -->
<div class="border-t pt-4 mt-4">
<div class="flex justify-between items-center mb-2">
<h3 class="text-lg font-semibold"># DE GAFETES</h3>
<div class="flex gap-2">
<Button size="sm" variant="outline" onclick={openNewGafeteDialog}>
<Plus class="h-4 w-4 mr-1" /> Nuevo
</Button>
</div>
</div>
<div class="border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead>Línea</TableHead>
<TableHead>Número Gafete Único</TableHead>
<TableHead>Nombre</TableHead>
<TableHead class="text-center">Acciones</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#if currentCatalogoTransportista.gafetes && currentCatalogoTransportista.gafetes.length > 0}
{#each currentCatalogoTransportista.gafetes as gafete, index}
<TableRow>
<TableCell>{gafete.linea}</TableCell>
<TableCell>{gafete.numeroGafeteUnico}</TableCell>
<TableCell>{gafete.nombreTransportista}</TableCell>
<TableCell class="text-center">
<div class="flex gap-1 justify-center">
<Button size="icon" variant="ghost" onclick={() => editGafete(index)}>
<Pencil class="h-4 w-4" />
</Button>
<Button size="icon" variant="ghost" onclick={() => deleteGafete(index)}>
<Trash2 class="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
{/each}
{:else}
<TableRow>
<TableCell colspan={4} class="text-center text-muted-foreground">
Sin gafetes registrados
</TableCell>
</TableRow>
{/if}
</TableBody>
</Table>
</div>
<div class="mt-2">
<Label for="buscar_gafete">Buscar:</Label>
<Input
id="buscar_gafete"
placeholder="Buscar gafete..."
/>
</div>
</div>
</div>
{/if}
<DialogFooter class="flex justify-end gap-2 mt-6">
<Button variant="outline" onclick={() => (isTransportistaFormDialogOpen = false)}>Cancelar</Button>
<Button onclick={saveCatalogoTransportista}>Guardar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Diálogo de Guías de Pedimento -->
<Dialog open={isGuiasDialogOpen} onOpenChange={(open) => (isGuiasDialogOpen = open)}>
<DialogContent class="max-w-md">
@@ -1137,3 +1840,57 @@
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Diálogo de Gafetes Únicos -->
<Dialog open={isGafeteDialogOpen} onOpenChange={(open) => (isGafeteDialogOpen = open)}>
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>GAFETES UNICOS</DialogTitle>
</DialogHeader>
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="gafete_clave_transportista">Clave Transportista:</Label>
<Input
id="gafete_clave_transportista"
value={currentGafete.claveTransportista}
placeholder="Clave"
disabled
/>
</div>
<div class="space-y-2">
<Label for="gafete_linea">Línea:</Label>
<Input
id="gafete_linea"
bind:value={currentGafete.linea}
placeholder="0"
/>
</div>
</div>
<div class="space-y-2">
<Label for="gafete_numero">Número Gafete Único:</Label>
<Input
id="gafete_numero"
bind:value={currentGafete.numeroGafeteUnico}
placeholder="Número de gafete"
/>
</div>
<div class="space-y-2">
<Label for="gafete_nombre">Nombre Transportista:</Label>
<Input
id="gafete_nombre"
value={currentGafete.nombreTransportista}
placeholder="Nombre"
disabled
/>
</div>
</div>
<DialogFooter class="flex justify-end gap-2 mt-6">
<Button variant="outline" onclick={() => (isGafeteDialogOpen = false)}>Cancelar</Button>
<Button onclick={saveGafete}>Aceptar</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -0,0 +1,771 @@
<script lang="ts">
import { Card, CardContent } from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '$lib/components/ui/table';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter
} from '$lib/components/ui/dialog';
import { Checkbox } from '$lib/components/ui/checkbox';
import {
Plus,
Pencil,
Trash2
} from 'lucide-svelte';
const opcionesContribuciones = [
'DTA ',
'REC G',
'OTROS',
'DTI',
'MULT',
'RT',
'PRV',
'REU',
'ECI',
'IVA PRV',
'DFC'
];
interface Contribucion {
id?: number;
contribucion: string;
tipo_tasa: string;
tasa: number;
forma_pago: string;
importe: number;
gravamen: string;
abreviacion: string;
forma_pago_2: string;
importe_2: number;
contribuciones_generales?: Array<{
pedimento_sys_id: string;
clave_contribucion: string;
clave_tasa: string;
tasa_contribucion: number;
clave_forma_pago: string;
importe_contribucion: number;
}>;
}
interface ContribucionGeneral {
id?: number;
pedimento_sys_id: string;
clave_contribucion: string;
clave_tasa: string;
tasa_contribucion: number;
clave_forma_pago: string;
importe_contribucion: number;
}
let {
formData = $bindable({
// Campos por pestaña
dta: {
forma_pago_recargo: 0,
tasa_recargo: 0.0,
importe_recargo: 0
},
prev: {
forma_pago_prevalidacion: 0
},
eci: {
forma_pago_eci: 0,
importe_eci: 0
},
mult: {
forma_pago_multa: 0,
importe_multa: 0
},
rec: {
forma_pago_uia: 0,
importe_compensar: 0
},
// Configuración
calculo_manual: false,
operaciones_regla_31_40: false,
// Contribuciones (tabla compartida)
contribuciones: [] as Contribucion[],
contribuciones_generales: [] as ContribucionGeneral[]
})
}: {
formData: {
dta: {
forma_pago_recargo: number;
tasa_recargo: number;
importe_recargo: number;
};
prev: {
forma_pago_prevalidacion: number;
};
eci: {
forma_pago_eci: number;
importe_eci: number;
};
mult: {
forma_pago_multa: number;
importe_multa: number;
};
rec: {
forma_pago_uia: number;
importe_compensar: number;
};
calculo_manual: boolean;
operaciones_regla_31_40: boolean;
contribuciones: Contribucion[];
contribuciones_generales: ContribucionGeneral[];
};
} = $props();
// Pestaña activa
let activeTab = $state('DTA');
// Estados para diálogo
let isDialogOpen = $state(false);
let editingIndex = $state<number | null>(null);
// Estados para contribuciones generales (dentro del diálogo)
let isContribGenDialogOpen = $state(false);
let editingContribGenIndex = $state<number | null>(null);
// Estado temporal para contribución general dentro del diálogo de contribución
let tempContribGen = $state({
pedimento_sys_id: '',
clave_contribucion: '',
clave_tasa: '',
tasa_contribucion: 0,
clave_forma_pago: '',
importe_contribucion: 0
});
let currentContribucion = $state<Contribucion>({
contribucion: '',
tipo_tasa: '',
tasa: 0,
forma_pago: '',
importe: 0,
gravamen: '',
abreviacion: '',
forma_pago_2: '',
importe_2: 0,
contribuciones_generales: []
});
let currentContribGen = $state<ContribucionGeneral>({
pedimento_sys_id: '',
clave_contribucion: '',
clave_tasa: '',
tasa_contribucion: 0,
clave_forma_pago: '',
importe_contribucion: 0
});
function openNewContribucion() {
editingIndex = null;
currentContribucion = {
contribucion: '',
tipo_tasa: '',
tasa: 0,
forma_pago: '',
importe: 0,
gravamen: '',
abreviacion: '',
forma_pago_2: '',
importe_2: 0,
contribuciones_generales: []
};
isDialogOpen = true;
}
function openEditContribucion(index: number) {
if (!formData?.contribuciones) return;
editingIndex = index;
currentContribucion = { ...formData.contribuciones[index] };
isDialogOpen = true;
}
function saveContribucion() {
if (!formData?.contribuciones) return;
if (editingIndex !== null) {
formData.contribuciones[editingIndex] = { ...currentContribucion };
} else {
formData.contribuciones = [...formData.contribuciones, { ...currentContribucion }];
}
isDialogOpen = false;
}
function deleteContribucion(index: number) {
if (!formData?.contribuciones) return;
if (confirm('¿Está seguro de eliminar esta contribución?')) {
formData.contribuciones = formData.contribuciones.filter((_, i) => i !== index);
}
}
// Funciones para Contribuciones Generales (Registro 510) dentro del diálogo de contribución
function openNewContribGenInDialog() {
tempContribGen = {
pedimento_sys_id: '',
clave_contribucion: '',
clave_tasa: '',
tasa_contribucion: 0,
clave_forma_pago: '',
importe_contribucion: 0
};
isContribGenDialogOpen = true;
}
function saveContribGenInDialog() {
if (!currentContribucion.contribuciones_generales) {
currentContribucion.contribuciones_generales = [];
}
currentContribucion.contribuciones_generales.push({
pedimento_sys_id: tempContribGen.pedimento_sys_id,
clave_contribucion: tempContribGen.clave_contribucion,
clave_tasa: tempContribGen.clave_tasa,
tasa_contribucion: tempContribGen.tasa_contribucion,
clave_forma_pago: tempContribGen.clave_forma_pago,
importe_contribucion: tempContribGen.importe_contribucion
});
isContribGenDialogOpen = false;
}
// Funciones para Contribuciones Generales (Registro 510)
function openNewContribGen() {
editingContribGenIndex = null;
currentContribGen = {
pedimento_sys_id: '',
clave_contribucion: '',
clave_tasa: '',
tasa_contribucion: 0,
clave_forma_pago: '',
importe_contribucion: 0
};
isContribGenDialogOpen = true;
}
function openEditContribGen(index: number) {
if (!formData?.contribuciones_generales) return;
editingContribGenIndex = index;
currentContribGen = { ...formData.contribuciones_generales[index] };
isContribGenDialogOpen = true;
}
function saveContribGen() {
if (!formData?.contribuciones_generales) return;
if (editingContribGenIndex !== null) {
formData.contribuciones_generales[editingContribGenIndex] = { ...currentContribGen };
} else {
formData.contribuciones_generales = [...formData.contribuciones_generales, { ...currentContribGen }];
}
isContribGenDialogOpen = false;
}
function deleteContribGen(index: number) {
if (!formData?.contribuciones_generales) return;
if (confirm('¿Está seguro de eliminar esta contribución general?')) {
formData.contribuciones_generales = formData.contribuciones_generales.filter((_, i) => i !== index);
}
}
</script>
<Card>
<CardContent class="p-6 space-y-4">
<!-- Pestañas de navegación -->
<div class="flex gap-1 border-b">
<button
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors {activeTab === 'DTA'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'}"
onclick={() => (activeTab = 'DTA')}
>
DTA
</button>
<button
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors {activeTab === 'PREV'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'}"
onclick={() => (activeTab = 'PREV')}
>
PREV
</button>
<button
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors {activeTab === 'ECI'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'}"
onclick={() => (activeTab = 'ECI')}
>
ECI
</button>
<button
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors {activeTab === 'MULT'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'}"
onclick={() => (activeTab = 'MULT')}
>
MULT
</button>
<button
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors {activeTab === 'REC'
? 'border-primary text-primary'
: 'border-transparent text-muted-foreground hover:text-foreground'}"
onclick={() => (activeTab = 'REC')}
>
REC
</button>
</div>
<!-- Contenido dinámico según pestaña -->
<div class="border rounded-lg p-4 bg-muted/30">
{#if activeTab === 'DTA'}
<div class="grid grid-cols-3 gap-4">
<div class="space-y-2">
<Label for="forma_pago_recargo">Forma de pago Recargo:</Label>
<Input
id="forma_pago_recargo"
type="number"
bind:value={formData.dta.forma_pago_recargo}
placeholder="0"
/>
</div>
<div class="space-y-2">
<Label for="tasa_recargo">Tasa Recargo:</Label>
<Input
id="tasa_recargo"
type="number"
step="0.00001"
bind:value={formData.dta.tasa_recargo}
placeholder="0.00000"
/>
<p class="text-xs text-muted-foreground">(dejar en cero para calcular automático)</p>
</div>
<div class="space-y-2">
<Label for="importe_recargo">Importe Recargo:</Label>
<Input
id="importe_recargo"
type="number"
bind:value={formData.dta.importe_recargo}
placeholder="0"
/>
<p class="text-xs text-muted-foreground">(dejar en cero para calcular automático)</p>
</div>
</div>
{:else if activeTab === 'PREV'}
<div class="grid grid-cols-1 gap-4 max-w-md">
<div class="space-y-2">
<Label for="forma_pago_prevalidacion">Forma de pago Prevalidación:</Label>
<Input
id="forma_pago_prevalidacion"
type="number"
bind:value={formData.prev.forma_pago_prevalidacion}
placeholder="0"
/>
</div>
</div>
{:else if activeTab === 'ECI'}
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="forma_pago_eci">Forma de pago ECI:</Label>
<Input
id="forma_pago_eci"
type="number"
bind:value={formData.eci.forma_pago_eci}
placeholder="0"
/>
</div>
<div class="space-y-2">
<Label for="importe_eci">Importe ECI:</Label>
<Input
id="importe_eci"
type="number"
bind:value={formData.eci.importe_eci}
placeholder="0"
/>
<p class="text-xs text-muted-foreground">
(dejar en cero para que el sistema calcule el importe)
</p>
</div>
</div>
{:else if activeTab === 'MULT'}
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="forma_pago_multa">Forma de pago Multa:</Label>
<Input
id="forma_pago_multa"
type="number"
bind:value={formData.mult.forma_pago_multa}
placeholder="0"
/>
</div>
<div class="space-y-2">
<Label for="importe_multa">Importe Multa:</Label>
<Input
id="importe_multa"
type="number"
bind:value={formData.mult.importe_multa}
placeholder="0"
/>
</div>
</div>
{:else if activeTab === 'REC'}
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="forma_pago_uia">Forma de pago U I A:</Label>
<Input
id="forma_pago_uia"
type="number"
bind:value={formData.rec.forma_pago_uia}
placeholder="0"
/>
</div>
<div class="space-y-2">
<Label for="importe_compensar">Importe a Compensar:</Label>
<Input
id="importe_compensar"
type="number"
bind:value={formData.rec.importe_compensar}
placeholder="0"
/>
</div>
</div>
{/if}
</div>
<!-- Tabla de contribuciones -->
<div class="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Contribución</TableHead>
<TableHead>T.T.</TableHead>
<TableHead>Tasa</TableHead>
<TableHead>F.P.</TableHead>
<TableHead class="text-right">Importe</TableHead>
<TableHead>Gravamen</TableHead>
<TableHead>Abreviación</TableHead>
<TableHead>F.P.</TableHead>
<TableHead class="text-right">Importe</TableHead>
<TableHead class="w-[100px]">Acciones</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#if !formData?.contribuciones || formData.contribuciones.length === 0}
<TableRow>
<TableCell colspan={10} class="text-center text-muted-foreground py-8">
No hay contribuciones registradas
</TableCell>
</TableRow>
{:else}
{#each formData.contribuciones as contribucion, index}
<TableRow>
<TableCell>{contribucion.contribucion}</TableCell>
<TableCell>{contribucion.tipo_tasa}</TableCell>
<TableCell class="text-right"
>{contribucion.tasa.toLocaleString('es-MX', {
minimumFractionDigits: 2,
maximumFractionDigits: 5
})}</TableCell
>
<TableCell>{contribucion.forma_pago}</TableCell>
<TableCell class="text-right"
>{contribucion.importe.toLocaleString('es-MX', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}</TableCell
>
<TableCell>{contribucion.gravamen}</TableCell>
<TableCell>{contribucion.abreviacion}</TableCell>
<TableCell>{contribucion.forma_pago_2}</TableCell>
<TableCell class="text-right"
>{contribucion.importe_2.toLocaleString('es-MX', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})}</TableCell
>
<TableCell>
<div class="flex gap-1">
<Button size="sm" variant="ghost" onclick={() => openEditContribucion(index)}>
<Pencil size={14} />
</Button>
<Button size="sm" variant="ghost" onclick={() => deleteContribucion(index)}>
<Trash2 size={14} />
</Button>
</div>
</TableCell>
</TableRow>
{/each}
{/if}
</TableBody>
</Table>
</div>
<!-- Controles inferiores -->
<div class="flex items-center justify-between border-t pt-4">
<div class="flex items-center gap-6">
<div class="flex items-center space-x-2">
<Checkbox
id="calculo_manual"
checked={formData?.calculo_manual ?? false}
onCheckedChange={(checked: boolean | 'indeterminate') =>
formData && (formData.calculo_manual = checked === true)}
/>
<Label for="calculo_manual" class="font-normal cursor-pointer text-sm"
>Cálculo Manual</Label
>
</div>
<div class="flex items-center space-x-2">
<Checkbox
id="operaciones_regla"
checked={formData?.operaciones_regla_31_40 ?? false}
onCheckedChange={(checked: boolean | 'indeterminate') =>
formData && (formData.operaciones_regla_31_40 = checked === true)}
/>
<Label for="operaciones_regla" class="font-normal cursor-pointer text-sm"
>Operaciones al amparo de la regla 31.40</Label
>
</div>
</div>
<div class="flex gap-2">
<Button size="sm" onclick={openNewContribucion}>
<Plus class="mr-1.5" size={14} />
Nuevo
</Button>
<Button
size="sm"
variant="outline"
disabled={formData?.contribuciones?.length === 0}
onclick={() => {
const index = 0;
if (formData?.contribuciones?.[index]) openEditContribucion(index);
}}
>
<Pencil class="mr-1.5" size={14} />
Editar
</Button>
<Button
size="sm"
variant="destructive"
disabled={formData?.contribuciones?.length === 0}
onclick={() => {
const index = 0;
if (formData?.contribuciones?.[index]) deleteContribucion(index);
}}
>
<Trash2 class="mr-1.5" size={14} />
Borrar
</Button>
</div>
</div>
</CardContent>
</Card>
<!-- Dialog para Contribuciones -->
<Dialog bind:open={isDialogOpen}>
<DialogContent class="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
{editingIndex !== null ? 'Editar Contribución' : 'Nueva Contribución'}
</DialogTitle>
</DialogHeader>
<div class="space-y-6 py-4">
<!-- Tasas Pedimento - Registro 509 -->
<div class="space-y-4">
<h3 class="font-semibold text-sm">Tasas Pedimento - Registro 509</h3>
<div class="space-y-2">
<Label for="contribucion">Contribución:</Label>
<Select.Root
type="single"
value={currentContribucion.contribucion}
onValueChange={(v: string | undefined) => v && (currentContribucion.contribucion = v)}
>
<Select.Trigger id="contribucion">
<span class="truncate">{currentContribucion.contribucion || 'Seleccionar'}</span>
</Select.Trigger>
<Select.Content>
{#each opcionesContribuciones as opcion}
<Select.Item value={opcion}>{opcion}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="space-y-2">
<Label for="tipo_tasa">Tipo de Tasa:</Label>
<div class="flex gap-2">
<Select.Root
type="single"
value={currentContribucion.tipo_tasa}
onValueChange={(v: string | undefined) => v && (currentContribucion.tipo_tasa = v)}
>
<Select.Trigger id="tipo_tasa" class="flex-1">
<span class="truncate">{currentContribucion.tipo_tasa || 'Seleccionar'}</span>
</Select.Trigger>
<Select.Content>
<Select.Item value="Porcentual">Porcentual</Select.Item>
<Select.Item value="Especifico">Específico</Select.Item>
<Select.Item value="CuotaMinimaDTA">Cuota minima (DTA)</Select.Item>
<Select.Item value="CuotaFijaDTA">Cuota fija (DTA)</Select.Item>
<Select.Item value="TasaDescuentoAdValorem">Tasa de descuento sobre ad valorem</Select.Item>
<Select.Item value="FactorAplicacionTIGIE">Factor de aplicación sobre tigie.</Select.Item>
<Select.Item value="AlMillarDTA">Al millar (DTA)</Select.Item>
<Select.Item value="TasaDescuentoArancelEspecifico">Tasa de descuento sobre el arancel específico</Select.Item>
<Select.Item value="TasaEspecificaPreciosReferencia">Tasa especifica sobre precios de referencia</Select.Item>
<Select.Item value="TasaEspecificaPreciosReferenciaUM">Tasa especifica sobre precios de referencia con UM</Select.Item>
</Select.Content>
</Select.Root>
<Button size="icon" variant="outline">
<Plus class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-2">
<Label for="tasa">Tasa:</Label>
<Input
id="tasa"
type="number"
step="0.00001"
bind:value={currentContribucion.tasa}
placeholder="0.00000"
/>
</div>
</div>
<!-- Contribuciones Generales - Registro 510 -->
<div class="space-y-4">
<h3 class="font-semibold text-sm">Contribuciones Generales - Registro 510</h3>
<!-- Tabla de Contribuciones Generales -->
{#if currentContribucion.contribuciones_generales && currentContribucion.contribuciones_generales.length > 0}
<Table>
<TableHeader>
<TableRow>
<TableHead>Clave Contribución</TableHead>
<TableHead>Forma de Pago</TableHead>
<TableHead>Importe</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each currentContribucion.contribuciones_generales as contribGen, i}
<TableRow>
<TableCell>{contribGen.clave_contribucion}</TableCell>
<TableCell>{contribGen.clave_forma_pago}</TableCell>
<TableCell>${contribGen.importe_contribucion.toFixed(2)}</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
{:else}
<p class="text-sm text-muted-foreground">No hay contribuciones generales registradas</p>
{/if}
<div class="flex gap-2">
<Button size="sm" variant="outline" onclick={openNewContribGenInDialog}>
<Plus class="h-4 w-4 mr-1" /> Nuevo
</Button>
<Button size="sm" variant="outline" disabled>
<Pencil class="h-4 w-4 mr-1" /> Editar
</Button>
<Button size="sm" variant="outline" disabled>
<Trash2 class="h-4 w-4 mr-1" /> Borrar
</Button>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onclick={() => (isDialogOpen = false)}>Cancelar</Button>
<Button onclick={saveContribucion}>Aceptar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- Dialog para Contribución General (510) dentro del diálogo de Contribución -->
<Dialog bind:open={isContribGenDialogOpen}>
<DialogContent class="max-w-md">
<DialogHeader>
<DialogTitle>Insertando</DialogTitle>
</DialogHeader>
<div class="space-y-4 py-4">
<div class="space-y-2">
<Label for="pedimento_sys_id_temp">PedimentoSysID:</Label>
<Input
id="pedimento_sys_id_temp"
bind:value={tempContribGen.pedimento_sys_id}
placeholder="39,641"
/>
</div>
<div class="space-y-2">
<Label for="clave_contribucion_temp">Clave de Contribución:</Label>
<Input
id="clave_contribucion_temp"
bind:value={tempContribGen.clave_contribucion}
placeholder="0"
/>
</div>
<div class="space-y-2">
<Label for="clave_tasa_temp">Clave de Tasa:</Label>
<Input
id="clave_tasa_temp"
bind:value={tempContribGen.clave_tasa}
placeholder=""
/>
</div>
<div class="space-y-2">
<Label for="tasa_contribucion_temp">Tasa de Contribución:</Label>
<Input
id="tasa_contribucion_temp"
type="number"
step="0.00001"
bind:value={tempContribGen.tasa_contribucion}
placeholder="0.00000"
/>
</div>
<div class="space-y-2">
<Label for="clave_forma_pago_temp">Clave Forma de Pago:</Label>
<Input
id="clave_forma_pago_temp"
bind:value={tempContribGen.clave_forma_pago}
placeholder="0"
/>
</div>
<div class="space-y-2">
<Label for="importe_contribucion_temp">Importe de Contribución:</Label>
<Input
id="importe_contribucion_temp"
type="number"
step="0.01"
bind:value={tempContribGen.importe_contribucion}
placeholder="0"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onclick={() => (isContribGenDialogOpen = false)}>Cancelar</Button>
<Button onclick={saveContribGenInDialog}>Aceptar</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -0,0 +1,55 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ fetch }) => {
// Configurar la URL de la API usando las variables de entorno
let apiUrl = process.env.INTERNAL_API_URL;
if (!apiUrl) {
apiUrl = process.env.VITE_API_URL;
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
}
// Normalizar la URL
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
try {
// Endpoint público de países - no requiere autenticación
// Nota: La barra final "/" es importante para evitar redirects 307
// El backend limita page_size a 100, así que necesitamos hacer múltiples llamadas
const allCountries: any[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(
`${baseUrl}v1/public/refrence_data/countries/?page=${page}&page_size=100`,
{
headers: {
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
console.error('Error fetching countries:', response.status, response.statusText);
const errorText = await response.text();
console.error('Error response:', errorText);
return json({ error: 'Failed to fetch countries' }, { status: response.status });
}
const data = await response.json();
allCountries.push(...(data.items || []));
// Si hay menos items que el page_size, es la última página
hasMore = data.items && data.items.length === 100;
page++;
}
return json(allCountries);
} catch (error) {
console.error('Error fetching countries:', error);
return json({ error: 'Internal server error' }, { status: 500 });
}
};