Merge pull request 'fixed/corregir_agentes_aduanales' (#42) from fixed/corregir_agentes_aduanales into development
Reviewed-on: ADUANASOFT/anexo76#42
This commit is contained in:
@@ -1,35 +1,64 @@
|
||||
from typing import Dict, Any
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from . import dto, services
|
||||
|
||||
# Create main router
|
||||
router = APIRouter()
|
||||
|
||||
# Create CRUD routes for CustomsBroker using TenantCRUDRoutes
|
||||
customs_broker_crud = TenantCRUDRoutes(
|
||||
service=services.CustomsBrokerService,
|
||||
create_schema=dto.CustomsBrokerCreateDTO,
|
||||
update_schema=dto.CustomsBrokerUpdateDTO,
|
||||
response_schema=dto.CustomsBrokerResponseDTO,
|
||||
prefix="/customs-brokers", # No prefix since it's already in the parent router
|
||||
prefix="/customs-brokers",
|
||||
tags=[],
|
||||
resource_name="Customs Broker",
|
||||
id_name="broker_key",
|
||||
id_type=str,
|
||||
enable_list=True, # Enable list endpoint with pagination
|
||||
enable_list=True,
|
||||
)
|
||||
|
||||
# Include the CRUD routes
|
||||
router.include_router(customs_broker_crud.router)
|
||||
|
||||
|
||||
# Additional routes for child resources (CustomsBrokerVU and CustomsBrokerPersonnel)
|
||||
# These remain as manual routes since they have different patterns
|
||||
@router.patch(
|
||||
"/customs-brokers/{broker_key}",
|
||||
response_model=dto.CustomsBrokerResponseDTO,
|
||||
)
|
||||
def update_customs_broker(
|
||||
broker_key: str,
|
||||
broker_data: dto.CustomsBrokerUpdateDTO,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Actualización parcial (PATCH).
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
||||
if not broker:
|
||||
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
||||
|
||||
updated_broker = services.CustomsBrokerService.update(
|
||||
db=db,
|
||||
broker_key=broker_key,
|
||||
tenant_id=tenant_id,
|
||||
broker_data=broker_data,
|
||||
company_id=company_id
|
||||
)
|
||||
|
||||
if not updated_broker:
|
||||
raise HTTPException(status_code=400, detail="Error updating Customs Broker")
|
||||
|
||||
return updated_broker
|
||||
|
||||
|
||||
@router.put(
|
||||
"/customs-broker-vu/{broker_key}",
|
||||
@@ -44,7 +73,6 @@ def update_customs_broker_vu(
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the broker exists and belongs to the tenant/company
|
||||
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
||||
if not broker:
|
||||
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
||||
@@ -69,7 +97,6 @@ def update_customs_broker_personnel(
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the broker exists and belongs to the tenant/company
|
||||
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
||||
if not broker:
|
||||
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
||||
@@ -81,5 +108,4 @@ def update_customs_broker_personnel(
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Customs Broker Personnel not found"
|
||||
)
|
||||
return updated_personnel
|
||||
|
||||
return updated_personnel
|
||||
@@ -1,141 +1,118 @@
|
||||
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface CustomsBroker {
|
||||
type?: string | null;
|
||||
broker_key: string;
|
||||
name?: string | null;
|
||||
address?: string | null;
|
||||
postal_code?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
phone?: string | null;
|
||||
fax?: string | null;
|
||||
email?: string | null;
|
||||
country?: string | null;
|
||||
tax_id?: string | null;
|
||||
personal_id?: string | null;
|
||||
position?: string | null;
|
||||
license: string;
|
||||
company?: string | null;
|
||||
contact?: string | null;
|
||||
tenant_id: string;
|
||||
company_id: string;
|
||||
type?: string | null;
|
||||
broker_key: string;
|
||||
name?: string | null;
|
||||
address?: string | null;
|
||||
postal_code?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
phone?: string | null;
|
||||
fax?: string | null;
|
||||
email?: string | null;
|
||||
country?: string | null;
|
||||
tax_id?: string | null;
|
||||
personal_id?: string | null;
|
||||
position?: string | null;
|
||||
license: string;
|
||||
company?: string | null;
|
||||
contact?: string | null;
|
||||
tenant_id: string;
|
||||
company_id: string;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerVU {
|
||||
certificate_path?: string | null;
|
||||
key_path?: string | null;
|
||||
access_key?: string | null;
|
||||
fiel_format?: string | null;
|
||||
signature_read_path?: string | null;
|
||||
archive_path?: string | null;
|
||||
fiel_access_key?: string | null;
|
||||
web_service_user?: string | null;
|
||||
web_service_access_key?: string | null;
|
||||
vu_email?: string | null;
|
||||
vu_figure_type?: string | null;
|
||||
xml_files_path?: string | null;
|
||||
query_tax_id?: string | null;
|
||||
doda_certificate_path?: string | null;
|
||||
doda_key_path?: string | null;
|
||||
doda_web_service_user?: string | null;
|
||||
doda_web_service_access_key?: string | null;
|
||||
doda_fiel_access_key?: string | null;
|
||||
doda_xml_files_path?: string | null;
|
||||
certificate_path?: string | null;
|
||||
key_path?: string | null;
|
||||
access_key?: string | null;
|
||||
fiel_format?: string | null;
|
||||
signature_read_path?: string | null;
|
||||
archive_path?: string | null;
|
||||
fiel_access_key?: string | null;
|
||||
web_service_user?: string | null;
|
||||
web_service_access_key?: string | null;
|
||||
vu_email?: string | null;
|
||||
vu_figure_type?: string | null;
|
||||
xml_files_path?: string | null;
|
||||
query_tax_id?: string | null;
|
||||
doda_certificate_path?: string | null;
|
||||
doda_key_path?: string | null;
|
||||
doda_web_service_user?: string | null;
|
||||
doda_web_service_access_key?: string | null;
|
||||
doda_fiel_access_key?: string | null;
|
||||
doda_xml_files_path?: string | null;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerPersonnel {
|
||||
broker_key: string;
|
||||
line: number;
|
||||
name?: string | null;
|
||||
tax_id?: string | null;
|
||||
personal_id?: string | null;
|
||||
position?: string | null;
|
||||
license?: string | null;
|
||||
first_name?: string | null;
|
||||
last_name?: string | null;
|
||||
middle_name?: string | null;
|
||||
email?: string | null;
|
||||
broker_key: string;
|
||||
line: number;
|
||||
name?: string | null;
|
||||
tax_id?: string | null;
|
||||
personal_id?: string | null;
|
||||
position?: string | null;
|
||||
license?: string | null;
|
||||
first_name?: string | null;
|
||||
last_name?: string | null;
|
||||
middle_name?: string | null;
|
||||
email?: string | null;
|
||||
}
|
||||
|
||||
export interface CreateCustomsBrokerData {
|
||||
type?: string | null;
|
||||
broker_key: string;
|
||||
name?: string | null;
|
||||
address?: string | null;
|
||||
postal_code?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
phone?: string | null;
|
||||
fax?: string | null;
|
||||
email?: string | null;
|
||||
country?: string | null;
|
||||
tax_id?: string | null;
|
||||
personal_id?: string | null;
|
||||
position?: string | null;
|
||||
license?: string | null;
|
||||
company?: string | null;
|
||||
contact?: string | null;
|
||||
tenant_id: string;
|
||||
company_id: string;
|
||||
type?: string | null;
|
||||
broker_key: string;
|
||||
name?: string | null;
|
||||
address?: string | null;
|
||||
postal_code?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
phone?: string | null;
|
||||
fax?: string | null;
|
||||
email?: string | null;
|
||||
country?: string | null;
|
||||
tax_id?: string | null;
|
||||
personal_id?: string | null;
|
||||
position?: string | null;
|
||||
license?: string | null;
|
||||
company?: string | null;
|
||||
contact?: string | null;
|
||||
tenant_id?: string;
|
||||
company_id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API para Agentes Aduanales
|
||||
*/
|
||||
export const customsBrokersApi = {
|
||||
/**
|
||||
* Lista todos los agentes aduanales
|
||||
*/
|
||||
list: (companyId: string) => {
|
||||
return api.get<CustomsBroker[]>(`/v1/a76/customs-brokers/?company_id=${companyId}`);
|
||||
},
|
||||
list: (companyId: string) => {
|
||||
return api.get<CustomsBroker[]>(`/v1/a76/customs-brokers/?company_id=${companyId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene un agente aduanal por su clave
|
||||
*/
|
||||
get: (brokerKey: string) => {
|
||||
return api.get<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}`);
|
||||
},
|
||||
get: (brokerKey: string, companyId: string) => {
|
||||
return api.get<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`);
|
||||
},
|
||||
|
||||
create: (data: CreateCustomsBrokerData, companyId: string) => {
|
||||
return api.post<CustomsBroker>(`/v1/a76/customs-brokers/?company_id=${companyId}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Crea un nuevo agente aduanal
|
||||
*/
|
||||
create: (data: CreateCustomsBrokerData) => {
|
||||
const companyId = data.company_id;
|
||||
return api.post<CustomsBroker>(`/v1/a76/customs-brokers/?company_id=${companyId}`, data);
|
||||
},
|
||||
update: (brokerKey: string, data: CreateCustomsBrokerData, companyId: string) => {
|
||||
return api.patch<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Elimina un agente aduanal
|
||||
*/
|
||||
delete: (brokerKey: string, companyId: string) => {
|
||||
return api.delete<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`);
|
||||
},
|
||||
delete: (brokerKey: string, companyId: string) => {
|
||||
return api.delete<void>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Actualiza la información de un agente aduanal
|
||||
*/
|
||||
update: (brokerKey: string, data: CreateCustomsBrokerData) => {
|
||||
const companyId = data.company_id;
|
||||
return api.put<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`, data);
|
||||
},
|
||||
updateVU: (brokerKey: string, data: CustomsBrokerVU, companyId: string) => {
|
||||
return api.put<CustomsBrokerVU>(`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${companyId}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Actualiza la información de VU de un agente aduanal
|
||||
*/
|
||||
updateVU: (brokerKey: string, data: CustomsBrokerVU) => {
|
||||
return api.put<CustomsBrokerVU>(`/v1/a76/customs-broker-vu/${brokerKey}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Actualiza el personal de un agente aduanal
|
||||
*/
|
||||
updatePersonnel: (brokerKey: string, line: number, data: CustomsBrokerPersonnel) => {
|
||||
return api.put<CustomsBrokerPersonnel>(
|
||||
`/v1/a76/customs-broker-personnel/${brokerKey}/${line}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
};
|
||||
updatePersonnel: (brokerKey: string, line: number, data: CustomsBrokerPersonnel, companyId: string) => {
|
||||
return api.put<CustomsBrokerPersonnel>(
|
||||
`/v1/a76/customs-broker-personnel/${brokerKey}/${line}?company_id=${companyId}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { CustomsBroker } from "./columns.js";
|
||||
import DetailsDialog from "./details-dialog.svelte";
|
||||
import DeleteDialog from "./delete-dialog.svelte";
|
||||
import EditDialog from "./edit-dialog.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
let {
|
||||
broker,
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
let showDetailsDialog = $state(false);
|
||||
let showDeleteDialog = $state(false);
|
||||
let showEditDialog = $state(false);
|
||||
|
||||
function handleCopyKey() {
|
||||
navigator.clipboard.writeText(broker.broker_key);
|
||||
@@ -52,7 +51,7 @@
|
||||
<span class="sr-only">Abrir menú</span>
|
||||
<EllipsisIcon class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenu.Trigger>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content>
|
||||
<DropdownMenu.Group>
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
@@ -70,7 +69,7 @@
|
||||
<DropdownMenu.Item onclick={handleViewDetails}>
|
||||
Ver detalles
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<DropdownMenu.Item onclick={() => goto(`/dashboard/customs_brokers/edit/${broker.broker_key}`)}>
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item onclick={handleDelete} class="text-destructive">
|
||||
@@ -81,5 +80,4 @@
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<DetailsDialog bind:open={showDetailsDialog} {broker} />
|
||||
<EditDialog bind:open={showEditDialog} {broker} {onSuccess} />
|
||||
<DeleteDialog bind:open={showDeleteDialog} {broker} {onSuccess} />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts" generics="TData, TValue">
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
type ColumnDef,
|
||||
getCoreRowModel
|
||||
@@ -12,18 +11,16 @@
|
||||
data: TData[];
|
||||
};
|
||||
|
||||
let {
|
||||
data,
|
||||
columns
|
||||
}: DataTableProps<TData, TValue> = $props();
|
||||
let { data, columns }: DataTableProps<TData, TValue> = $props();
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
});
|
||||
|
||||
const table = $derived(
|
||||
createSvelteTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel()
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
@@ -45,9 +42,10 @@
|
||||
</Table.Row>
|
||||
{/each}
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{#each table.getRowModel().rows as row (row.id)}
|
||||
<Table.Row data-state={row.getIsSelected() && "selected"}>
|
||||
<Table.Row>
|
||||
{#each row.getVisibleCells() as cell (cell.id)}
|
||||
<Table.Cell>
|
||||
<FlexRender
|
||||
|
||||
@@ -1,285 +1,274 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { customsBrokersApi, type CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
|
||||
import DataTable from '$lib/components/dashboard/customs_brokers/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/customs_brokers/columns.js';
|
||||
import CreateDialog from '$lib/components/dashboard/customs_brokers/create-dialog.svelte';
|
||||
import * as Card 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 type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, Search, Trash2 } from 'lucide-svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { customsBrokersApi, type CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
|
||||
import DataTable from '$lib/components/dashboard/customs_brokers/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/customs_brokers/columns.js';
|
||||
import CreateDialog from '$lib/components/dashboard/customs_brokers/create-dialog.svelte';
|
||||
import * as Card 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 type { PageData } from './$types';
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, Search, Trash2, Loader2 } from 'lucide-svelte';
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
// Estado para el diálogo de crear
|
||||
let showCreateDialog = $state(false);
|
||||
let { data }: { data: PageData } = $props();
|
||||
let showCreateDialog = $state(false);
|
||||
|
||||
// Estado para la lista de agentes aduanales (inicializar con datos del servidor)
|
||||
let brokersList = $state<CustomsBroker[]>(data.brokers || []);
|
||||
let listLoading = $state(false);
|
||||
let listError = $state<string | null>(data.error || null);
|
||||
let brokersList = $state<CustomsBroker[]>(data.brokers || []);
|
||||
let listLoading = $state(false);
|
||||
let listError = $state<string | null>(data.error || null);
|
||||
|
||||
// Estado para búsqueda
|
||||
let searchKey = $state('');
|
||||
let searchedBroker = $state<CustomsBroker | null>(null);
|
||||
let searchLoading = $state(false);
|
||||
let searchError = $state<string | null>(null);
|
||||
// Estado para búsqueda
|
||||
let searchKey = $state('');
|
||||
let searchedBroker = $state<CustomsBroker | null>(null);
|
||||
let searchLoading = $state(false);
|
||||
let searchError = $state<string | null>(null);
|
||||
|
||||
// Sincronizar token de cookies a localStorage al montar el componente
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
// Función para obtener el valor de una cookie
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
|
||||
let lastLoadedCompanyId = $state<string>(data.currentCompanyId?.toString() || "");
|
||||
|
||||
// Verificar si hay token en las cookies
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
$effect(() => {
|
||||
const currentId = companyStore.activeCompany?.id?.toString();
|
||||
|
||||
|
||||
if (browser && currentId && currentId !== lastLoadedCompanyId) {
|
||||
lastLoadedCompanyId = currentId;
|
||||
|
||||
reloadData(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (cookieToken && cookieToken !== localToken) {
|
||||
localStorage.setItem('access_token', cookieToken);
|
||||
}
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
const getCookie = (name: string): string | null => {
|
||||
const value = `; ${document.cookie}`;
|
||||
const parts = value.split(`; ${name}=`);
|
||||
if (parts.length === 2) return parts.pop()?.split(';').shift() || null;
|
||||
return null;
|
||||
};
|
||||
|
||||
// También sincronizar refresh_token si existe
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
const cookieToken = getCookie('access_token');
|
||||
const localToken = localStorage.getItem('access_token');
|
||||
if (cookieToken && cookieToken !== localToken) localStorage.setItem('access_token', cookieToken);
|
||||
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) {
|
||||
localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
const cookieRefreshToken = getCookie('refresh_token');
|
||||
const localRefreshToken = localStorage.getItem('refresh_token');
|
||||
if (cookieRefreshToken && cookieRefreshToken !== localRefreshToken) localStorage.setItem('refresh_token', cookieRefreshToken);
|
||||
}
|
||||
});
|
||||
|
||||
// Escuchar cambios de compañía
|
||||
const handleCompanyChange = (event: CustomEvent) => {
|
||||
// Recargar la página para obtener datos de la nueva compañía
|
||||
reloadData();
|
||||
};
|
||||
async function handleSearch() {
|
||||
if (!searchKey.trim()) {
|
||||
searchError = 'Por favor ingresa una clave de agente aduanal';
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
searchLoading = true;
|
||||
searchError = null;
|
||||
searchedBroker = null;
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
window.removeEventListener('companyChanged', handleCompanyChange as EventListener);
|
||||
};
|
||||
}
|
||||
});
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id.toString() || '';
|
||||
const response = await customsBrokersApi.get(searchKey.trim(), companyId);
|
||||
|
||||
async function handleSearch() {
|
||||
if (!searchKey.trim()) {
|
||||
searchError = 'Por favor ingresa una clave de agente aduanal';
|
||||
return;
|
||||
}
|
||||
if (response.error) {
|
||||
if (response.status === 404) {
|
||||
searchError = 'No se encontró un agente aduanal con esta clave';
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
searchError = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
searchError = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
searchLoading = true;
|
||||
searchError = null;
|
||||
searchedBroker = null;
|
||||
if (response.data) {
|
||||
searchedBroker = response.data;
|
||||
}
|
||||
} catch (e) {
|
||||
searchError = 'Error al buscar agente aduanal';
|
||||
console.error('Error searching broker:', e);
|
||||
} finally {
|
||||
searchLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await customsBrokersApi.get(searchKey.trim());
|
||||
/**
|
||||
* Recarga los datos.
|
||||
* @param silent Si es true, NO bloquea la tabla con el spinner
|
||||
*/
|
||||
async function reloadData(silent = false) {
|
||||
searchKey = '';
|
||||
searchedBroker = null;
|
||||
searchError = null;
|
||||
|
||||
if (!companyStore.activeCompany) return;
|
||||
|
||||
if (!silent) {
|
||||
listLoading = true;
|
||||
}
|
||||
|
||||
listError = null;
|
||||
|
||||
if (response.error) {
|
||||
if (response.status === 404) {
|
||||
searchError = 'No se encontró un agente aduanal con esta clave';
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
searchError = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
searchError = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await customsBrokersApi.list(companyStore.activeCompany.id.toString());
|
||||
|
||||
if (response.data) {
|
||||
searchedBroker = response.data;
|
||||
}
|
||||
} catch (e) {
|
||||
searchError = 'Error al buscar agente aduanal';
|
||||
console.error('Error searching broker:', e);
|
||||
} finally {
|
||||
searchLoading = false;
|
||||
}
|
||||
}
|
||||
if (response.error) {
|
||||
console.error('📊 [CustomsBrokers] Error en reloadData:', response.error);
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
listError = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
if (!silent) listError = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function reloadData() {
|
||||
// Limpiar búsqueda
|
||||
searchKey = '';
|
||||
searchedBroker = null;
|
||||
searchError = null;
|
||||
|
||||
// Recargar lista de brokers desde la API
|
||||
if (!companyStore.activeCompany) return;
|
||||
|
||||
listLoading = true;
|
||||
listError = null;
|
||||
if (response.data) {
|
||||
if (Array.isArray(response.data)) {
|
||||
brokersList = response.data;
|
||||
} else if (response.data.items && Array.isArray(response.data.items)) {
|
||||
brokersList = response.data.items;
|
||||
} else {
|
||||
console.error("⚠️ Estructura de datos inesperada:", response.data);
|
||||
brokersList = [];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
listError = 'Error recargando datos';
|
||||
console.error('📊 [CustomsBrokers] Error reloading:', e);
|
||||
} finally {
|
||||
listLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await customsBrokersApi.list(companyStore.activeCompany.id.toString());
|
||||
function handleCreateClick() {
|
||||
showCreateDialog = true;
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
console.error('📊 [CustomsBrokers] Error en reloadData:', response.error);
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
listError = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
} else {
|
||||
listError = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
function handleSuccess() {
|
||||
reloadData(false);
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
// Reemplazar la lista con los nuevos datos
|
||||
brokersList = response.data;
|
||||
}
|
||||
} catch (e) {
|
||||
listError = 'Error recargando datos';
|
||||
console.error('📊 [CustomsBrokers] Error reloading:', e);
|
||||
} finally {
|
||||
listLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateClick() {
|
||||
showCreateDialog = true;
|
||||
}
|
||||
|
||||
function handleSuccess() {
|
||||
// Recargar datos después de crear/eliminar
|
||||
reloadData();
|
||||
}
|
||||
|
||||
// Crear columnas con el callback onSuccess
|
||||
const columns = createColumns(handleSuccess);
|
||||
|
||||
// Array para mostrar en la tabla (búsqueda o lista completa)
|
||||
const brokers = $derived(searchedBroker ? [searchedBroker] : brokersList);
|
||||
const columns = createColumns(handleSuccess);
|
||||
const brokers = $derived(searchedBroker ? [searchedBroker] : brokersList);
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Agentes Aduanales</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona el catálogo de agentes aduanales
|
||||
</p>
|
||||
</div>
|
||||
<Button href="/dashboard/customs_brokers/new" >
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nuevo Agente Aduanal
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Agentes Aduanales</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Gestiona el catálogo de agentes aduanales
|
||||
</p>
|
||||
</div>
|
||||
<Button href="/dashboard/customs_brokers/edit" >
|
||||
<Plus class="mr-2" size={16} />
|
||||
Nuevo Agente Aduanal
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Búsqueda -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Buscar Agente Aduanal</Card.Title>
|
||||
<Card.Description>
|
||||
Ingresa la clave del agente aduanal para buscarlo
|
||||
{#if companyStore.activeCompany}
|
||||
- Compañía: {companyStore.activeCompany.name}
|
||||
{/if}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSearch(); }} class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 space-y-2">
|
||||
<Label for="search-key">Clave del Agente Aduanal</Label>
|
||||
<Input
|
||||
id="search-key"
|
||||
bind:value={searchKey}
|
||||
placeholder="Ej: 12345"
|
||||
maxlength={5}
|
||||
disabled={searchLoading}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<Button type="submit" disabled={searchLoading}>
|
||||
{#if searchLoading}
|
||||
<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>
|
||||
Buscando...
|
||||
</div>
|
||||
{:else}
|
||||
<Search class="mr-2" size={16} />
|
||||
Buscar
|
||||
{/if}
|
||||
</Button>
|
||||
{#if searchedBroker}
|
||||
<Button type="button" variant="outline" onclick={reloadData}>
|
||||
<Trash2 class="mr-2" size={16} />
|
||||
Limpiar
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Buscar Agente Aduanal</Card.Title>
|
||||
<Card.Description>
|
||||
Ingresa la clave del agente aduanal para buscarlo
|
||||
{#if companyStore.activeCompany}
|
||||
- Compañía: {companyStore.activeCompany.name}
|
||||
{/if}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSearch(); }} class="space-y-4">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1 space-y-2">
|
||||
<Label for="search-key">Clave del Agente Aduanal</Label>
|
||||
<Input
|
||||
id="search-key"
|
||||
bind:value={searchKey}
|
||||
placeholder="Ej: 12345"
|
||||
maxlength={5}
|
||||
disabled={searchLoading}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<Button type="submit" disabled={searchLoading}>
|
||||
{#if searchLoading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Buscando...
|
||||
{:else}
|
||||
<Search class="mr-2" size={16} />
|
||||
Buscar
|
||||
{/if}
|
||||
</Button>
|
||||
{#if searchedBroker}
|
||||
<Button type="button" variant="outline" onclick={() => reloadData(false)}>
|
||||
<Trash2 class="mr-2" size={16} />
|
||||
Limpiar
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if searchError}
|
||||
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{searchError}
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
{#if searchError}
|
||||
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{searchError}
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<!-- Resultados -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>
|
||||
{#if searchedBroker}
|
||||
Resultado de la Búsqueda
|
||||
{:else}
|
||||
Agentes Aduanales
|
||||
{/if}
|
||||
</Card.Title>
|
||||
<Card.Description>
|
||||
{#if searchedBroker}
|
||||
Se encontró 1 agente aduanal
|
||||
{:else if listLoading}
|
||||
Cargando agentes aduanales...
|
||||
{:else}
|
||||
Total: {brokersList.length} agente{brokersList.length !== 1 ? 's' : ''} aduanal{brokersList.length !== 1 ? 'es' : ''}
|
||||
{/if}
|
||||
</Card.Description>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
{#if listError}
|
||||
<div class="rounded-lg border border-destructive bg-destructive/10 p-4 text-sm text-destructive">
|
||||
{listError}
|
||||
</div>
|
||||
{:else if listLoading}
|
||||
<div class="flex items-center justify-center py-8">
|
||||
<div class="flex items-center gap-2 text-muted-foreground">
|
||||
<div class="h-6 w-6 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
Cargando agentes aduanales...
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<DataTable
|
||||
data={brokers}
|
||||
{columns}
|
||||
/>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>
|
||||
{#if searchedBroker}
|
||||
Resultado de la Búsqueda
|
||||
{:else}
|
||||
Agentes Aduanales
|
||||
{/if}
|
||||
</Card.Title>
|
||||
<Card.Description>
|
||||
{#if searchedBroker}
|
||||
Se encontró 1 agente aduanal
|
||||
{:else}
|
||||
Total: {brokersList.length} agente{brokersList.length !== 1 ? 's' : ''} aduanal{brokersList.length !== 1 ? 'es' : ''}
|
||||
{/if}
|
||||
</Card.Description>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content class="relative min-h-[200px]">
|
||||
{#if listError}
|
||||
<div class="rounded-lg border border-destructive bg-destructive/10 p-4 text-sm text-destructive">
|
||||
{listError}
|
||||
</div>
|
||||
{:else}
|
||||
|
||||
{#if listLoading}
|
||||
<div class="absolute inset-0 z-10 flex items-center justify-center bg-background/60 backdrop-blur-[1px] rounded-lg transition-all duration-300">
|
||||
<div class="flex items-center gap-2 rounded-full bg-background px-4 py-2 shadow-lg border animate-in fade-in zoom-in duration-300">
|
||||
<Loader2 class="h-4 w-4 animate-spin text-primary" />
|
||||
<span class="text-sm font-medium">Actualizando...</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class={listLoading ? 'opacity-50 transition-opacity duration-300' : 'transition-opacity duration-300'}>
|
||||
<DataTable
|
||||
data={brokers}
|
||||
{columns}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<!-- Diálogo de crear -->
|
||||
<CreateDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />
|
||||
<CreateDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />
|
||||
@@ -0,0 +1,275 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { customsBrokersApi, type CreateCustomsBrokerData } from "$lib/api/dashboard/a76/customs-brokers";
|
||||
|
||||
// UI Components
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { ArrowLeft, Loader2, Save, User, Phone, MapPin } from 'lucide-svelte';
|
||||
|
||||
// --- 1. LÓGICA DE IDENTIFICACIÓN ---
|
||||
let routeId = $derived($page.params.id);
|
||||
let isEdit = $derived(!!routeId && routeId !== 'new');
|
||||
let title = $derived(isEdit ? "Editar Agente Aduanal" : "Nuevo Agente Aduanal");
|
||||
|
||||
// --- 2. ESTADO ---
|
||||
let loading = $state(false);
|
||||
let activeTab = $state('general');
|
||||
let error = $state<string | null>(null);
|
||||
let dataLoaded = $state(false);
|
||||
|
||||
let formData = $state<CreateCustomsBrokerData>({
|
||||
broker_key: "",
|
||||
license: "",
|
||||
name: "",
|
||||
tax_id: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
contact: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
country: "MEX",
|
||||
type: "",
|
||||
personal_id: "",
|
||||
position: "",
|
||||
company: "",
|
||||
company_id: ""
|
||||
});
|
||||
|
||||
// --- 3. CARGA DE DATOS REACTIVA ---
|
||||
$effect(() => {
|
||||
const company = companyStore.activeCompany;
|
||||
if (company && isEdit && routeId && !dataLoaded && !loading) {
|
||||
loadBrokerData(routeId, company.id.toString());
|
||||
} else if (company && !isEdit) {
|
||||
formData.company_id = company.id.toString();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadBrokerData(key: string, cId: string) {
|
||||
if (!key || key === 'undefined') return;
|
||||
loading = true;
|
||||
try {
|
||||
const res = await customsBrokersApi.get(key, cId);
|
||||
if (res.data) {
|
||||
const d = res.data;
|
||||
// Mapeo exhaustivo para asegurar reactividad
|
||||
formData = {
|
||||
broker_key: d.broker_key || "",
|
||||
license: d.license || "",
|
||||
name: d.name || "",
|
||||
tax_id: d.tax_id || "",
|
||||
email: d.email || "",
|
||||
phone: d.phone || "",
|
||||
fax: d.fax || "",
|
||||
contact: d.contact || "",
|
||||
address: d.address || "",
|
||||
postal_code: d.postal_code || "",
|
||||
city: d.city || "",
|
||||
state: d.state || "",
|
||||
country: d.country || "MEX",
|
||||
type: d.type || "",
|
||||
personal_id: d.personal_id || "",
|
||||
position: d.position || "",
|
||||
company: d.company || "",
|
||||
company_id: cId
|
||||
};
|
||||
dataLoaded = true;
|
||||
} else if (res.error) {
|
||||
error = res.error;
|
||||
}
|
||||
} catch (e) {
|
||||
error = "Error al conectar con el servidor";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 4. GUARDADO ---
|
||||
async function handleSave() {
|
||||
if (!formData.broker_key?.trim() || !formData.license?.trim()) {
|
||||
error = "Clave y Patente son obligatorios";
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const cId = companyStore.activeCompany?.id.toString() || "";
|
||||
const res = isEdit
|
||||
? await customsBrokersApi.update(routeId!, formData, cId)
|
||||
: await customsBrokersApi.create(formData, cId);
|
||||
|
||||
if (res.error) throw new Error(res.error);
|
||||
goto('/dashboard/customs_brokers');
|
||||
} catch (e: any) {
|
||||
error = e.message || "Error al procesar la solicitud";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full mx-auto max-w-5xl py-6 px-4 pb-40">
|
||||
<div class="flex items-center gap-4 mb-8">
|
||||
<Button variant="outline" size="icon" href="/dashboard/customs_brokers">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">{title}</h1>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
{isEdit ? `Modificando registro: ${routeId}` : 'Completa la información para el nuevo Agente.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="bg-destructive/10 text-destructive p-4 rounded-lg mb-6 border border-destructive/20 flex items-center gap-2 animate-in fade-in zoom-in duration-200">
|
||||
<span class="text-lg">⚠️</span> {error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="bg-card border rounded-2xl shadow-sm overflow-hidden">
|
||||
<div class="flex bg-muted/50 p-1 gap-1 border-b">
|
||||
{#each [
|
||||
{ id: 'general', label: 'Identificación', icon: User },
|
||||
{ id: 'contacto', label: 'Contacto', icon: Phone },
|
||||
{ id: 'direccion', label: 'Dirección', icon: MapPin }
|
||||
] as tab}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-center gap-2 flex-1 py-2.5 text-sm font-medium rounded-lg transition-all
|
||||
{activeTab === tab.id
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
: 'text-muted-foreground hover:bg-background/50 hover:text-foreground'}"
|
||||
onclick={() => activeTab = tab.id}
|
||||
>
|
||||
<tab.icon class="h-4 w-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="p-8">
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSave(); }}>
|
||||
|
||||
{#if activeTab === 'general'}
|
||||
<div class="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label class="required">Clave Agente</Label>
|
||||
<Input bind:value={formData.broker_key} placeholder="Ej. 550" disabled={isEdit || loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label class="required">Patente</Label>
|
||||
<Input bind:value={formData.license} placeholder="Ej. 3421" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Nombre / Razón Social</Label>
|
||||
<Input bind:value={formData.name} placeholder="Nombre oficial" disabled={loading} />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label>RFC</Label>
|
||||
<Input bind:value={formData.tax_id} placeholder="RFC de la empresa" disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>CURP</Label>
|
||||
<Input bind:value={formData.personal_id} placeholder="CURP si aplica" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'contacto'}
|
||||
<div class="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label>Persona de Contacto</Label>
|
||||
<Input bind:value={formData.contact} placeholder="Nombre del contacto" disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Puesto / Cargo</Label>
|
||||
<Input bind:value={formData.position} placeholder="Ej. Gerente Comercial" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label>Teléfono</Label>
|
||||
<Input bind:value={formData.phone} placeholder="656-000-0000" disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Fax</Label>
|
||||
<Input bind:value={formData.fax} disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Correo Electrónico</Label>
|
||||
<Input type="email" bind:value={formData.email} placeholder="correo@empresa.com" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{:else if activeTab === 'direccion'}
|
||||
<div class="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300">
|
||||
<div class="grid gap-2">
|
||||
<Label>Calle y Número</Label>
|
||||
<Input bind:value={formData.address} placeholder="Dirección completa" disabled={loading} />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label>C.P.</Label>
|
||||
<Input bind:value={formData.postal_code} placeholder="32000" disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2 md:col-span-2">
|
||||
<Label>Ciudad</Label>
|
||||
<Input bind:value={formData.city} placeholder="Ciudad" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label>Estado</Label>
|
||||
<Input bind:value={formData.state} placeholder="Estado" disabled={loading} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>País</Label>
|
||||
<Input bind:value={formData.country} placeholder="MEX" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="fixed bottom-0 left-0 right-0 md:left-64 bg-background/95 backdrop-blur-sm border-t p-4 z-50">
|
||||
<div class="max-w-5xl mx-auto flex justify-end items-center gap-4">
|
||||
<Button variant="ghost" href="/dashboard/customs_brokers" disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading} class="min-w-[150px] shadow-lg shadow-primary/20">
|
||||
{#if loading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
<Save class="mr-2 h-4 w-4" />
|
||||
{isEdit ? 'Actualizar Agente' : 'Guardar Agente'}
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.required::after) {
|
||||
content: " *";
|
||||
color: hsl(var(--destructive));
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user