fix(customs-brokers): update VU and personnel for multi-tenancy support
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
.mypy_cache/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
@@ -43,7 +43,7 @@ class CustomsBrokerResponseDTO(CustomsBrokerBaseDTO):
|
||||
broker_key: str
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
vu: Optional["CustomsBrokerVUCreateDTO"] = None
|
||||
vu: Optional["CustomsBrokerVUResponseDTO"] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -95,6 +95,11 @@ class CustomsBrokerVUCreateDTO(BaseModel):
|
||||
doda_web_service_access_key: Optional[str] = None
|
||||
doda_fiel_access_key: Optional[str] = None
|
||||
doda_xml_files_path: Optional[str] = None
|
||||
tenant_id: Optional[int] = None
|
||||
company_id: Optional[int] = None
|
||||
|
||||
class CustomsBrokerVUResponseDTO(CustomsBrokerVUCreateDTO):
|
||||
customs_broker_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -112,6 +117,8 @@ class CustomsBrokerPersonnelDTO(BaseModel):
|
||||
last_name: Optional[str] = None
|
||||
middle_name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
tenant_id: Optional[int] = None
|
||||
company_id: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -62,7 +62,7 @@ def update_customs_broker(
|
||||
|
||||
@router.put(
|
||||
"/customs-broker-vu/{broker_key}",
|
||||
response_model=dto.CustomsBrokerVUCreateDTO,
|
||||
response_model=dto.CustomsBrokerVUResponseDTO,
|
||||
)
|
||||
def update_customs_broker_vu(
|
||||
broker_key: str,
|
||||
@@ -77,7 +77,7 @@ def update_customs_broker_vu(
|
||||
if not broker:
|
||||
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
||||
|
||||
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data)
|
||||
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data, tenant_id, company_id)
|
||||
if not updated_vu:
|
||||
raise HTTPException(status_code=404, detail="Customs Broker VU not found")
|
||||
return updated_vu
|
||||
@@ -102,7 +102,7 @@ def update_customs_broker_personnel(
|
||||
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
||||
|
||||
updated_personnel = services.CustomsBrokerPersonnelService.update_personnel(
|
||||
db, broker_key, line, personnel_data
|
||||
db, broker_key, line, personnel_data, tenant_id, company_id
|
||||
)
|
||||
if not updated_personnel:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -90,9 +90,17 @@ class CustomsBrokerVUService:
|
||||
return new_vu
|
||||
|
||||
@staticmethod
|
||||
def update_vu(db: Session, broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO):
|
||||
def update_vu(db: Session, broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO, tenant_id: int, company_id: int):
|
||||
# We need the custom broker ID to insert a new VU
|
||||
broker = db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first()
|
||||
broker = (
|
||||
db.query(models.CustomsBroker)
|
||||
.filter(
|
||||
models.CustomsBroker.broker_key == broker_key,
|
||||
models.CustomsBroker.tenant_id == tenant_id,
|
||||
models.CustomsBroker.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not broker:
|
||||
return None
|
||||
|
||||
@@ -108,6 +116,8 @@ class CustomsBrokerVUService:
|
||||
else:
|
||||
# Create new
|
||||
new_vu_data = vu_data.model_dump()
|
||||
new_vu_data["tenant_id"] = tenant_id
|
||||
new_vu_data["company_id"] = company_id
|
||||
new_vu = models.CustomsBrokerVU(customs_broker_id=broker.id, **new_vu_data)
|
||||
db.add(new_vu)
|
||||
db.commit()
|
||||
@@ -125,24 +135,36 @@ class CustomsBrokerVUService:
|
||||
|
||||
class CustomsBrokerPersonnelService:
|
||||
@staticmethod
|
||||
def get_by_broker_key_and_line(db: Session, broker_key: str, line: int):
|
||||
def get_by_broker_key_and_line(db: Session, broker_key: str, line: int, tenant_id: int, company_id: int):
|
||||
return (
|
||||
db.query(models.CustomsBrokerPersonnel)
|
||||
.join(models.CustomsBroker)
|
||||
.filter(
|
||||
models.CustomsBroker.broker_key == broker_key,
|
||||
models.CustomsBrokerPersonnel.line == line,
|
||||
models.CustomsBroker.tenant_id == tenant_id,
|
||||
models.CustomsBroker.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_personnel(db: Session, broker_key: str, personnel_data: dto.CustomsBrokerPersonnelDTO):
|
||||
broker = db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first()
|
||||
def create_personnel(db: Session, broker_key: str, personnel_data: dto.CustomsBrokerPersonnelDTO, tenant_id: int, company_id: int):
|
||||
broker = (
|
||||
db.query(models.CustomsBroker)
|
||||
.filter(
|
||||
models.CustomsBroker.broker_key == broker_key,
|
||||
models.CustomsBroker.tenant_id == tenant_id,
|
||||
models.CustomsBroker.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not broker:
|
||||
return None
|
||||
|
||||
new_personnel_data = personnel_data.model_dump()
|
||||
new_personnel_data["tenant_id"] = tenant_id
|
||||
new_personnel_data["company_id"] = company_id
|
||||
new_personnel = models.CustomsBrokerPersonnel(customs_broker_id=broker.id, **new_personnel_data)
|
||||
db.add(new_personnel)
|
||||
db.commit()
|
||||
@@ -155,16 +177,22 @@ class CustomsBrokerPersonnelService:
|
||||
broker_key: str,
|
||||
line: int,
|
||||
personnel_data: dto.CustomsBrokerPersonnelDTO,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
):
|
||||
personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(
|
||||
db, broker_key, line
|
||||
db, broker_key, line, tenant_id, company_id
|
||||
)
|
||||
if personnel:
|
||||
for key, value in personnel_data.model_dump(exclude_unset=True).items():
|
||||
setattr(personnel, key, value)
|
||||
db.commit()
|
||||
db.refresh(personnel)
|
||||
return personnel
|
||||
return personnel
|
||||
else:
|
||||
return CustomsBrokerPersonnelService.create_personnel(
|
||||
db, broker_key, personnel_data, tenant_id, company_id
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete_personnel(db: Session, broker_key: str, line: int):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api } from '$lib/api';
|
||||
import { companyStore } from '$lib/stores/company.svelte'; // <--- NUEVO: Importamos el store para el fallback
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface CustomsBroker {
|
||||
@@ -26,6 +27,8 @@ export interface CustomsBroker {
|
||||
}
|
||||
|
||||
export interface CustomsBrokerVU {
|
||||
tenant_id?: string | null;
|
||||
company_id?: string | null;
|
||||
certificate_path?: string | null;
|
||||
key_path?: string | null;
|
||||
access_key?: string | null;
|
||||
@@ -95,7 +98,9 @@ export interface CustomsBrokerListResponse {
|
||||
*/
|
||||
export const customsBrokersApi = {
|
||||
list: (companyId: string, page = 1, pageSize = 50) => {
|
||||
return api.get<CustomsBrokerListResponse>(`/v1/a76/customs-brokers?company_id=${companyId}&page=${page}&page_size=${pageSize}`);
|
||||
return api.get<CustomsBrokerListResponse>(
|
||||
`/v1/a76/customs-brokers?company_id=${companyId}&page=${page}&page_size=${pageSize}`
|
||||
);
|
||||
},
|
||||
|
||||
get: (brokerKey: string, companyId: string) => {
|
||||
@@ -111,20 +116,47 @@ export const customsBrokersApi = {
|
||||
*/
|
||||
update: (brokerKey: string, data: CreateCustomsBrokerData) => {
|
||||
const companyId = data.company_id;
|
||||
return api.put<CustomsBroker>(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data);
|
||||
return api.put<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}`);
|
||||
return api.delete<CustomsBroker>(
|
||||
`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Actualiza la información de Ventanilla Única (VU)
|
||||
*/
|
||||
updateVU: (brokerKey: string, data: CustomsBrokerVU, companyId: string) => {
|
||||
return api.put<CustomsBrokerVU>(`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${companyId}`, data);
|
||||
// LOGICA DE RESCATE:
|
||||
// Si companyId llega nulo/undefined, intentamos obtenerlo del store global
|
||||
let finalCompanyId = companyId;
|
||||
|
||||
if (!finalCompanyId && companyStore.activeCompany?.id) {
|
||||
finalCompanyId = companyStore.activeCompany.id.toString();
|
||||
console.warn("WARN: companyId no fue provisto a updateVU, usando companyStore:", finalCompanyId);
|
||||
}
|
||||
|
||||
// Aseguramos que el payload tenga los IDs
|
||||
const payload = {
|
||||
...data,
|
||||
company_id: finalCompanyId,
|
||||
tenant_id: finalCompanyId
|
||||
};
|
||||
|
||||
console.log('[DEBUG] Enviando payload VU:', payload);
|
||||
|
||||
return api.put<CustomsBrokerVU>(
|
||||
`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${finalCompanyId}`,
|
||||
payload
|
||||
);
|
||||
},
|
||||
|
||||
updatePersonnel: (brokerKey: string, line: number, data: CustomsBrokerPersonnel, companyId: string) => {
|
||||
|
||||
@@ -187,8 +187,7 @@
|
||||
const brokerColumns = createBrokerColumns(handleActionSuccess);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-[calc(100vh-4rem)] p-4 gap-4 pb-15">
|
||||
<!-- Title -->
|
||||
<div class="flex h-[calc(100vh-4rem)] flex-col gap-4 p-4 pb-15">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h1 class="text-2xl font-bold">GESTIÓN ADUANAL</h1>
|
||||
@@ -196,17 +195,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs.Root bind:value={activeTab} class="flex-1 flex flex-col overflow-hidden">
|
||||
<Tabs.List class="w-full justify-start border-b rounded-none bg-transparent p-0 mb-4">
|
||||
<Tabs.Root bind:value={activeTab} class="flex flex-1 flex-col overflow-hidden">
|
||||
<Tabs.List class="mb-4 w-full justify-start rounded-none border-b bg-transparent p-0">
|
||||
<Tabs.Trigger
|
||||
value="brokers"
|
||||
class="data-[state=active]:border-primary border-b-2 border-transparent rounded-none"
|
||||
class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
|
||||
>
|
||||
Agentes Aduanales
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="customs"
|
||||
class="data-[state=active]:border-primary border-b-2 border-transparent rounded-none"
|
||||
class="rounded-none border-b-2 border-transparent data-[state=active]:border-primary"
|
||||
>
|
||||
Secciones Aduanales
|
||||
</Tabs.Trigger>
|
||||
@@ -214,13 +213,11 @@
|
||||
|
||||
<Tabs.Content
|
||||
value="brokers"
|
||||
class="flex-1 flex gap-4 overflow-hidden mt-0 data-[state=inactive]:hidden"
|
||||
class="mt-0 flex flex-1 gap-4 overflow-hidden data-[state=inactive]:hidden"
|
||||
>
|
||||
<!-- Left Panel: Table -->
|
||||
<div class="flex-1 flex flex-col gap-4 overflow-hidden">
|
||||
<!-- Filters -->
|
||||
<div class="border rounded-lg bg-card">
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="flex flex-1 flex-col gap-4 overflow-hidden">
|
||||
<div class="rounded-lg border bg-card">
|
||||
<div class="space-y-4 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-sm font-semibold">Filtros</h2>
|
||||
<span class="text-xs text-muted-foreground">Busque por nombre o patente</span>
|
||||
@@ -244,23 +241,20 @@
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<!-- Placeholder for layout balance -->
|
||||
</div>
|
||||
<div class="flex items-end"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="flex-1 flex flex-col border rounded-lg overflow-hidden">
|
||||
<div class="flex items-center justify-between p-3 border-b bg-muted/30">
|
||||
<div class="flex flex-1 flex-col overflow-hidden rounded-lg border">
|
||||
<div class="flex items-center justify-between border-b bg-muted/30 p-3">
|
||||
<h2 class="text-sm font-semibold">Listado</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{filteredItems.length} registros
|
||||
</span>
|
||||
<Button variant="outline" size="sm" onclick={loadItems}>
|
||||
<RefreshCw class="h-4 w-4 mr-2" />
|
||||
<RefreshCw class="mr-2 h-4 w-4" />
|
||||
Actualizar
|
||||
</Button>
|
||||
</div>
|
||||
@@ -275,9 +269,8 @@
|
||||
idField="broker_key"
|
||||
/>
|
||||
</div>
|
||||
<!-- Simple Pagination Controls -->
|
||||
{#if totalItems > pageSize}
|
||||
<div class="p-2 border-t flex justify-end gap-2">
|
||||
<div class="flex justify-end gap-2 border-t p-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -286,7 +279,7 @@
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<span class="flex items-center text-xs text-muted-foreground px-2">
|
||||
<span class="flex items-center px-2 text-xs text-muted-foreground">
|
||||
Página {currentPage} de {Math.ceil(totalItems / pageSize)}
|
||||
</span>
|
||||
<Button
|
||||
@@ -302,33 +295,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Details -->
|
||||
<div
|
||||
class="w-96 flex-none flex flex-col border rounded-xl bg-muted/30 shadow-sm overflow-hidden"
|
||||
class="flex w-96 flex-none flex-col overflow-hidden rounded-xl border bg-muted/30 shadow-sm"
|
||||
>
|
||||
<div class="p-4 border-b bg-card">
|
||||
<p class="text-[10px] uppercase tracking-widest opacity-80 text-muted-foreground">
|
||||
<div class="border-b bg-card p-4">
|
||||
<p class="text-[10px] tracking-widest text-muted-foreground uppercase opacity-80">
|
||||
Detalles del Agente
|
||||
</p>
|
||||
<h2
|
||||
class="text-xl font-black font-mono tracking-tighter truncate"
|
||||
class="truncate font-mono text-xl font-black tracking-tighter"
|
||||
title={selectedItem?.name || ''}
|
||||
>
|
||||
{selectedItem?.name || '---'}
|
||||
</h2>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<span class="text-xs font-mono text-muted-foreground"
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<span class="font-mono text-xs text-muted-foreground"
|
||||
>Patente: {selectedItem?.broker_key || ''}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto p-5 space-y-6 bg-card">
|
||||
<div class="flex-1 space-y-6 overflow-auto bg-card p-5">
|
||||
{#if selectedItem}
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div class="space-y-1">
|
||||
<Label
|
||||
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
|
||||
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
|
||||
>
|
||||
<FileText size={10} /> Licencia / Autorización
|
||||
</Label>
|
||||
@@ -338,21 +330,21 @@
|
||||
{#if selectedItem.tax_id}
|
||||
<div class="space-y-1">
|
||||
<Label
|
||||
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
|
||||
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
|
||||
>
|
||||
<Hash size={10} /> RFC / Tax ID
|
||||
</Label>
|
||||
<p class="text-sm font-mono">{selectedItem.tax_id}</p>
|
||||
<p class="font-mono text-sm">{selectedItem.tax_id}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pt-4 border-t space-y-3">
|
||||
<div class="space-y-3 border-t pt-4">
|
||||
<Label
|
||||
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
|
||||
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
|
||||
>
|
||||
<MapPin size={10} /> Dirección
|
||||
</Label>
|
||||
<div class="text-sm space-y-1">
|
||||
<div class="space-y-1 text-sm">
|
||||
<p>{selectedItem.address || ''}</p>
|
||||
<p>
|
||||
{[selectedItem.city, selectedItem.state].filter(Boolean).join(', ')}
|
||||
@@ -363,9 +355,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t space-y-3">
|
||||
<div class="space-y-3 border-t pt-4">
|
||||
<Label
|
||||
class="text-[10px] uppercase text-muted-foreground font-bold flex items-center gap-1"
|
||||
class="flex items-center gap-1 text-[10px] font-bold text-muted-foreground uppercase"
|
||||
>
|
||||
<Phone size={10} /> Contacto
|
||||
</Label>
|
||||
@@ -391,9 +383,9 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center h-full text-center text-muted-foreground opacity-50"
|
||||
class="flex h-full flex-col items-center justify-center text-center text-muted-foreground opacity-50"
|
||||
>
|
||||
<Building2 class="h-12 w-12 mb-3" />
|
||||
<Building2 class="mb-3 h-12 w-12" />
|
||||
<p class="text-sm">Selecciona un agente</p>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -401,9 +393,8 @@
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="customs" class="flex-1 overflow-auto mt-0 data-[state=inactive]:hidden">
|
||||
<!-- Reusing DataTable from Customs Sections -->
|
||||
<Card.Root class="h-full flex flex-col border-none shadow-none">
|
||||
<Tabs.Content value="customs" class="mt-0 flex-1 overflow-auto data-[state=inactive]:hidden">
|
||||
<Card.Root class="flex h-full flex-col border-none shadow-none">
|
||||
<Card.Content class="flex-1 p-0">
|
||||
<SectionsDataTable
|
||||
data={sections}
|
||||
@@ -418,13 +409,13 @@
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
<div
|
||||
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
>
|
||||
<div class="px-4 py-4 max-w-[1400px] mx-auto">
|
||||
<div class="mx-auto max-w-[1400px] px-4 py-4">
|
||||
<div class="flex justify-end gap-2">
|
||||
{#if activeTab === 'brokers'}
|
||||
<Button size="sm" href="/dashboard/customs_brokers/edit">
|
||||
<Plus class="h-4 w-4 mr-1" />
|
||||
<Button size="sm" href="/dashboard/customs_brokers/edit/new">
|
||||
<Plus class="mr-1 h-4 w-4" />
|
||||
Nuevo
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onclick={handleEdit} disabled={!selectedItem}>
|
||||
@@ -441,7 +432,7 @@
|
||||
</Button>
|
||||
{:else}
|
||||
<Button size="sm" onclick={() => toast.info('Pendiente')}>
|
||||
<Plus class="h-4 w-4 mr-1" />
|
||||
<Plus class="mr-1 h-4 w-4" />
|
||||
Nueva Sección
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user