feat: Implement Ventanilla Única (VU) management for customs brokers, including API, UI, and data model updates, and add a new state selection dialog.
This commit is contained in:
@@ -94,9 +94,8 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
"""Upgrade schema."""
|
||||
|
||||
# --- UTILIDAD DE FORMATEO ---
|
||||
def format_value(val):
|
||||
if val is None or str(val).strip() == "" or str(val).upper() == "NONE":
|
||||
return "NULL"
|
||||
|
||||
@@ -43,6 +43,7 @@ class CustomsBrokerResponseDTO(CustomsBrokerBaseDTO):
|
||||
broker_key: str
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
vu: Optional["CustomsBrokerVUCreateDTO"] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -75,25 +76,25 @@ class CustomsBrokerDTO(BaseModel):
|
||||
|
||||
|
||||
class CustomsBrokerVUCreateDTO(BaseModel):
|
||||
certificate_path: Optional[str]
|
||||
key_path: Optional[str]
|
||||
access_key: Optional[str]
|
||||
fiel_format: Optional[str]
|
||||
signature_read_path: Optional[str]
|
||||
archive_path: Optional[str]
|
||||
fiel_access_key: Optional[str]
|
||||
web_service_user: Optional[str]
|
||||
web_service_access_key: Optional[str]
|
||||
vu_email: Optional[str]
|
||||
vu_figure_type: Optional[str]
|
||||
xml_files_path: Optional[str]
|
||||
query_tax_id: Optional[str]
|
||||
doda_certificate_path: Optional[str]
|
||||
doda_key_path: Optional[str]
|
||||
doda_web_service_user: Optional[str]
|
||||
doda_web_service_access_key: Optional[str]
|
||||
doda_fiel_access_key: Optional[str]
|
||||
doda_xml_files_path: Optional[str]
|
||||
certificate_path: Optional[str] = None
|
||||
key_path: Optional[str] = None
|
||||
access_key: Optional[str] = None
|
||||
fiel_format: Optional[str] = None
|
||||
signature_read_path: Optional[str] = None
|
||||
archive_path: Optional[str] = None
|
||||
fiel_access_key: Optional[str] = None
|
||||
web_service_user: Optional[str] = None
|
||||
web_service_access_key: Optional[str] = None
|
||||
vu_email: Optional[str] = None
|
||||
vu_figure_type: Optional[str] = None
|
||||
xml_files_path: Optional[str] = None
|
||||
query_tax_id: Optional[str] = None
|
||||
doda_certificate_path: Optional[str] = None
|
||||
doda_key_path: Optional[str] = None
|
||||
doda_web_service_user: Optional[str] = None
|
||||
doda_web_service_access_key: Optional[str] = None
|
||||
doda_fiel_access_key: Optional[str] = None
|
||||
doda_xml_files_path: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -102,15 +103,15 @@ class CustomsBrokerVUCreateDTO(BaseModel):
|
||||
class CustomsBrokerPersonnelDTO(BaseModel):
|
||||
broker_key: str = Field(..., max_length=5, pattern=r"^[a-zA-Z0-9]+$")
|
||||
line: int
|
||||
name: Optional[str]
|
||||
tax_id: Optional[str]
|
||||
personal_id: Optional[str]
|
||||
position: Optional[str]
|
||||
name: Optional[str] = None
|
||||
tax_id: Optional[str] = None
|
||||
personal_id: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
license: Optional[str] = Field(None, max_length=4, pattern=r"^\d*$")
|
||||
first_name: Optional[str]
|
||||
last_name: Optional[str]
|
||||
middle_name: Optional[str]
|
||||
email: Optional[str]
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
middle_name: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -32,7 +32,7 @@ class CustomsBroker(Base, TenantScopedMixin, TimestampMixin):
|
||||
contact = Column(String(80), nullable=True)
|
||||
|
||||
vu = relationship(
|
||||
"CustomsBrokerVU", back_populates="customs_broker", cascade="all, delete"
|
||||
"CustomsBrokerVU", back_populates="customs_broker", cascade="all, delete", uselist=False
|
||||
)
|
||||
personnel = relationship(
|
||||
"CustomsBrokerPersonnel", back_populates="customs_broker", cascade="all, delete"
|
||||
|
||||
@@ -76,7 +76,8 @@ class CustomsBrokerVUService:
|
||||
def get_by_broker_key(db: Session, broker_key: str):
|
||||
return (
|
||||
db.query(models.CustomsBrokerVU)
|
||||
.filter(models.CustomsBrokerVU.broker_key == broker_key)
|
||||
.join(models.CustomsBroker)
|
||||
.filter(models.CustomsBroker.broker_key == broker_key)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -90,13 +91,28 @@ class CustomsBrokerVUService:
|
||||
|
||||
@staticmethod
|
||||
def update_vu(db: Session, broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO):
|
||||
vu = CustomsBrokerVUService.get_by_broker_key(db, broker_key)
|
||||
# We need the custom broker ID to insert a new VU
|
||||
broker = db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first()
|
||||
if not broker:
|
||||
return None
|
||||
|
||||
vu = db.query(models.CustomsBrokerVU).filter(models.CustomsBrokerVU.customs_broker_id == broker.id).first()
|
||||
|
||||
if vu:
|
||||
for key, value in vu_data.dict(exclude_unset=True).items():
|
||||
# Update existing
|
||||
for key, value in vu_data.model_dump(exclude_unset=True).items():
|
||||
setattr(vu, key, value)
|
||||
db.commit()
|
||||
db.refresh(vu)
|
||||
return vu
|
||||
return vu
|
||||
else:
|
||||
# Create new
|
||||
new_vu_data = vu_data.model_dump()
|
||||
new_vu = models.CustomsBrokerVU(customs_broker_id=broker.id, **new_vu_data)
|
||||
db.add(new_vu)
|
||||
db.commit()
|
||||
db.refresh(new_vu)
|
||||
return new_vu
|
||||
|
||||
@staticmethod
|
||||
def delete_vu(db: Session, broker_key: str):
|
||||
@@ -112,16 +128,22 @@ class CustomsBrokerPersonnelService:
|
||||
def get_by_broker_key_and_line(db: Session, broker_key: str, line: int):
|
||||
return (
|
||||
db.query(models.CustomsBrokerPersonnel)
|
||||
.join(models.CustomsBroker)
|
||||
.filter(
|
||||
models.CustomsBrokerPersonnel.broker_key == broker_key,
|
||||
models.CustomsBroker.broker_key == broker_key,
|
||||
models.CustomsBrokerPersonnel.line == line,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_personnel(db: Session, personnel_data: dto.CustomsBrokerPersonnelDTO):
|
||||
new_personnel = models.CustomsBrokerPersonnel(**personnel_data.dict())
|
||||
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()
|
||||
if not broker:
|
||||
return None
|
||||
|
||||
new_personnel_data = personnel_data.model_dump()
|
||||
new_personnel = models.CustomsBrokerPersonnel(customs_broker_id=broker.id, **new_personnel_data)
|
||||
db.add(new_personnel)
|
||||
db.commit()
|
||||
db.refresh(new_personnel)
|
||||
@@ -138,7 +160,7 @@ class CustomsBrokerPersonnelService:
|
||||
db, broker_key, line
|
||||
)
|
||||
if personnel:
|
||||
for key, value in personnel_data.dict(exclude_unset=True).items():
|
||||
for key, value in personnel_data.model_dump(exclude_unset=True).items():
|
||||
setattr(personnel, key, value)
|
||||
db.commit()
|
||||
db.refresh(personnel)
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface CustomsBroker {
|
||||
contact?: string | null;
|
||||
tenant_id: string;
|
||||
company_id: string;
|
||||
vu?: CustomsBrokerVU | null;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerVU {
|
||||
|
||||
@@ -28,8 +28,12 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBroker>[
|
||||
cell: ({ row }) => {
|
||||
const typeSnippet = createRawSnippet<[{ type: string | null | undefined }]>((getType) => {
|
||||
const { type } = getType();
|
||||
let display = type || '-';
|
||||
if (type === 'MEX') display = 'Agente Aduanal Mexicano';
|
||||
else if (type === 'USA') display = 'Agente Aduanal Americano (Broker)';
|
||||
|
||||
return {
|
||||
render: () => `<div class="max-w-[120px] truncate">${type || '-'}</div>`
|
||||
render: () => `<div class="max-w-[200px] truncate text-sm">${display}</div>`
|
||||
};
|
||||
});
|
||||
return renderSnippet(typeSnippet, { type: row.original.type });
|
||||
@@ -68,8 +72,8 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBroker>[
|
||||
const postalSnippet = createRawSnippet<[{ postal: string | null | undefined }]>((getPostal) => {
|
||||
const { postal } = getPostal();
|
||||
return {
|
||||
render: () =>
|
||||
postal
|
||||
render: () =>
|
||||
postal
|
||||
? `<code class="bg-muted px-1 py-0.5 rounded text-sm">${postal}</code>`
|
||||
: `<span class="text-muted-foreground">-</span>`
|
||||
};
|
||||
@@ -136,8 +140,8 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBroker>[
|
||||
const licenseSnippet = createRawSnippet<[{ license: string | null | undefined }]>((getLicense) => {
|
||||
const { license } = getLicense();
|
||||
return {
|
||||
render: () =>
|
||||
license
|
||||
render: () =>
|
||||
license
|
||||
? `<code class="relative rounded bg-blue-100 dark:bg-blue-900 px-[0.3rem] py-[0.2rem] font-mono text-xs font-semibold">${license}</code>`
|
||||
: `<span class="text-muted-foreground">-</span>`
|
||||
};
|
||||
@@ -175,9 +179,9 @@ export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBroker>[
|
||||
id: "actions",
|
||||
header: "Acciones",
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
return renderComponent(DataTableActions, {
|
||||
broker: row.original,
|
||||
onSuccess
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Loader2, Search } from 'lucide-svelte';
|
||||
import { statesApi, type State } from '$lib/api/dashboard/reference_data/states';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
onSelect,
|
||||
countryCode = ''
|
||||
}: {
|
||||
open: boolean;
|
||||
onSelect: (state: State) => void;
|
||||
countryCode?: string;
|
||||
} = $props();
|
||||
|
||||
let items: State[] = $state([]);
|
||||
let filteredItems: State[] = $state([]);
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state('');
|
||||
let error = $state('');
|
||||
|
||||
async function loadStates() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const response = await statesApi.list(1, 100);
|
||||
if (response.data?.items) {
|
||||
items = response.data.items;
|
||||
filteredItems = items;
|
||||
} else if (response.error) {
|
||||
error = response.error;
|
||||
}
|
||||
} catch (err) {
|
||||
error = 'Error loading states';
|
||||
console.error('Error loading states:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function filterStates() {
|
||||
let list = items;
|
||||
|
||||
// Filter by country if provided
|
||||
if (countryCode) {
|
||||
list = list.filter((item) => item.m3_key === countryCode);
|
||||
}
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm.trim()) {
|
||||
const term = searchTerm.toLowerCase();
|
||||
list = list.filter(
|
||||
(item) =>
|
||||
item.m3_key?.toLowerCase().includes(term) ||
|
||||
item.mex_key?.toLowerCase().includes(term) ||
|
||||
item.description?.toLowerCase().includes(term)
|
||||
);
|
||||
}
|
||||
|
||||
filteredItems = list;
|
||||
}
|
||||
|
||||
function handleSelect(item: State) {
|
||||
onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
loadStates();
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
filterStates();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="flex max-h-[90vh] w-[70vw] !max-w-[70vw] flex-col p-0">
|
||||
<Dialog.Header class="border-b px-6 py-4">
|
||||
<Dialog.Title class="text-lg font-semibold">CATALOGO DE ESTADOS</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="border-b bg-zinc-50 px-6 py-3 dark:bg-zinc-900">
|
||||
<div class="flex items-center gap-2">
|
||||
<Search class="h-4 w-4 text-zinc-400" />
|
||||
<Input
|
||||
bind:value={searchTerm}
|
||||
placeholder="Buscando por clave o nombre..."
|
||||
class="h-9 flex-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto px-6 py-4">
|
||||
{#if loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader2 class="h-8 w-8 animate-spin text-zinc-900 dark:text-zinc-100" />
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex items-center justify-center py-20 text-red-600">
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-hidden rounded-md border">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-zinc-900 text-white dark:bg-zinc-800">
|
||||
<tr>
|
||||
<th class="w-24 border-r border-zinc-700 px-3 py-2 text-left font-semibold"
|
||||
>Clave M3</th
|
||||
>
|
||||
<th class="w-24 border-r border-zinc-700 px-3 py-2 text-left font-semibold"
|
||||
>Clave Mex</th
|
||||
>
|
||||
<th class="w-24 border-r border-zinc-700 px-3 py-2 text-left font-semibold"
|
||||
>Clave Ame</th
|
||||
>
|
||||
<th class="px-3 py-2 text-left font-semibold">Descripción</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each filteredItems as item}
|
||||
<tr
|
||||
class="cursor-pointer border-b transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
onclick={() => handleSelect(item)}
|
||||
>
|
||||
<td class="border-r px-3 py-2 font-mono font-semibold">{item.m3_key || ''}</td>
|
||||
<td class="border-r px-3 py-2">{item.mex_key || ''}</td>
|
||||
<td class="border-r px-3 py-2">{item.ame_key || ''}</td>
|
||||
<td class="px-3 py-2">{item.description || ''}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{#if filteredItems.length === 0}
|
||||
<tr>
|
||||
<td colspan="4" class="px-3 py-8 text-center text-zinc-500">
|
||||
No se encontraron resultados
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-2 border-t bg-zinc-50 px-6 py-3 dark:bg-zinc-900">
|
||||
<Button variant="outline" size="sm" onclick={() => (open = false)}>Cancelar</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -4,6 +4,7 @@ export const obtenerAtajosEdicionAgente = (acciones: {
|
||||
irGeneral: () => void;
|
||||
irContacto: () => void;
|
||||
irDireccion: () => void;
|
||||
irVU: () => void;
|
||||
guardar: () => void;
|
||||
cancelar: () => void;
|
||||
}): ShortcutDef[] => [
|
||||
@@ -22,6 +23,11 @@ export const obtenerAtajosEdicionAgente = (acciones: {
|
||||
description: 'Tab Dirección',
|
||||
action: acciones.irDireccion
|
||||
},
|
||||
{
|
||||
key: 'Alt+Digit4',
|
||||
description: 'Tab Ventanilla Única',
|
||||
action: acciones.irVU
|
||||
},
|
||||
{
|
||||
key: 'Ctrl+S',
|
||||
description: 'Guardar',
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
} from '$lib/api/dashboard/a76/customs-brokers';
|
||||
|
||||
// UI Components
|
||||
import CountryDialog from '$lib/components/dashboard/invoices/edit/items/fa/country-dialog.svelte';
|
||||
import StateDialog from '$lib/components/dashboard/invoices/edit/items/fa/state-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
@@ -15,6 +17,7 @@
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
@@ -24,7 +27,18 @@
|
||||
MapPin,
|
||||
Settings,
|
||||
FileText,
|
||||
Hash
|
||||
Hash,
|
||||
FileKey,
|
||||
Key,
|
||||
Globe,
|
||||
Folder,
|
||||
Mail,
|
||||
UserRound,
|
||||
Fingerprint,
|
||||
Lock,
|
||||
Signature,
|
||||
Archive,
|
||||
ShieldCheck
|
||||
} from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||
@@ -40,6 +54,8 @@
|
||||
let activeTab = $state('general');
|
||||
let error = $state<string | null>(null);
|
||||
let dataLoaded = $state(false);
|
||||
let showCountryDialog = $state(false);
|
||||
let showStateDialog = $state(false);
|
||||
|
||||
let formData = $state<CreateCustomsBrokerData>({
|
||||
broker_key: '',
|
||||
@@ -62,6 +78,28 @@
|
||||
company_id: ''
|
||||
});
|
||||
|
||||
let vuData = $state({
|
||||
certificate_path: '',
|
||||
key_path: '',
|
||||
xml_files_path: '',
|
||||
fiel_access_key: '',
|
||||
doda_web_service_user: '',
|
||||
doda_web_service_access_key: '',
|
||||
doda_certificate_path: '',
|
||||
doda_key_path: '',
|
||||
doda_fiel_access_key: '',
|
||||
doda_xml_files_path: '',
|
||||
web_service_user: '',
|
||||
web_service_access_key: '',
|
||||
query_tax_id: '',
|
||||
vu_email: '',
|
||||
vu_figure_type: '',
|
||||
fiel_format: '',
|
||||
access_key: '',
|
||||
signature_read_path: '',
|
||||
archive_path: ''
|
||||
});
|
||||
|
||||
let brokerKeyError = $state(false);
|
||||
let licenseError = $state(false);
|
||||
let brokerKeyTimeout: ReturnType<typeof setTimeout>;
|
||||
@@ -77,6 +115,16 @@
|
||||
}
|
||||
});
|
||||
|
||||
// --- 5. FUNCIONES ---
|
||||
function handleLocalFileSelect(event: Event, targetKey: keyof typeof vuData) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (file) {
|
||||
vuData[targetKey] = file.name;
|
||||
toast.success(`Archivo ${file.name} seleccionado`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBrokerData(key: string, cId: string) {
|
||||
if (!key || key === 'undefined') return;
|
||||
loading = true;
|
||||
@@ -106,9 +154,32 @@
|
||||
company: d.company || '',
|
||||
company_id: cId
|
||||
};
|
||||
if (d.vu) {
|
||||
vuData = {
|
||||
certificate_path: d.vu.certificate_path || '',
|
||||
key_path: d.vu.key_path || '',
|
||||
xml_files_path: d.vu.xml_files_path || '',
|
||||
fiel_access_key: d.vu.fiel_access_key || '',
|
||||
doda_web_service_user: d.vu.doda_web_service_user || '',
|
||||
doda_web_service_access_key: d.vu.doda_web_service_access_key || '',
|
||||
doda_certificate_path: d.vu.doda_certificate_path || '',
|
||||
doda_key_path: d.vu.doda_key_path || '',
|
||||
doda_fiel_access_key: d.vu.doda_fiel_access_key || '',
|
||||
doda_xml_files_path: d.vu.doda_xml_files_path || '',
|
||||
web_service_user: d.vu.web_service_user || '',
|
||||
web_service_access_key: d.vu.web_service_access_key || '',
|
||||
query_tax_id: d.vu.query_tax_id || '',
|
||||
vu_email: d.vu.vu_email || '',
|
||||
vu_figure_type: d.vu.vu_figure_type || '',
|
||||
fiel_format: d.vu.fiel_format || '',
|
||||
access_key: d.vu.access_key || '',
|
||||
signature_read_path: d.vu.signature_read_path || '',
|
||||
archive_path: d.vu.archive_path || ''
|
||||
};
|
||||
}
|
||||
dataLoaded = true;
|
||||
} else if (d.error) {
|
||||
error = d.error;
|
||||
error = d.error as string;
|
||||
toast.error(error);
|
||||
}
|
||||
} catch (e: any) {
|
||||
@@ -119,6 +190,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleCountrySelect(country: any) {
|
||||
const nextCountry = country.m3_key || country.mex_key || country.ame_key;
|
||||
if (formData.country !== nextCountry) {
|
||||
formData.state = '';
|
||||
}
|
||||
formData.country = nextCountry;
|
||||
}
|
||||
|
||||
function handleStateSelect(state: any) {
|
||||
formData.state = state.m3_key || state.mex_key || state.ame_key;
|
||||
}
|
||||
|
||||
// --- 4. GUARDADO ---
|
||||
async function handleSave() {
|
||||
if (!companyStore.activeCompany) {
|
||||
@@ -151,15 +234,31 @@
|
||||
formData.company_id = cId;
|
||||
|
||||
const res = isEdit
|
||||
? await customsBrokersApi.update(routeId!, formData, cId)
|
||||
? await customsBrokersApi.update(routeId!, formData)
|
||||
: await customsBrokersApi.create(formData, cId);
|
||||
|
||||
if ((res as any).error) throw new Error((res as any).error);
|
||||
|
||||
// UPSERT VU
|
||||
try {
|
||||
const vuRes = await customsBrokersApi.updateVU(formData.broker_key, vuData, cId);
|
||||
if ((vuRes as any).error) {
|
||||
toast.error(
|
||||
'Agente guardado, pero ocurrió un error guardando Ventanilla Única: ' +
|
||||
(vuRes as any).error
|
||||
);
|
||||
return; // Avoid triggering success redirection
|
||||
}
|
||||
} catch (vuErr: any) {
|
||||
toast.error('Agente guardado, pero ocurrió un error guardando Ventanilla Única.');
|
||||
console.error(vuErr);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(isEdit ? 'Agente actualizado' : 'Agente creado');
|
||||
goto('/dashboard/customs_brokers');
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Error al procesar la solicitud';
|
||||
error = (e.message || 'Error al procesar la solicitud') as string;
|
||||
toast.error(error);
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -176,6 +275,7 @@
|
||||
irGeneral: () => (activeTab = 'general'),
|
||||
irContacto: () => (activeTab = 'contact'),
|
||||
irDireccion: () => (activeTab = 'address'),
|
||||
irVU: () => (activeTab = 'vu'),
|
||||
guardar: handleSave,
|
||||
cancelar: handleCancel
|
||||
})
|
||||
@@ -197,7 +297,7 @@
|
||||
{isEdit ? 'Edición' : 'Nuevo'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p class="text-muted-foreground ml-12">
|
||||
<p class="ml-12 text-muted-foreground">
|
||||
{isEdit
|
||||
? 'Modifica la información del agente aduanal'
|
||||
: 'Registra un nuevo agente aduanal en el sistema'}
|
||||
@@ -224,7 +324,31 @@
|
||||
<Card.Description>Identificación oficial del agente y patente.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>Tipo de Agente Aduanal</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.type || undefined}
|
||||
onValueChange={(v) => (formData.type = v)}
|
||||
disabled={loading}
|
||||
>
|
||||
<Select.Trigger class="h-10 w-full">
|
||||
{formData.type === 'MEX'
|
||||
? 'Agente Aduanal Mexicano'
|
||||
: formData.type === 'USA'
|
||||
? 'Agente Aduanal Americano (Broker)'
|
||||
: 'Selecciona un tipo...'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="MEX">Agente Aduanal Mexicano</Select.Item>
|
||||
<Select.Item value="USA">Agente Aduanal Americano (Broker)</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label class="required"
|
||||
>Clave Agente <span class="text-destructive">*</span></Label
|
||||
@@ -248,8 +372,8 @@
|
||||
}
|
||||
}}
|
||||
placeholder="Ej. 550"
|
||||
maxlength="6"
|
||||
class={brokerKeyError ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
maxlength={6}
|
||||
class={`h-10 ${brokerKeyError ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
|
||||
disabled={isEdit || loading}
|
||||
/>
|
||||
{#if brokerKeyError}
|
||||
@@ -281,8 +405,8 @@
|
||||
}
|
||||
}}
|
||||
placeholder="Ej. 3421"
|
||||
maxlength="5"
|
||||
class={licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''}
|
||||
maxlength={5}
|
||||
class={`h-10 ${licenseError ? 'border-red-500 focus-visible:ring-red-500' : ''}`}
|
||||
disabled={loading}
|
||||
/>
|
||||
{#if licenseError}
|
||||
@@ -297,16 +421,22 @@
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label>Nombre / Razón Social</Label>
|
||||
<Input bind:value={formData.name} placeholder="Nombre oficial" disabled={loading} />
|
||||
<Input
|
||||
bind:value={formData.name}
|
||||
placeholder="Nombre oficial"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>RFC</Label>
|
||||
<Input
|
||||
bind:value={formData.tax_id}
|
||||
placeholder="RFC de la empresa"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
@@ -315,6 +445,7 @@
|
||||
bind:value={formData.personal_id}
|
||||
placeholder="CURP si aplica"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,13 +461,14 @@
|
||||
<Card.Description>Datos para comunicación con el agente.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>Persona de Contacto</Label>
|
||||
<Input
|
||||
bind:value={formData.contact}
|
||||
placeholder="Nombre del contacto"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
@@ -345,24 +477,26 @@
|
||||
bind:value={formData.position}
|
||||
placeholder="Ej. Gerente Comercial"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="grid gap-2">
|
||||
<Label>Teléfono</Label>
|
||||
<Input
|
||||
bind:value={formData.phone}
|
||||
placeholder="656-000-0000"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Fax</Label>
|
||||
<Input bind:value={formData.fax} disabled={loading} />
|
||||
<Input bind:value={formData.fax} disabled={loading} class="h-10" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Correo Electrónico</Label>
|
||||
@@ -371,6 +505,7 @@
|
||||
bind:value={formData.email}
|
||||
placeholder="correo@empresa.com"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -392,26 +527,501 @@
|
||||
bind:value={formData.address}
|
||||
placeholder="Dirección completa"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="grid gap-2">
|
||||
<Label>C.P.</Label>
|
||||
<Input bind:value={formData.postal_code} placeholder="32000" disabled={loading} />
|
||||
<Input
|
||||
bind:value={formData.postal_code}
|
||||
placeholder="32000"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2 md:col-span-2">
|
||||
<Label>Ciudad</Label>
|
||||
<Input bind:value={formData.city} placeholder="Ciudad" disabled={loading} />
|
||||
<Input
|
||||
bind:value={formData.city}
|
||||
placeholder="Ciudad"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>Estado</Label>
|
||||
<Input bind:value={formData.state} placeholder="Estado" disabled={loading} />
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
value={formData.state}
|
||||
readonly
|
||||
placeholder="Estado"
|
||||
disabled={loading}
|
||||
class="h-10 flex-1 cursor-pointer bg-muted"
|
||||
onclick={() => (showStateDialog = true)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-10 w-10 shrink-0"
|
||||
onclick={() => (showStateDialog = true)}
|
||||
disabled={loading}
|
||||
>
|
||||
<MapPin class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>País</Label>
|
||||
<Input bind:value={formData.country} placeholder="MEX" disabled={loading} />
|
||||
<div class="flex gap-1">
|
||||
<Input
|
||||
value={formData.country}
|
||||
readonly
|
||||
placeholder="MEX"
|
||||
disabled={loading}
|
||||
class="h-10 flex-1 cursor-pointer bg-muted"
|
||||
onclick={() => (showCountryDialog = true)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-10 w-10 shrink-0"
|
||||
onclick={() => (showCountryDialog = true)}
|
||||
disabled={loading}
|
||||
>
|
||||
<Globe class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<CountryDialog bind:open={showCountryDialog} onSelect={handleCountrySelect} />
|
||||
<StateDialog
|
||||
bind:open={showStateDialog}
|
||||
onSelect={handleStateSelect}
|
||||
countryCode={formData.country ?? undefined}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
<!-- Tab: Ventanilla Unica -->
|
||||
<Tabs.Content value="vu">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Ventanilla Única / Web Services</Card.Title>
|
||||
<Card.Description
|
||||
>Certificados y credenciales para integración con DODA/PITA.</Card.Description
|
||||
>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Ruta de archivo .cer <FileKey
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
bind:value={vuData.certificate_path}
|
||||
placeholder="/ruta/certificados/agente.cer"
|
||||
disabled={loading}
|
||||
class="h-10 flex-1"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
onchange={(e) => handleLocalFileSelect(e, 'certificate_path')}
|
||||
id="vu-cert-file"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-10 w-10 shrink-0"
|
||||
disabled={loading}
|
||||
onclick={() => document.getElementById('vu-cert-file')?.click()}
|
||||
>
|
||||
<Folder class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Ruta de archivo .key <FileKey
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
bind:value={vuData.key_path}
|
||||
placeholder="/ruta/certificados/agente.key"
|
||||
disabled={loading}
|
||||
class="h-10 flex-1"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
onchange={(e) => handleLocalFileSelect(e, 'key_path')}
|
||||
id="vu-key-file"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-10 w-10 shrink-0"
|
||||
disabled={loading}
|
||||
onclick={() => document.getElementById('vu-key-file')?.click()}
|
||||
>
|
||||
<Folder class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Clave acceso (FIEL) <Key
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
bind:value={vuData.fiel_access_key}
|
||||
placeholder="********"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Tipo figura VU <ShieldCheck
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<Select.Root type="single" bind:value={vuData.vu_figure_type} disabled={loading}>
|
||||
<Select.Trigger class="h-10">
|
||||
{vuData.vu_figure_type || 'Seleccionar tipo de figura'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="AGENTE ADUANAL">AGENTE ADUANAL</Select.Item>
|
||||
<Select.Item value="APODERADO ADUANAL">APODERADO ADUANAL</Select.Item>
|
||||
<Select.Item value="MANDATARIO">MANDATARIO</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Correo electrónico <Mail
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<Input
|
||||
type="email"
|
||||
bind:value={vuData.vu_email}
|
||||
placeholder="agente@ejemplo.com"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>R.F.C de consulta <Hash
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<Input
|
||||
bind:value={vuData.query_tax_id}
|
||||
placeholder="RFC123456789"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Guardar archivos COVE <FileText
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
bind:value={vuData.xml_files_path}
|
||||
placeholder="/ruta/cove/"
|
||||
disabled={loading}
|
||||
class="h-10 flex-1"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
onchange={(e) => handleLocalFileSelect(e, 'xml_files_path')}
|
||||
id="vu-cove-file"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-10 w-10 shrink-0"
|
||||
disabled={loading}
|
||||
onclick={() => document.getElementById('vu-cove-file')?.click()}
|
||||
>
|
||||
<Folder class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 rounded-lg border bg-muted/30 p-4">
|
||||
<h3 class="text-xs font-bold tracking-wider text-muted-foreground uppercase">
|
||||
Configuración Adicional
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>Formato FIEL</Label>
|
||||
<Input
|
||||
bind:value={vuData.fiel_format}
|
||||
placeholder="Ej: PKCS#12"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Clave Acceso Adicional</Label>
|
||||
<Input
|
||||
type="password"
|
||||
bind:value={vuData.access_key}
|
||||
placeholder="********"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Ruta Lectura Firma</Label>
|
||||
<Input
|
||||
bind:value={vuData.signature_read_path}
|
||||
placeholder="/ruta/lectura/firma"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label>Ruta Archivo Respaldo</Label>
|
||||
<Input
|
||||
bind:value={vuData.archive_path}
|
||||
placeholder="/ruta/respaldo/"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: DODA -->
|
||||
<Tabs.Content value="doda">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>DODA-PITA</Card.Title>
|
||||
<Card.Description>Configuración de servicios DODA / PITA.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>Usuario <User size={14} class="ml-1 inline text-muted-foreground" /></Label
|
||||
>
|
||||
<Input
|
||||
bind:value={vuData.doda_web_service_user}
|
||||
placeholder="Usuario Web Service"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Clave de acceso <Key
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
bind:value={vuData.doda_web_service_access_key}
|
||||
placeholder="Contraseña WS"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Ruta archivo .cer <FileKey
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
bind:value={vuData.doda_certificate_path}
|
||||
placeholder="/ruta/certificados/doda.cer"
|
||||
disabled={loading}
|
||||
class="h-10 flex-1"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
onchange={(e) => handleLocalFileSelect(e, 'doda_certificate_path')}
|
||||
id="doda-cert-file"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-10 w-10 shrink-0"
|
||||
disabled={loading}
|
||||
onclick={() => document.getElementById('doda-cert-file')?.click()}
|
||||
>
|
||||
<Folder class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Ruta archivo .key <FileKey
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
bind:value={vuData.doda_key_path}
|
||||
placeholder="/ruta/certificados/doda.key"
|
||||
disabled={loading}
|
||||
class="h-10 flex-1"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
onchange={(e) => handleLocalFileSelect(e, 'doda_key_path')}
|
||||
id="doda-key-file"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-10 w-10 shrink-0"
|
||||
disabled={loading}
|
||||
onclick={() => document.getElementById('doda-key-file')?.click()}
|
||||
>
|
||||
<Folder class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Clave acceso .key <Key
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
bind:value={vuData.doda_fiel_access_key}
|
||||
placeholder="********"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Guardar archivos DODA-XML, en: <FileText
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input
|
||||
bind:value={vuData.doda_xml_files_path}
|
||||
placeholder="/ruta/doda-xml/"
|
||||
disabled={loading}
|
||||
class="h-10 flex-1"
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
onchange={(e) => handleLocalFileSelect(e, 'doda_xml_files_path')}
|
||||
id="doda-xml-file"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="h-10 w-10 shrink-0"
|
||||
disabled={loading}
|
||||
onclick={() => document.getElementById('doda-xml-file')?.click()}
|
||||
>
|
||||
<Folder class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="anam">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>ANAM</Card.Title>
|
||||
<Card.Description>Configuración de acceso para ANAM.</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label>Usuario <User size={14} class="ml-1 inline text-muted-foreground" /></Label
|
||||
>
|
||||
<Input
|
||||
bind:value={vuData.web_service_user}
|
||||
placeholder="Usuario ANAM"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label
|
||||
>Clave acceso <Lock
|
||||
size={14}
|
||||
class="ml-1 inline text-muted-foreground"
|
||||
/></Label
|
||||
>
|
||||
<Input
|
||||
type="password"
|
||||
bind:value={vuData.web_service_access_key}
|
||||
placeholder="********"
|
||||
disabled={loading}
|
||||
class="h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
@@ -424,22 +1034,19 @@
|
||||
|
||||
<!-- Sticky Footer -->
|
||||
<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 space-y-4 max-w-[1400px] mx-auto">
|
||||
<div class="mx-auto max-w-[1400px] space-y-4 px-4 py-4">
|
||||
<!-- Footer Navigation -->
|
||||
<Tabs.Root bind:value={activeTab}>
|
||||
<div class="w-full overflow-x-auto pb-2">
|
||||
<Tabs.List class="inline-flex md:grid md:w-full md:grid-cols-3">
|
||||
<Tabs.Trigger value="general" class="whitespace-nowrap">
|
||||
<User size={16} class="mr-2" /> General
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="contact" class="whitespace-nowrap">
|
||||
<Phone size={16} class="mr-2" /> Contacto
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="address" class="whitespace-nowrap">
|
||||
<MapPin size={16} class="mr-2" /> Dirección
|
||||
</Tabs.Trigger>
|
||||
<Tabs.List class="grid w-full grid-cols-6">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="contact">Contacto</Tabs.Trigger>
|
||||
<Tabs.Trigger value="address">Domicilio</Tabs.Trigger>
|
||||
<Tabs.Trigger value="vu">VU</Tabs.Trigger>
|
||||
<Tabs.Trigger value="doda">DODA</Tabs.Trigger>
|
||||
<Tabs.Trigger value="anam">ANAM</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
</Tabs.Root>
|
||||
|
||||
Reference in New Issue
Block a user