Merge pull request 'fixing/fixing_code' (#38) from fixing/fixing_code into development
Reviewed-on: ADUANASOFT/anexo76#38
This commit is contained in:
@@ -1,27 +1,40 @@
|
||||
"""
|
||||
DTOs for Customs Broker Concepts.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
|
||||
class CustomsBrokerConceptBase(BaseModel):
|
||||
broker_key: str = Field(..., max_length=5, description="Customs Broker Key (CLAVEAA)")
|
||||
concept: str = Field(..., max_length=15, description="Concept")
|
||||
amount: Optional[Decimal] = Field(None, description="Amount")
|
||||
priority: Optional[int] = Field(None, description="Priority")
|
||||
concept: str = Field(..., max_length=15, description="Concept/Code")
|
||||
amount: Optional[float] = Field(None, description="Amount (IMPORTE)")
|
||||
priority: Optional[int] = Field(None, description="Priority (PRIORIDAD)")
|
||||
|
||||
|
||||
class CustomsBrokerConceptCreate(CustomsBrokerConceptBase):
|
||||
"""Schema for creating a concept"""
|
||||
pass
|
||||
|
||||
|
||||
class CustomsBrokerConceptUpdate(BaseModel):
|
||||
broker_key: Optional[str] = Field(None, max_length=5)
|
||||
concept: Optional[str] = Field(None, max_length=15)
|
||||
amount: Optional[Decimal] = None
|
||||
priority: Optional[int] = None
|
||||
"""Schema for updating a concept"""
|
||||
broker_key: Optional[str] = Field(None, max_length=5, description="Customs Broker Key (CLAVEAA)")
|
||||
concept: Optional[str] = Field(None, max_length=15, description="Concept/Code")
|
||||
amount: Optional[float] = Field(None, description="Amount (IMPORTE)")
|
||||
priority: Optional[int] = Field(None, description="Priority (PRIORIDAD)")
|
||||
|
||||
|
||||
class CustomsBrokerConceptResponse(CustomsBrokerConceptBase):
|
||||
"""Schema for concept response"""
|
||||
id: int
|
||||
company_id: int
|
||||
tenant_id: int
|
||||
created_at: Optional[datetime] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
"""
|
||||
Routes for managing Customs Broker Concepts.
|
||||
"""
|
||||
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
|
||||
from .dto import CustomsBrokerConceptCreate, CustomsBrokerConceptResponse, CustomsBrokerConceptUpdate
|
||||
from .service import CustomsBrokerConceptService
|
||||
|
||||
# Create router using TenantCRUDRoutes factory
|
||||
router = TenantCRUDRoutes(
|
||||
service=CustomsBrokerConceptService,
|
||||
create_schema=CustomsBrokerConceptCreate,
|
||||
update_schema=CustomsBrokerConceptUpdate,
|
||||
response_schema=CustomsBrokerConceptResponse,
|
||||
prefix="/customs-broker-concepts",
|
||||
tags=["a76.general_catalogs.customs_broker_concepts"],
|
||||
prefix="/customs-broker-concepts",
|
||||
tags=["a76 / customs_broker_concepts"],
|
||||
resource_name="Customs Broker Concept",
|
||||
enable_list=True,
|
||||
id_name="concept_id",
|
||||
enable_list=True, # Enable GET /customs-broker-concepts with pagination
|
||||
enable_filters=True, # Enable filtering
|
||||
default_page_size=50,
|
||||
max_page_size=100,
|
||||
).router
|
||||
|
||||
|
||||
|
||||
@@ -1,94 +1,125 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
"""
|
||||
Service layer for Customs Broker Concepts.
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple, List, Dict, Any
|
||||
import logging
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .models import CustomsBrokerConcept
|
||||
from .dto import CustomsBrokerConceptCreate, CustomsBrokerConceptUpdate
|
||||
from . import dto, models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CustomsBrokerConceptService:
|
||||
"""Service for Customs Broker Concept CRUD operations with tenant support"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
limit: int = 50,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[CustomsBrokerConcept], int]:
|
||||
query = db.query(CustomsBrokerConcept).filter(
|
||||
CustomsBrokerConcept.tenant_id == tenant_id,
|
||||
CustomsBrokerConcept.company_id == company_id
|
||||
) -> Tuple[List[models.CustomsBrokerConcept], int]:
|
||||
"""Get all customs broker concepts for a tenant/company with pagination"""
|
||||
query = db.query(models.CustomsBrokerConcept).filter(
|
||||
models.CustomsBrokerConcept.tenant_id == tenant_id,
|
||||
models.CustomsBrokerConcept.company_id == company_id,
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
# Apply filters if provided
|
||||
if filters:
|
||||
if filters.get("broker_key"):
|
||||
query = query.filter(
|
||||
models.CustomsBrokerConcept.broker_key.ilike(f"%{filters['broker_key']}%")
|
||||
)
|
||||
if filters.get("concept"):
|
||||
query = query.filter(
|
||||
models.CustomsBrokerConcept.concept.ilike(
|
||||
f"%{filters['concept']}%")
|
||||
)
|
||||
|
||||
return items, total
|
||||
total = query.count()
|
||||
concepts = query.offset(skip).limit(limit).all()
|
||||
|
||||
return concepts, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[CustomsBrokerConcept]:
|
||||
return db.query(CustomsBrokerConcept).filter(
|
||||
CustomsBrokerConcept.id == id,
|
||||
CustomsBrokerConcept.tenant_id == tenant_id,
|
||||
CustomsBrokerConcept.company_id == company_id
|
||||
).first()
|
||||
db: Session, concept_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[models.CustomsBrokerConcept]:
|
||||
"""Get customs broker concept by ID"""
|
||||
return (
|
||||
db.query(models.CustomsBrokerConcept)
|
||||
.filter(
|
||||
models.CustomsBrokerConcept.id == concept_id,
|
||||
models.CustomsBrokerConcept.tenant_id == tenant_id,
|
||||
models.CustomsBrokerConcept.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session, data: CustomsBrokerConceptCreate, tenant_id: int, company_id: int
|
||||
) -> CustomsBrokerConcept:
|
||||
db_obj = CustomsBrokerConcept(
|
||||
**data.model_dump(),
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
db: Session,
|
||||
concept_data: dto.CustomsBrokerConceptCreate,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> models.CustomsBrokerConcept:
|
||||
"""Create a new customs broker concept"""
|
||||
new_concept = models.CustomsBrokerConcept(
|
||||
**concept_data.model_dump(), tenant_id=tenant_id, company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.add(new_concept)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
db.refresh(new_concept)
|
||||
return new_concept
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session, id: int, tenant_id: int, data: CustomsBrokerConceptUpdate, company_id: int
|
||||
) -> Optional[CustomsBrokerConcept]:
|
||||
db_obj = CustomsBrokerConceptService.get_by_id(
|
||||
db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
db: Session,
|
||||
concept_id: int,
|
||||
tenant_id: int,
|
||||
concept_data: dto.CustomsBrokerConceptUpdate,
|
||||
company_id: int,
|
||||
) -> Optional[models.CustomsBrokerConcept]:
|
||||
"""Update a customs broker concept"""
|
||||
concept = CustomsBrokerConceptService.get_by_id(
|
||||
db, concept_id, tenant_id, company_id)
|
||||
if not concept:
|
||||
return None
|
||||
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
setattr(db_obj, key, value)
|
||||
# Update fields
|
||||
update_data = concept_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(concept, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
db.refresh(concept)
|
||||
return concept
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session, id: int, tenant_id: int, company_id: int
|
||||
db: Session, concept_id: int, tenant_id: int, company_id: int
|
||||
) -> bool:
|
||||
db_obj = CustomsBrokerConceptService.get_by_id(
|
||||
db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
"""Delete a customs broker concept"""
|
||||
concept = CustomsBrokerConceptService.get_by_id(
|
||||
db, concept_id, tenant_id, company_id)
|
||||
if not concept:
|
||||
return False
|
||||
|
||||
try:
|
||||
db.delete(db_obj)
|
||||
db.delete(concept)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError deleting customs broker concept {id}: {str(e)}")
|
||||
logger.error(f"IntegrityError deleting concept {concept_id}: {str(e)}")
|
||||
if "foreign key constraint" in str(e).lower():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
@@ -97,5 +128,5 @@ class CustomsBrokerConceptService:
|
||||
raise HTTPException(status_code=400, detail="Error al eliminar el concepto")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting customs broker concept {id}: {str(e)}")
|
||||
logger.error(f"Error deleting concept {concept_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error al eliminar el concepto")
|
||||
|
||||
@@ -41,11 +41,11 @@
|
||||
"valuation_methods": "Metódos de valoración",
|
||||
"countries": "Países",
|
||||
"ports": "Puertos",
|
||||
"unit_measures": "Unidades de medida general",
|
||||
"um_customs_mex": "Unidades de medida - Aduanas Mexicanas",
|
||||
"um_customs_ame": "Unidades de medida - Aduanas Americanas",
|
||||
"um_ace": "Unidades de medida - ACE",
|
||||
"um_oma": "Unidades de medida - OMA",
|
||||
"unit_measures": "UM general",
|
||||
"um_customs_mex": "UM Aduanas MX",
|
||||
"um_customs_ame": "UM Aduanas USA",
|
||||
"um_ace": "UM ACE",
|
||||
"um_oma": "UM OMA",
|
||||
"conversions": "Conversiones",
|
||||
"equivalences": "Equivalencias",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
|
||||
@@ -3,33 +3,21 @@ import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface CustomsBrokerConcept {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
broker_key: string;
|
||||
concept: string;
|
||||
amount?: number | null;
|
||||
priority?: number | null;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
updated_at?: string | null;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerConceptCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
broker_key: string;
|
||||
concept: string;
|
||||
amount?: number | null;
|
||||
priority?: number | null;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface CustomsBrokerConcept {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerConceptCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}
|
||||
|
||||
export interface CustomsBrokerConceptListResponse {
|
||||
items: CustomsBrokerConcept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getCustomsBrokerConcepts(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {},
|
||||
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
return await api.get(`/v1/a76/customs-broker-concepts?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.get(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.post(`/v1/a76/customs-broker-concepts?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.put(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
// --- Interfaces para detalles (Hijos) ---
|
||||
|
||||
export interface DodaContainerSeal {
|
||||
id: number;
|
||||
@@ -28,7 +29,6 @@ export interface DodaContainerCreate {
|
||||
seals_detail?: DodaContainerSealCreate[];
|
||||
}
|
||||
|
||||
|
||||
export interface DodaAmericanPedimento {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
@@ -66,14 +66,13 @@ export interface DodaPedimentoCreate {
|
||||
authorization_patent?: string;
|
||||
document?: string;
|
||||
shipment?: string;
|
||||
|
||||
effective_amount_usd?: number;
|
||||
}
|
||||
|
||||
// --- Interfaces Principales (Padre) ---
|
||||
|
||||
export interface Doda {
|
||||
id: number;
|
||||
|
||||
integration_number?: string;
|
||||
doda_date?: number;
|
||||
doda_time?: number;
|
||||
@@ -87,18 +86,38 @@ export interface Doda {
|
||||
operation_type?: string;
|
||||
status?: string;
|
||||
|
||||
|
||||
// Relaciones
|
||||
containers?: DodaContainer[];
|
||||
american_pedimentos?: DodaAmericanPedimento[];
|
||||
pedimentos_detail?: DodaPedimento[];
|
||||
|
||||
|
||||
// Metadata
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
|
||||
// Campos extra del formulario que vimos en el frontend
|
||||
selected?: boolean;
|
||||
user_selected?: string;
|
||||
last_user?: string;
|
||||
responsible?: string;
|
||||
carrier?: string;
|
||||
shipments?: string;
|
||||
pedimento_type?: string;
|
||||
original_chain?: string;
|
||||
serial_number?: string;
|
||||
electronic_signature?: string;
|
||||
transaction_number?: string;
|
||||
linq_sat_qr?: string;
|
||||
sat_certificate?: string;
|
||||
sat_digital_seal?: string;
|
||||
xml_doda_sent_path?: string;
|
||||
xml_doda_response_path?: string;
|
||||
sat_original_chain?: string;
|
||||
customs_clearance?: number;
|
||||
unique_badge_number?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface DodaCreate {
|
||||
integration_number?: string;
|
||||
doda_date?: number;
|
||||
@@ -143,6 +162,7 @@ export interface DodaListResponse {
|
||||
pages: number;
|
||||
}
|
||||
|
||||
// --- Funciones de API ---
|
||||
|
||||
export async function getDodas(
|
||||
page: number = 1,
|
||||
@@ -177,7 +197,7 @@ export async function createDoda(data: DodaCreate, companyId: number): Promise<D
|
||||
}
|
||||
|
||||
export async function updateDoda(id: number, data: DodaUpdate, companyId: number): Promise<Doda> {
|
||||
const response = await api.patch(`/v1/a76/doda/${id}?company_id=${companyId}`, data);
|
||||
const response = await api.put(`/v1/a76/doda/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,20 +34,17 @@
|
||||
const response = await deleteClassificationConcept(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Clasificación "${item.classification}" eliminada correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { deleteCompany, type Company } from "$lib/api/dashboard/a76/general_catalogs/company";
|
||||
import { EllipsisVertical, Pencil, LoaderCircle, Trash2 } from 'lucide-svelte';
|
||||
import CreateEditDialog from "./create-edit-dialog.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
let {
|
||||
item,
|
||||
@@ -30,13 +31,11 @@
|
||||
|
||||
// Si hay error en la respuesta
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito (status 204 o 200)
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Empresa "${item.name}" eliminada correctamente`);
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
@@ -44,7 +43,6 @@
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -62,7 +60,7 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={() => dialogOpen = true}>
|
||||
<DropdownMenu.Item onclick={() => goto(`/dashboard/general_catalogs/company_information/edit/${item.id}`)}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
@@ -77,9 +75,3 @@
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
item={item}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,20 +34,17 @@
|
||||
const response = await deleteConcept(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Concepto "${item.code}" eliminado correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -6,24 +6,24 @@ import DataTableActions from './data-table-actions.svelte';
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<CustomsBrokerConcept>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'code',
|
||||
header: 'Código',
|
||||
cell: ({ row }) => row.original.code || '-'
|
||||
accessorKey: 'broker_key',
|
||||
header: 'Clave Agente',
|
||||
cell: ({ row }) => row.original.broker_key || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || '-'
|
||||
accessorKey: 'concept',
|
||||
header: 'Concepto',
|
||||
cell: ({ row }) => row.original.concept || '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: 'Tipo',
|
||||
cell: ({ row }) => row.original.type || '-'
|
||||
accessorKey: 'amount',
|
||||
header: 'Importe',
|
||||
cell: ({ row }) => row.original.amount ? `$${row.original.amount.toFixed(2)}` : '-'
|
||||
},
|
||||
{
|
||||
accessorKey: 'section',
|
||||
header: 'Sección',
|
||||
cell: ({ row }) => row.original.section?.toString() || '-'
|
||||
accessorKey: 'priority',
|
||||
header: 'Prioridad',
|
||||
cell: ({ row }) => row.original.priority?.toString() || '-'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
||||
@@ -6,49 +6,51 @@
|
||||
import {
|
||||
createCustomsBrokerConcept,
|
||||
updateCustomsBrokerConcept,
|
||||
type CustomsBrokerConcept
|
||||
type CustomsBrokerConcept,
|
||||
type CustomsBrokerConceptCreate
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/customs-broker-concepts";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
companyId,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: CustomsBrokerConcept | null;
|
||||
item?: CustomsBrokerConcept | null;
|
||||
companyId: number;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Concepto AA" : "Nuevo Concepto AA");
|
||||
|
||||
// 3. Estado alineado al modelo de BD
|
||||
let formData = $state({
|
||||
// Estado alineado a CustomsBrokerConceptCreate
|
||||
let formData = $state<CustomsBrokerConceptCreate>({
|
||||
broker_key: '',
|
||||
concept: '',
|
||||
amount: null as number | null,
|
||||
priority: null as number | null
|
||||
amount: undefined,
|
||||
priority: undefined
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// 4. Cargar datos al editar
|
||||
// Cargar datos al editar
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
formData = {
|
||||
broker_key: item.broker_key || '',
|
||||
concept: item.concept || '',
|
||||
amount: item.amount || null,
|
||||
priority: item.priority || null
|
||||
amount: item.amount,
|
||||
priority: item.priority
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
broker_key: '',
|
||||
concept: '',
|
||||
amount: null,
|
||||
priority: null
|
||||
amount: undefined,
|
||||
priority: undefined
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -58,34 +60,30 @@
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
// Validaciones
|
||||
if (!formData.broker_key.trim()) throw new Error('La Clave AA es requerida');
|
||||
if (!formData.broker_key.trim()) throw new Error('La Clave del Agente es requerida');
|
||||
if (!formData.concept.trim()) throw new Error('El Concepto es requerido');
|
||||
|
||||
// 5. Preparar datos con los tipos correctos (Números)
|
||||
const dataToSend = {
|
||||
// Limpiar y enviar datos
|
||||
const dataToSend: CustomsBrokerConceptCreate = {
|
||||
broker_key: formData.broker_key.trim(),
|
||||
concept: formData.concept.trim(),
|
||||
amount: formData.amount ? Number(formData.amount) : undefined,
|
||||
priority: formData.priority ? Number(formData.priority) : undefined
|
||||
};
|
||||
|
||||
// 6. Corregida la sintaxis de llamada a la API
|
||||
if (isEdit && item) {
|
||||
// UPDATE: (id, data, companyId)
|
||||
await updateCustomsBrokerConcept(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
// CREATE: (data, companyId)
|
||||
await createCustomsBrokerConcept(dataToSend, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
@@ -93,11 +91,11 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-md max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Content class="max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{isEdit ? 'Modifica el concepto del agente aduanal' : 'Crea un nuevo concepto'}
|
||||
{isEdit ? 'Modifica los detalles del concepto' : 'Ingresa los datos para el nuevo concepto'}
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
@@ -108,29 +106,30 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="broker_key">Clave AA <span class="text-destructive">*</span></Label>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2 col-span-1">
|
||||
<Label for="broker_key">Clave del Agente <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="broker_key"
|
||||
bind:value={formData.broker_key}
|
||||
placeholder="Ej: 550"
|
||||
maxlength={5}
|
||||
placeholder="Ej: 01001"
|
||||
maxlength="5"
|
||||
disabled={isEdit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="grid gap-2 col-span-1">
|
||||
<Label for="concept">Concepto <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="concept"
|
||||
bind:value={formData.concept}
|
||||
placeholder="Ej: FLETE"
|
||||
maxlength={15}
|
||||
placeholder="Ej: 001"
|
||||
maxlength="15"
|
||||
disabled={isEdit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="grid gap-2 col-span-1">
|
||||
<Label for="amount">Importe</Label>
|
||||
<Input
|
||||
id="amount"
|
||||
@@ -141,13 +140,13 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="grid gap-2 col-span-1">
|
||||
<Label for="priority">Prioridad</Label>
|
||||
<Input
|
||||
id="priority"
|
||||
type="number"
|
||||
bind:value={formData.priority}
|
||||
placeholder="Ej: 1"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,20 +34,17 @@
|
||||
const response = await deleteCustomsBrokerConcept(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Concepto "${item.code}" eliminado correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { deleteDoda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
let {
|
||||
item,
|
||||
@@ -27,7 +28,6 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,11 +35,9 @@
|
||||
|
||||
try {
|
||||
await deleteDoda(item.id, companyId);
|
||||
alert('✅ Registro eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el registro';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting doda:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -63,7 +61,7 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
|
||||
<DropdownMenu.Item onclick={handleEdit}>
|
||||
<DropdownMenu.Item onclick={() => goto(`/dashboard/general_catalogs/doda/edit/${item.id}`)}>
|
||||
<Pencil class="mr-2 h-4 w-4" />
|
||||
Editar
|
||||
</DropdownMenu.Item>
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,11 +34,9 @@
|
||||
|
||||
try {
|
||||
await deleteElectronicNotice(item.id, companyId);
|
||||
alert('✅ Aviso electrónico eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el aviso electrónico';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting electronic notice:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,11 +39,9 @@
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
alert('✅ Equivalencia eliminada correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar la equivalencia';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting equivalency:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
@@ -83,14 +83,12 @@
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateErrorCatalog(item.id, basePayload, companyId);
|
||||
alert("✅ Error actualizado correctamente");
|
||||
} else {
|
||||
const createPayload = {
|
||||
code: formData.code.trim(),
|
||||
...basePayload
|
||||
};
|
||||
await createErrorCatalog(createPayload, companyId);
|
||||
alert("✅ Error creado correctamente");
|
||||
}
|
||||
|
||||
open = false;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,13 +34,11 @@
|
||||
|
||||
try {
|
||||
await deleteErrorCatalog(item.id, companyId);
|
||||
alert('✅ Error eliminado correctamente');
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el error';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,11 +34,9 @@
|
||||
|
||||
try {
|
||||
await deleteIdentifier(item.id, companyId);
|
||||
alert('✅ Identificador eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el identificador';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting identifier:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,11 +34,9 @@
|
||||
|
||||
try {
|
||||
await deleteINPC(item.id, companyId);
|
||||
alert('✅ Registro eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el registro';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting INPC:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,20 +35,17 @@
|
||||
const response = await deleteLegend(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Leyenda "${item.code}" eliminada correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -60,14 +60,12 @@
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateLocation(item.id, basePayload, companyId);
|
||||
alert("✅ Ubicación actualizada correctamente");
|
||||
} else {
|
||||
const createPayload = {
|
||||
location_code: formData.location_code.trim(),
|
||||
...basePayload
|
||||
};
|
||||
await createLocation(createPayload, companyId);
|
||||
alert("✅ Ubicación creada correctamente");
|
||||
}
|
||||
|
||||
open = false;
|
||||
|
||||
@@ -93,10 +93,8 @@
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateMultiCurrencyType(item.id, dataToSend, companyId);
|
||||
alert(`✅ Tipo de moneda múltiple actualizado correctamente`);
|
||||
} else {
|
||||
await createMultiCurrencyType(dataToSend, companyId);
|
||||
alert(`✅ Tipo de moneda múltiple creado correctamente`);
|
||||
}
|
||||
|
||||
open = false;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,11 +34,9 @@
|
||||
|
||||
try {
|
||||
await deletePrevalidator(item.id, companyId);
|
||||
alert('✅ Prevalidador eliminado correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el prevalidador';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting prevalidator:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,11 +34,9 @@
|
||||
|
||||
try {
|
||||
await deleteSignature(item.id, companyId);
|
||||
alert('✅ Firma eliminada correctamente');
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar la firma';
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting signature:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
@@ -70,10 +70,8 @@
|
||||
|
||||
if (isEdit && conversion) {
|
||||
await updateUnitConversion(conversion.id, dataToSend, companyId);
|
||||
alert(`✅ Conversión "${dataToSend.from_unit_code} → ${dataToSend.to_unit_code}" actualizada correctamente`);
|
||||
} else {
|
||||
await createUnitConversion(dataToSend, companyId);
|
||||
alert(`✅ Conversión "${dataToSend.from_unit_code} → ${dataToSend.to_unit_code}" creada correctamente`);
|
||||
}
|
||||
|
||||
open = false;
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -31,13 +30,11 @@
|
||||
|
||||
try {
|
||||
await deleteUnitConversion(conversion.id, companyStore.activeCompany.id);
|
||||
alert(`✅ Conversión "${conversion.from_unit_code} → ${conversion.to_unit_code}" eliminada correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
}
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
alert('❌ Error: No hay una compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -33,19 +32,16 @@
|
||||
const response = await deleteUnitOfMeasureACE(item.id, companyStore.activeCompany.id);
|
||||
|
||||
if (response.error) {
|
||||
alert(`❌ Error al eliminar:\n\n${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 204 || response.status === 200 || !response.error) {
|
||||
alert(`✅ Unidad ACE "${item.code}" eliminada correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = e instanceof Error ? e.message : 'Error desconocido al eliminar el registro';
|
||||
alert(`❌ Error al eliminar:\n\n${errorMsg}`);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,9 +58,7 @@
|
||||
: await createUnitOfMeasureAmerican(data, activeCompanyId);
|
||||
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al guardar');
|
||||
} else {
|
||||
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
|
||||
open = false;
|
||||
onSuccess?.();
|
||||
}
|
||||
|
||||
@@ -20,15 +20,12 @@
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteUnitOfMeasureAmerican(unit.id, activeCompanyId);
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al eliminar');
|
||||
} else if (response.status === 204 || response.status === 200) {
|
||||
alert('Unidad eliminada');
|
||||
onSuccess?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,9 +61,7 @@
|
||||
: await createUnitOfMeasureCustoms(data, activeCompanyId);
|
||||
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al guardar');
|
||||
} else {
|
||||
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
|
||||
open = false;
|
||||
onSuccess?.();
|
||||
}
|
||||
|
||||
@@ -20,15 +20,12 @@
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteUnitOfMeasureCustoms(unit.id, activeCompanyId);
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al eliminar');
|
||||
} else if (response.status === 204 || response.status === 200) {
|
||||
alert('Unidad eliminada');
|
||||
onSuccess?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,9 +58,7 @@
|
||||
: await createUnitOfMeasureGeneral(data, activeCompanyId);
|
||||
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al guardar');
|
||||
} else {
|
||||
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
|
||||
open = false;
|
||||
onSuccess?.();
|
||||
}
|
||||
|
||||
@@ -20,15 +20,12 @@
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteUnitOfMeasureGeneral(unit.id, activeCompanyId);
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al eliminar');
|
||||
} else if (response.status === 204 || response.status === 200) {
|
||||
alert('Unidad eliminada');
|
||||
onSuccess?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,9 +58,7 @@
|
||||
: await createUnitOfMeasureOMA(data, activeCompanyId);
|
||||
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al guardar');
|
||||
} else {
|
||||
alert(unit ? 'Unidad actualizada' : 'Unidad creada');
|
||||
open = false;
|
||||
onSuccess?.();
|
||||
}
|
||||
|
||||
@@ -20,15 +20,12 @@
|
||||
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) {
|
||||
alert('No hay una compañía activa seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await deleteUnitOfMeasureOMA(unit.id, activeCompanyId);
|
||||
if (response.error) {
|
||||
alert(response.error.detail || 'Error al eliminar');
|
||||
} else if (response.status === 204 || response.status === 200) {
|
||||
alert('Unidad eliminada');
|
||||
onSuccess?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
// Ya no es necesario inicializar aquí, se inicializa en +layout.svelte con datos SSR
|
||||
</script>
|
||||
|
||||
<Sidebar.Menu>
|
||||
|
||||
@@ -14,7 +14,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
companies: parentData.companies || []
|
||||
};
|
||||
}
|
||||
/////HOLAAA
|
||||
|
||||
try {
|
||||
// Obtener company_id de múltiples fuentes (en orden de prioridad):
|
||||
// 1. URL query param (permite cambiar vía navegación)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Plus, Search, Trash2 } from 'lucide-svelte';
|
||||
//HOLAA
|
||||
|
||||
// Los datos iniciales vienen del servidor
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import * as Card from "$lib/components/ui/card";
|
||||
import { customsBrokersApi, type CreateCustomsBrokerData } from "$lib/api/dashboard/a76/customs-brokers";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { ArrowLeft } from "lucide-svelte";
|
||||
|
||||
|
||||
// Eliminar props de modal ya que ahora es una página
|
||||
// let { open = $bindable(false), onSuccess }: { open: boolean; onSuccess?: () => void; } = $props();
|
||||
|
||||
|
||||
let formData = $state({
|
||||
broker_key: "",
|
||||
name: "",
|
||||
type: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
email: "",
|
||||
country: "",
|
||||
tax_id: "",
|
||||
personal_id: "",
|
||||
position: "",
|
||||
license: "",
|
||||
company: "",
|
||||
contact: ""
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
error = "No hay compañía seleccionada";
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
const payload: CreateCustomsBrokerData = {
|
||||
broker_key: formData.broker_key,
|
||||
name: formData.name || null,
|
||||
type: formData.type || null,
|
||||
address: formData.address || null,
|
||||
postal_code: formData.postal_code || null,
|
||||
city: formData.city || null,
|
||||
state: formData.state || null,
|
||||
phone: formData.phone || null,
|
||||
fax: formData.fax || null,
|
||||
email: formData.email || null,
|
||||
country: formData.country || null,
|
||||
tax_id: formData.tax_id || null,
|
||||
personal_id: formData.personal_id || null,
|
||||
position: formData.position || null,
|
||||
license: formData.license || null,
|
||||
company: formData.company || null,
|
||||
contact: formData.contact || null,
|
||||
tenant_id: "1", // TODO: Get from user context
|
||||
company_id: companyStore.activeCompany.id.toString()
|
||||
};
|
||||
|
||||
const response = await customsBrokersApi.create(payload);
|
||||
|
||||
if (response.error) {
|
||||
if (response.status === 409) {
|
||||
error = 'Ya existe un agente aduanal con la clave proporcionada.';
|
||||
} else
|
||||
if (response.status === 401) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Éxito - redirigir a la lista
|
||||
goto('/dashboard/customs_brokers');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error al guardar";
|
||||
console.error("Error saving:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
// Limpiar formulario
|
||||
formData = {
|
||||
broker_key: "",
|
||||
name: "",
|
||||
type: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
email: "",
|
||||
country: "",
|
||||
tax_id: "",
|
||||
personal_id: "",
|
||||
position: "",
|
||||
license: "",
|
||||
company: "",
|
||||
contact: ""
|
||||
};
|
||||
error = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-4">
|
||||
<Button variant="ghost" size="icon" onclick={() => goto('/dashboard/customs_brokers')}>
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold tracking-tight">Nuevo Agente Aduanal</h1>
|
||||
<p class="text-muted-foreground">
|
||||
Completa los datos para crear un nuevo agente aduanal
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulario -->
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>Información del Agente Aduanal</Card.Title>
|
||||
<Card.Description>
|
||||
Todos los campos marcados con * son obligatorios
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<form onsubmit={handleSubmit} class="space-y-6">
|
||||
{#if error}
|
||||
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Información básica -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información Básica</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="broker_key">Clave *</Label>
|
||||
<Input
|
||||
id="broker_key"
|
||||
bind:value={formData.broker_key}
|
||||
placeholder="Ej: 12345"
|
||||
maxlength={5}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="type">Tipo</Label>
|
||||
<Input
|
||||
id="type"
|
||||
bind:value={formData.type}
|
||||
placeholder="Tipo de agente"
|
||||
maxlength={9}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Nombre</Label>
|
||||
<Input
|
||||
id="name"
|
||||
bind:value={formData.name}
|
||||
placeholder="Nombre del agente aduanal"
|
||||
maxlength={80}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="license">Patente</Label>
|
||||
<Input
|
||||
id="license"
|
||||
bind:value={formData.license}
|
||||
placeholder="Número de patente"
|
||||
maxlength={4}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="company">Empresa</Label>
|
||||
<Input
|
||||
id="company"
|
||||
bind:value={formData.company}
|
||||
placeholder="Empresa del agente"
|
||||
maxlength={200}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información de contacto -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información de Contacto</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="phone">Teléfono</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
bind:value={formData.phone}
|
||||
placeholder="Número telefónico"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="fax">Fax</Label>
|
||||
<Input
|
||||
id="fax"
|
||||
bind:value={formData.fax}
|
||||
placeholder="Número de fax"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={formData.email}
|
||||
placeholder="correo@ejemplo.com"
|
||||
maxlength={100}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="contact">Contacto</Label>
|
||||
<Input
|
||||
id="contact"
|
||||
bind:value={formData.contact}
|
||||
placeholder="Nombre del contacto"
|
||||
maxlength={80}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dirección -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Dirección</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="address">Dirección</Label>
|
||||
<Input
|
||||
id="address"
|
||||
bind:value={formData.address}
|
||||
placeholder="Calle y número"
|
||||
maxlength={1500}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="postal_code">Código Postal</Label>
|
||||
<Input
|
||||
id="postal_code"
|
||||
bind:value={formData.postal_code}
|
||||
placeholder="C.P."
|
||||
maxlength={15}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input
|
||||
id="city"
|
||||
bind:value={formData.city}
|
||||
placeholder="Ciudad"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="state">Estado</Label>
|
||||
<Input
|
||||
id="state"
|
||||
bind:value={formData.state}
|
||||
placeholder="Estado"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="country">País</Label>
|
||||
<Input
|
||||
id="country"
|
||||
bind:value={formData.country}
|
||||
placeholder="País"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información fiscal -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información Fiscal</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="tax_id">RFC</Label>
|
||||
<Input
|
||||
id="tax_id"
|
||||
bind:value={formData.tax_id}
|
||||
placeholder="RFC"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="personal_id">CURP</Label>
|
||||
<Input
|
||||
id="personal_id"
|
||||
bind:value={formData.personal_id}
|
||||
placeholder="CURP"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="position">Posición</Label>
|
||||
<Input
|
||||
id="position"
|
||||
bind:value={formData.position}
|
||||
placeholder="Cargo o posición"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-0 right-0 left-0 md:left-64 py-2 px-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
|
||||
<Button type="button" variant="outline" onclick={() => goto('/dashboard/customs_brokers')} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onclick={resetForm} disabled={loading}>
|
||||
Limpiar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"></div>
|
||||
Guardando...
|
||||
</div>
|
||||
{:else}
|
||||
Crear Agente Aduanal
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
@@ -49,7 +49,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<!-- <Button onclick={() => dialogOpen = true}> -->
|
||||
<Button href="/dashboard/general_catalogs/company_information/new">
|
||||
<Button href="/dashboard/general_catalogs/company_information/edit">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nueva Empresa
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import {
|
||||
createCompany,
|
||||
updateCompany,
|
||||
getCompany, // Asumiendo que esta función existe en tu API
|
||||
type Company
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/company';
|
||||
import { ArrowLeft, LoaderCircle, Save } from 'lucide-svelte';
|
||||
|
||||
// 1. Lógica de Navegación y Modo
|
||||
const id = $derived($page.params.id);
|
||||
const isEdit = $derived(!!id);
|
||||
const title = $derived(isEdit ? "Editar Empresa" : "Nueva Empresa");
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// 2. Estado Inicial (Reset)
|
||||
const initialData = {
|
||||
name: '',
|
||||
rfc: '',
|
||||
curp: '',
|
||||
main_activity: '',
|
||||
program: '',
|
||||
program_number: '',
|
||||
prosec: 0,
|
||||
prosec_authorization: '',
|
||||
responsible_name: '',
|
||||
responsible_last_name: '',
|
||||
responsible_mother_last_name: '',
|
||||
responsible_rfc: '',
|
||||
position: '',
|
||||
manufacturer_id: '',
|
||||
has_express_line: false,
|
||||
is_service_company: false,
|
||||
order_format_type: '',
|
||||
ctpat_svi: '',
|
||||
trusted_exporter_number: ''
|
||||
};
|
||||
|
||||
let formData = $state({ ...initialData });
|
||||
|
||||
// 3. Efecto para "Heredar" datos o Limpiar
|
||||
$effect(() => {
|
||||
if (isEdit) {
|
||||
fetchData(id);
|
||||
} else {
|
||||
formData = { ...initialData };
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function fetchData(companyId: string) {
|
||||
loading = true;
|
||||
try {
|
||||
// Nota: Aquí usamos tu API para traer la info de una sola empresa
|
||||
const response = await getCompany(Number(companyId));
|
||||
if (response.data) {
|
||||
const item = response.data;
|
||||
formData = {
|
||||
name: item.name || '',
|
||||
rfc: item.rfc || '',
|
||||
curp: item.curp || '',
|
||||
main_activity: item.main_activity || '',
|
||||
program: item.program || '',
|
||||
program_number: item.program_number || '',
|
||||
prosec: item.prosec || 0,
|
||||
prosec_authorization: item.prosec_authorization || '',
|
||||
responsible_name: item.responsible_name || '',
|
||||
responsible_last_name: item.responsible_last_name || '',
|
||||
responsible_mother_last_name: item.responsible_mother_last_name || '',
|
||||
responsible_rfc: item.responsible_rfc || '',
|
||||
position: item.position || '',
|
||||
manufacturer_id: item.manufacturer_id || '',
|
||||
has_express_line: item.has_express_line || false,
|
||||
is_service_company: item.is_service_company || false,
|
||||
order_format_type: item.order_format_type || '',
|
||||
ctpat_svi: item.ctpat_svi || '',
|
||||
trusted_exporter_number: item.trusted_exporter_number || ''
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
error = "Error al cargar los datos de la empresa";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
const clean = (value: any) => (typeof value === 'string' ? value.trim() || null : value);
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
if (!formData.name.trim()) throw new Error('El nombre es requerido');
|
||||
if (!formData.rfc.trim()) throw new Error('El RFC es requerido');
|
||||
|
||||
const payload = {
|
||||
...formData,
|
||||
name: formData.name.trim(),
|
||||
rfc: formData.rfc.trim(),
|
||||
responsible: `${formData.responsible_name} ${formData.responsible_last_name}`.trim() || null,
|
||||
curp: clean(formData.curp),
|
||||
main_activity: clean(formData.main_activity),
|
||||
program: clean(formData.program),
|
||||
program_number: clean(formData.program_number),
|
||||
prosec_authorization: clean(formData.prosec_authorization),
|
||||
responsible_name: clean(formData.responsible_name),
|
||||
responsible_last_name: clean(formData.responsible_last_name),
|
||||
responsible_mother_last_name: clean(formData.responsible_mother_last_name),
|
||||
responsible_rfc: clean(formData.responsible_rfc),
|
||||
position: clean(formData.position),
|
||||
manufacturer_id: clean(formData.manufacturer_id),
|
||||
order_format_type: clean(formData.order_format_type),
|
||||
ctpat_svi: clean(formData.ctpat_svi),
|
||||
trusted_exporter_number: clean(formData.trusted_exporter_number)
|
||||
};
|
||||
|
||||
const response = isEdit
|
||||
? await updateCompany(Number(id), payload)
|
||||
: await createCompany(payload);
|
||||
|
||||
if (response.error) throw new Error(response.error);
|
||||
|
||||
goto('/dashboard/general_catalogs/company_information');
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full mx-auto max-w-6xl py-6 px-4 space-y-6 pb-48">
|
||||
<div class="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" href="/dashboard/general_catalogs/company_information">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
|
||||
<p class="text-muted-foreground">Configuración legal y operativa de la entidad.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-6">
|
||||
<Card.Root>
|
||||
<Card.Content class="p-6">
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<div class="min-h-[400px]">
|
||||
<Tabs.Content value="general" class="space-y-4 pt-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Razón Social <span class="text-destructive">*</span></Label>
|
||||
<Input id="name" bind:value={formData.name} placeholder="Nombre oficial" />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="rfc">RFC <span class="text-destructive">*</span></Label>
|
||||
<Input id="rfc" bind:value={formData.rfc} maxlength={13} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="curp">CURP</Label>
|
||||
<Input id="curp" bind:value={formData.curp} maxlength={18} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="main_activity">Actividad Principal</Label>
|
||||
<Input id="main_activity" bind:value={formData.main_activity} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="programas" class="space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="program">Programa (IMMEX)</Label>
|
||||
<Input id="program" bind:value={formData.program} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="program_number">No. Programa</Label>
|
||||
<Input id="program_number" bind:value={formData.program_number} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="prosec">Sector PROSEC (ID)</Label>
|
||||
<Input id="prosec" type="number" bind:value={formData.prosec} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="prosec_auth">Autorización PROSEC</Label>
|
||||
<Input id="prosec_auth" bind:value={formData.prosec_authorization} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="responsable" class="space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_name">Nombre</Label>
|
||||
<Input id="resp_name" bind:value={formData.responsible_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_last">Apellido Paterno</Label>
|
||||
<Input id="resp_last" bind:value={formData.responsible_last_name} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_mother">Apellido Materno</Label>
|
||||
<Input id="resp_mother" bind:value={formData.responsible_mother_last_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_rfc">RFC Responsable</Label>
|
||||
<Input id="resp_rfc" bind:value={formData.responsible_rfc} maxlength={13} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="position">Puesto / Cargo</Label>
|
||||
<Input id="position" bind:value={formData.position} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="config" class="space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="flex items-center gap-3 p-4 border rounded-lg">
|
||||
<Switch id="express" bind:checked={formData.has_express_line} />
|
||||
<Label for="express">Carril Exprés (OEA/NEEC)</Label>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 p-4 border rounded-lg">
|
||||
<Switch id="service" bind:checked={formData.is_service_company} />
|
||||
<Label for="service">Es Empresa de Servicios</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 pt-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="man_id">Manufacturer ID (MID)</Label>
|
||||
<Input id="man_id" bind:value={formData.manufacturer_id} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="order_format">Formato de Pedido</Label>
|
||||
<Input id="order_format" bind:value={formData.order_format_type} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="ctpat">C-TPAT / SVI</Label>
|
||||
<Input id="ctpat" bind:value={formData.ctpat_svi} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="exporter">No. Exportador Confiable</Label>
|
||||
<Input id="exporter" bind:value={formData.trusted_exporter_number} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</div>
|
||||
|
||||
<Tabs.List class="grid grid-cols-4 fixed bottom-24 left-1/2 -translate-x-1/2 w-[95%] max-w-2xl z-40 shadow-2xl bg-background border p-1 rounded-xl">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="programas">Programas</Tabs.Trigger>
|
||||
<Tabs.Trigger value="responsable">Responsable</Tabs.Trigger>
|
||||
<Tabs.Trigger value="config">Config</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50">
|
||||
<div class="max-w-6xl mx-auto flex justify-end gap-4">
|
||||
<Button type="button" variant="ghost" onclick={() => goto('/dashboard/general_catalogs/company_information')} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading} class="min-w-[140px]">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Save class="mr-2 h-4 w-4" />
|
||||
{/if}
|
||||
{isEdit ? 'Actualizar' : 'Guardar'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -1,247 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { createCompany } from '$lib/api/dashboard/a76/general_catalogs/company';
|
||||
import { ArrowLeft, LoaderCircle, Save } from 'lucide-svelte';
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let formData = $state({
|
||||
name: '',
|
||||
rfc: '',
|
||||
curp: '',
|
||||
main_activity: '',
|
||||
program: '',
|
||||
program_number: '',
|
||||
prosec: 0,
|
||||
prosec_authorization: '',
|
||||
responsible_name: '',
|
||||
responsible_last_name: '',
|
||||
responsible_mother_last_name: '',
|
||||
responsible_rfc: '',
|
||||
position: '',
|
||||
manufacturer_id: '',
|
||||
has_express_line: false,
|
||||
is_service_company: false,
|
||||
order_format_type: '',
|
||||
ctpat_svi: '',
|
||||
trusted_exporter_number: ''
|
||||
});
|
||||
|
||||
const clean = (value: string) => value.trim() || null;
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
if (!formData.name.trim()) throw new Error('El nombre es requerido');
|
||||
if (!formData.rfc.trim()) throw new Error('El RFC es requerido');
|
||||
|
||||
const payload = {
|
||||
name: formData.name.trim(),
|
||||
rfc: formData.rfc.trim(),
|
||||
curp: clean(formData.curp),
|
||||
main_activity: clean(formData.main_activity),
|
||||
program: clean(formData.program),
|
||||
program_number: clean(formData.program_number),
|
||||
prosec: formData.prosec || null,
|
||||
prosec_authorization: clean(formData.prosec_authorization),
|
||||
responsible: `${formData.responsible_name} ${formData.responsible_last_name}`.trim() || null,
|
||||
responsible_name: clean(formData.responsible_name),
|
||||
responsible_last_name: clean(formData.responsible_last_name),
|
||||
responsible_mother_last_name: clean(formData.responsible_mother_last_name),
|
||||
responsible_rfc: clean(formData.responsible_rfc),
|
||||
position: clean(formData.position),
|
||||
manufacturer_id: clean(formData.manufacturer_id),
|
||||
has_express_line: formData.has_express_line,
|
||||
is_service_company: formData.is_service_company,
|
||||
order_format_type: clean(formData.order_format_type),
|
||||
ctpat_svi: clean(formData.ctpat_svi),
|
||||
trusted_exporter_number: clean(formData.trusted_exporter_number)
|
||||
};
|
||||
|
||||
const response = await createCompany(payload);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
await goto('/dashboard/general_catalogs/company_information');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
goto('/dashboard/general_catalogs/company_information');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full mx-auto max-w-6xl py-6 px-4 space-y-6 pb-48">
|
||||
<div class="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" href="/dashboard/general_catalogs/company_information">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Nueva Empresa</h1>
|
||||
<p class="text-muted-foreground">Crea la información de la empresa.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form class="space-y-6" on:submit|preventDefault={handleSubmit}>
|
||||
<Card.Root>
|
||||
<Card.Content class="p-6">
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.Content value="general" class="space-y-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Razón Social <span class="text-destructive">*</span></Label>
|
||||
<Input id="name" bind:value={formData.name} placeholder="Nombre de la empresa" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="rfc">RFC <span class="text-destructive">*</span></Label>
|
||||
<Input id="rfc" bind:value={formData.rfc} maxlength={13} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="curp">CURP</Label>
|
||||
<Input id="curp" bind:value={formData.curp} maxlength={18} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="main_activity">Actividad Principal</Label>
|
||||
<Input id="main_activity" bind:value={formData.main_activity} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="programas" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="program">Programa (IMMEX)</Label>
|
||||
<Input id="program" bind:value={formData.program} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="program_number">No. Programa</Label>
|
||||
<Input id="program_number" bind:value={formData.program_number} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="prosec">Sector PROSEC (ID)</Label>
|
||||
<Input id="prosec" type="number" bind:value={formData.prosec} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="prosec_auth">Autorización PROSEC</Label>
|
||||
<Input id="prosec_auth" bind:value={formData.prosec_authorization} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="responsable" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_name">Nombre</Label>
|
||||
<Input id="resp_name" bind:value={formData.responsible_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_last">Apellido Paterno</Label>
|
||||
<Input id="resp_last" bind:value={formData.responsible_last_name} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_mother">Apellido Materno</Label>
|
||||
<Input id="resp_mother" bind:value={formData.responsible_mother_last_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="resp_rfc">RFC Responsable</Label>
|
||||
<Input id="resp_rfc" bind:value={formData.responsible_rfc} maxlength={13} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="position">Puesto / Cargo</Label>
|
||||
<Input id="position" bind:value={formData.position} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="config" class="space-y-4">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="express" bind:checked={formData.has_express_line} />
|
||||
<Label for="express">Carril Exprés (OEA/NEEC)</Label>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="service" bind:checked={formData.is_service_company} />
|
||||
<Label for="service">Es Empresa de Servicios</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t" />
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="man_id">Manufacturer ID (MID)</Label>
|
||||
<Input id="man_id" bind:value={formData.manufacturer_id} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="order_format">Formato de Pedido</Label>
|
||||
<Input id="order_format" bind:value={formData.order_format_type} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="ctpat">C-TPAT / SVI</Label>
|
||||
<Input id="ctpat" bind:value={formData.ctpat_svi} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="exporter">No. Exportador Confiable</Label>
|
||||
<Input id="exporter" bind:value={formData.trusted_exporter_number} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.List class="grid grid-cols-4 fixed bottom-24 left-1/2 -translate-x-1/2 w-[90%] max-w-2xl z-40 shadow-xl bg-background border rounded-xl">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="programas">Programas</Tabs.Trigger>
|
||||
<Tabs.Trigger value="responsable">Responsable</Tabs.Trigger>
|
||||
<Tabs.Trigger value="config">Config</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
|
||||
<div class="w-full mx-auto flex justify-end gap-4 px-4">
|
||||
<Button type="button" variant="outline" onclick={handleCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
<Save class="mr-2 h-4 w-4" />
|
||||
Guardar Empresa
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -6,48 +6,44 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
if (!accessToken) {
|
||||
return { error: 'No authenticated', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
return { error: 'No authenticated', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const page = Number(url.searchParams.get('page')) || 1;
|
||||
const pageSize = Number(url.searchParams.get('pageSize')) || 50;
|
||||
const filters: Record<string, string> = {};
|
||||
const code = url.searchParams.get('code');
|
||||
const description = url.searchParams.get('description');
|
||||
|
||||
if (code) filters.code = code;
|
||||
if (description) filters.description = description;
|
||||
|
||||
// Obtener company_id de la cookie o usar el primero disponible
|
||||
const parentData = await parent();
|
||||
const cookieCompanyId = cookies.get('active_company_id');
|
||||
const companyId = cookieCompanyId
|
||||
? parseInt(cookieCompanyId)
|
||||
: parentData.companies?.[0]?.id;
|
||||
const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id');
|
||||
|
||||
if (!companyId) {
|
||||
return {
|
||||
error: 'No se encontró una compañía seleccionada',
|
||||
concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }
|
||||
};
|
||||
return { error: 'No company selected', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const brokerKey = url.searchParams.get('broker_key');
|
||||
const concept = url.searchParams.get('concept');
|
||||
|
||||
if (brokerKey) filters.broker_key = brokerKey;
|
||||
if (concept) filters.concept = concept;
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId,
|
||||
...filters
|
||||
});
|
||||
const response = await authenticatedFetch(`v1/a76/customs-broker-concepts?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
return { error: 'Failed to load', concepts: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
|
||||
return { error: 'Failed to load', concepts: { items: [], total: 0, page, page_size: pageSize, pages: 0 }, activeCompanyId: companyId };
|
||||
}
|
||||
|
||||
return { concepts: await response.json() };
|
||||
const data = await response.json();
|
||||
// Calculate pages if not provided by API
|
||||
const pages = data.pages || Math.ceil(data.total / pageSize);
|
||||
|
||||
return { concepts: { ...data, pages }, activeCompanyId: parseInt(companyId) };
|
||||
} catch (error) {
|
||||
console.error('Error loading customs broker concepts:', error);
|
||||
return { error: 'Error loading', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 } };
|
||||
return { error: 'Error loading', concepts: { items: [], total: 0, page: 1, page_size: 50, pages: 0 }, activeCompanyId: null };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
let dialogOpen = $state(false);
|
||||
|
||||
// Filtros
|
||||
let searchCode = $state($page.url.searchParams.get('code') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let searchBrokerKey = $state($page.url.searchParams.get('broker_key') || '');
|
||||
let searchConcept = $state($page.url.searchParams.get('concept') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
// Solo necesitamos una declaración de handleSuccess
|
||||
@@ -31,11 +31,11 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchCode) url.searchParams.set('code', searchCode);
|
||||
else url.searchParams.delete('code');
|
||||
if (searchBrokerKey) url.searchParams.set('broker_key', searchBrokerKey);
|
||||
else url.searchParams.delete('broker_key');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
if (searchConcept) url.searchParams.set('concept', searchConcept);
|
||||
else url.searchParams.delete('concept');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
@@ -60,15 +60,15 @@
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por código..."
|
||||
bind:value={searchCode}
|
||||
placeholder="Buscar por clave de agente..."
|
||||
bind:value={searchBrokerKey}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
placeholder="Buscar por concepto..."
|
||||
bind:value={searchConcept}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
@@ -85,6 +85,7 @@
|
||||
|
||||
<CreateEditDialog
|
||||
bind:open={dialogOpen}
|
||||
companyId={data.activeCompanyId}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<!-- <Button onclick={() => dialogOpen = true}> -->
|
||||
<Button href="/dashboard/general_catalogs/doda/new">
|
||||
<Button href="/dashboard/general_catalogs/doda/edit">
|
||||
<Plus class="mr-2 h-4 w-4" />
|
||||
Nuevo DODA
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import {
|
||||
createDoda,
|
||||
updateDoda,
|
||||
getDoda,
|
||||
type DodaCreate
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { ArrowLeft, LoaderCircle, Save } from 'lucide-svelte';
|
||||
|
||||
// 1. Identificación reactiva
|
||||
let id = $derived($page.params.id);
|
||||
let isEdit = $derived(!!id);
|
||||
let title = $derived(isEdit ? "Editar DODA" : "Nuevo DODA");
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
function getEmptyForm(): DodaCreate {
|
||||
return {
|
||||
integration_number: '', doda_date: undefined, doda_time: undefined,
|
||||
dispatch_customs: '', customs_sections: '', patent: '',
|
||||
pedimentos: '', caat: '', transport_identification: '',
|
||||
fast_id: '', operation_type: '', selected: false,
|
||||
user_selected: '', last_user: '', responsible: '',
|
||||
carrier: '', shipments: '', pedimento_type: '',
|
||||
original_chain: '', serial_number: '', electronic_signature: '',
|
||||
transaction_number: '', status: '', linq_sat_qr: '',
|
||||
sat_certificate: '', sat_digital_seal: '', xml_doda_sent_path: '',
|
||||
xml_doda_response_path: '', sat_original_chain: '',
|
||||
customs_clearance: undefined, unique_badge_number: ''
|
||||
};
|
||||
}
|
||||
|
||||
let formData = $state<DodaCreate>(getEmptyForm());
|
||||
|
||||
$effect(() => {
|
||||
if (id) {
|
||||
loadDoda(Number(id));
|
||||
} else {
|
||||
formData = getEmptyForm();
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
async function loadDoda(dodaId: number) {
|
||||
loading = true;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
const data = await getDoda(dodaId, companyId);
|
||||
|
||||
if (data) {
|
||||
|
||||
formData = {
|
||||
integration_number: data.integration_number || '',
|
||||
doda_date: data.doda_date,
|
||||
doda_time: data.doda_time,
|
||||
dispatch_customs: data.dispatch_customs || '',
|
||||
customs_sections: data.customs_sections || '',
|
||||
patent: data.patent || '',
|
||||
pedimentos: data.pedimentos || '',
|
||||
caat: data.caat || '',
|
||||
transport_identification: data.transport_identification || '',
|
||||
fast_id: data.fast_id || '',
|
||||
operation_type: data.operation_type || '',
|
||||
selected: data.selected || false,
|
||||
user_selected: data.user_selected || '',
|
||||
last_user: data.last_user || '',
|
||||
responsible: data.responsible || '',
|
||||
carrier: data.carrier || '',
|
||||
shipments: data.shipments || '',
|
||||
pedimento_type: data.pedimento_type || '',
|
||||
original_chain: data.original_chain || '',
|
||||
serial_number: data.serial_number || '',
|
||||
electronic_signature: data.electronic_signature || '',
|
||||
transaction_number: data.transaction_number || '',
|
||||
status: data.status || '',
|
||||
linq_sat_qr: data.linq_sat_qr || '',
|
||||
sat_certificate: data.sat_certificate || '',
|
||||
sat_digital_seal: data.sat_digital_seal || '',
|
||||
xml_doda_sent_path: data.xml_doda_sent_path || '',
|
||||
xml_doda_response_path: data.xml_doda_response_path || '',
|
||||
sat_original_chain: data.sat_original_chain || '',
|
||||
customs_clearance: data.customs_clearance,
|
||||
unique_badge_number: data.unique_badge_number || ''
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
error = "No se pudo cargar la información del DODA";
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('Selecciona una compañía');
|
||||
if (!formData.integration_number?.trim()) throw new Error('El número de integración es requerido');
|
||||
|
||||
|
||||
const payload: DodaCreate = {
|
||||
...formData,
|
||||
integration_number: formData.integration_number.trim(),
|
||||
|
||||
doda_date: formData.doda_date || undefined,
|
||||
doda_time: formData.doda_time || undefined,
|
||||
customs_clearance: formData.customs_clearance || undefined
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
await updateDoda(Number(id), payload, companyId);
|
||||
} else {
|
||||
await createDoda(payload, companyId);
|
||||
}
|
||||
|
||||
goto('/dashboard/general_catalogs/doda');
|
||||
} catch (e: any) {
|
||||
error = e.message || 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full mx-auto max-w-6xl py-6 px-4 space-y-6 pb-48">
|
||||
<div class="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" href="/dashboard/general_catalogs/doda">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#key id}
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-6">
|
||||
<Card.Root>
|
||||
<Card.Content class="p-6">
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<div class="min-h-[500px]">
|
||||
<Tabs.Content value="general" class="space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="integration_number">No. Integración <span class="text-destructive">*</span></Label>
|
||||
<Input id="integration_number" bind:value={formData.integration_number} maxlength={30} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_date">Fecha (YYYYMMDD)</Label>
|
||||
<Input type="number" id="doda_date" bind:value={formData.doda_date} placeholder="Ej: 20240101" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_time">Hora (HHMMSS)</Label>
|
||||
<Input type="number" id="doda_time" bind:value={formData.doda_time} placeholder="Ej: 143000" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="operation_type">Tipo Operación</Label>
|
||||
<Input id="operation_type" bind:value={formData.operation_type} maxlength={1} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimentos">Pedimentos</Label>
|
||||
<Input id="pedimentos" bind:value={formData.pedimentos} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento_type">Tipo Pedimento</Label>
|
||||
<Input id="pedimento_type" bind:value={formData.pedimento_type} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="transport" class="space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} maxlength={4} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="dispatch_customs">Aduana Despacho</Label>
|
||||
<Input id="dispatch_customs" bind:value={formData.dispatch_customs} maxlength={3} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_sections">Sección Aduanera</Label>
|
||||
<Input id="customs_sections" bind:value={formData.customs_sections} maxlength={3} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="caat">CAAT</Label>
|
||||
<Input id="caat" bind:value={formData.caat} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="carrier">Carrier</Label>
|
||||
<Input id="carrier" bind:value={formData.carrier} />
|
||||
</div>
|
||||
<div class="grid gap-2 md:col-span-2">
|
||||
<Label for="transport_id">Ident. Transporte</Label>
|
||||
<Input id="transport_id" bind:value={formData.transport_identification} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fast_id">FAST ID</Label>
|
||||
<Input id="fast_id" bind:value={formData.fast_id} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="shipments">Embarques (Shipments)</Label>
|
||||
<Input id="shipments" bind:value={formData.shipments} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_clearance">Despacho Aduanero (ID)</Label>
|
||||
<Input type="number" id="customs_clearance" bind:value={formData.customs_clearance} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="sat" class="space-y-4 pt-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="serial">Número de Serie</Label>
|
||||
<Input id="serial" bind:value={formData.serial_number} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="transaction">No. Transacción</Label>
|
||||
<Input id="transaction" bind:value={formData.transaction_number} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="chain">Cadena Original</Label>
|
||||
<Textarea id="chain" bind:value={formData.original_chain} class="min-h-[80px]" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="signature">Firma Electrónica</Label>
|
||||
<Textarea id="signature" bind:value={formData.electronic_signature} class="min-h-[80px]" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="seal">Sello Digital SAT</Label>
|
||||
<Textarea id="seal" bind:value={formData.sat_digital_seal} class="min-h-[80px]" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_original_chain">Cadena Original SAT</Label>
|
||||
<Textarea id="sat_original_chain" bind:value={formData.sat_original_chain} class="min-h-[80px]" />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_sent">Ruta XML Enviado</Label>
|
||||
<Input id="xml_sent" bind:value={formData.xml_doda_sent_path} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_res">Ruta XML Respuesta</Label>
|
||||
<Input id="xml_res" bind:value={formData.xml_doda_response_path} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="other" class="space-y-4 pt-4">
|
||||
<div class="flex items-center gap-3 p-4 border rounded-lg">
|
||||
<Switch id="selected" bind:checked={formData.selected} />
|
||||
<Label for="selected">DODA Seleccionado para operación</Label>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="user_sel">Usuario Selección</Label>
|
||||
<Input id="user_sel" bind:value={formData.user_selected} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="last_user">Último Usuario</Label>
|
||||
<Input id="last_user" bind:value={formData.last_user} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="responsible">RFC Responsable</Label>
|
||||
<Input id="responsible" bind:value={formData.responsible} maxlength={14} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="badge">Número Único de Gafete</Label>
|
||||
<Input id="badge" bind:value={formData.unique_badge_number} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="qr">LINQ SAT QR</Label>
|
||||
<Input id="qr" bind:value={formData.linq_sat_qr} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_cert">Certificado SAT</Label>
|
||||
<Input id="sat_cert" bind:value={formData.sat_certificate} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</div>
|
||||
|
||||
<Tabs.List class="grid grid-cols-4 fixed bottom-24 left-1/2 -translate-x-1/2 w-[95%] max-w-2xl z-40 shadow-2xl bg-background border p-1 rounded-xl">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transport">Aduana</Tabs.Trigger>
|
||||
<Tabs.Trigger value="sat">SAT</Tabs.Trigger>
|
||||
<Tabs.Trigger value="other">Otros</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-inner">
|
||||
<div class="max-w-6xl mx-auto flex justify-end gap-4 px-4 w-full">
|
||||
<Button type="button" variant="ghost" href="/dashboard/general_catalogs/doda" disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading} class="min-w-[140px]">
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
<Save class="mr-2 h-4 w-4" />
|
||||
{isEdit ? 'Actualizar' : 'Guardar'}
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{/key}
|
||||
</div>
|
||||
@@ -1,277 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Textarea } from '$lib/components/ui/textarea';
|
||||
import { Switch } from '$lib/components/ui/switch';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { createDoda } from '$lib/api/dashboard/a76/general_catalogs/doda';
|
||||
import { ArrowLeft, LoaderCircle, Save } from 'lucide-svelte';
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let formData = $state({
|
||||
integration_number: '',
|
||||
doda_date: undefined as number | undefined,
|
||||
doda_time: undefined as number | undefined,
|
||||
dispatch_customs: '',
|
||||
customs_sections: '',
|
||||
patent: '',
|
||||
pedimentos: '',
|
||||
caat: '',
|
||||
transport_identification: '',
|
||||
fast_id: '',
|
||||
operation_type: '',
|
||||
selected: false,
|
||||
user_selected: '',
|
||||
last_user: '',
|
||||
responsible: '',
|
||||
carrier: '',
|
||||
shipments: '',
|
||||
pedimento_type: '',
|
||||
original_chain: '',
|
||||
serial_number: '',
|
||||
electronic_signature: '',
|
||||
transaction_number: '',
|
||||
status: '',
|
||||
linq_sat_qr: '',
|
||||
sat_certificate: '',
|
||||
sat_digital_seal: '',
|
||||
xml_doda_sent_path: '',
|
||||
xml_doda_response_path: '',
|
||||
sat_original_chain: '',
|
||||
customs_clearance: undefined as number | undefined,
|
||||
unique_badge_number: ''
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
if (!formData.integration_number.trim()) throw new Error('El número de integración es requerido');
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('Selecciona una compañía para crear el DODA');
|
||||
|
||||
const payload = { ...formData, integration_number: formData.integration_number.trim() };
|
||||
await createDoda(payload, companyId);
|
||||
|
||||
await goto('/dashboard/general_catalogs/doda');
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
goto('/dashboard/general_catalogs/doda');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full mx-auto max-w-6xl py-6 px-4 space-y-6 pb-48">
|
||||
<div class="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" href="/dashboard/general_catalogs/doda">
|
||||
<ArrowLeft class="h-4 w-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Nuevo DODA</h1>
|
||||
<p class="text-muted-foreground">Captura la información del documento.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium">
|
||||
⚠️ {error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form class="space-y-6" on:submit|preventDefault={handleSubmit}>
|
||||
<Card.Root>
|
||||
<Card.Content class="p-6">
|
||||
<Tabs.Root value="general" class="w-full">
|
||||
<Tabs.Content value="general" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="integration_number">No. Integración <span class="text-destructive">*</span></Label>
|
||||
<Input id="integration_number" bind:value={formData.integration_number} maxlength={30} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Estatus</Label>
|
||||
<Input id="status" bind:value={formData.status} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_date">Fecha (YYYYMMDD)</Label>
|
||||
<Input type="number" id="doda_date" bind:value={formData.doda_date} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="doda_time">Hora (HHMMSS)</Label>
|
||||
<Input type="number" id="doda_time" bind:value={formData.doda_time} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="operation_type">Tipo Operación</Label>
|
||||
<Input id="operation_type" bind:value={formData.operation_type} maxlength={1} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimentos">Pedimentos</Label>
|
||||
<Input id="pedimentos" bind:value={formData.pedimentos} maxlength={80} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="pedimento_type">Tipo Pedimento</Label>
|
||||
<Input id="pedimento_type" bind:value={formData.pedimento_type} maxlength={30} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="transport" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="patent">Patente</Label>
|
||||
<Input id="patent" bind:value={formData.patent} maxlength={4} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="dispatch_customs">Aduana Despacho</Label>
|
||||
<Input id="dispatch_customs" bind:value={formData.dispatch_customs} maxlength={3} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_sections">Sección Aduanera</Label>
|
||||
<Input id="customs_sections" bind:value={formData.customs_sections} maxlength={3} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="caat">CAAT</Label>
|
||||
<Input id="caat" bind:value={formData.caat} maxlength={10} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="carrier">Transportista (Carrier)</Label>
|
||||
<Input id="carrier" bind:value={formData.carrier} maxlength={8} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="transport_identification">Ident. Transporte</Label>
|
||||
<Input id="transport_identification" bind:value={formData.transport_identification} maxlength={20} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="fast_id">FAST ID</Label>
|
||||
<Input id="fast_id" bind:value={formData.fast_id} maxlength={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="shipments">Embarques (Shipments)</Label>
|
||||
<Input id="shipments" bind:value={formData.shipments} maxlength={80} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="customs_clearance">Despacho Aduanero (ID)</Label>
|
||||
<Input type="number" id="customs_clearance" bind:value={formData.customs_clearance} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="sat" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="serial_number">Número de Serie</Label>
|
||||
<Input id="serial_number" bind:value={formData.serial_number} maxlength={21} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="transaction_number">No. Transacción</Label>
|
||||
<Input id="transaction_number" bind:value={formData.transaction_number} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="unique_badge_number">Número Único de Gafete</Label>
|
||||
<Input id="unique_badge_number" bind:value={formData.unique_badge_number} maxlength={250} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="original_chain">Cadena Original</Label>
|
||||
<Textarea id="original_chain" bind:value={formData.original_chain} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="electronic_signature">Firma Electrónica</Label>
|
||||
<Textarea id="electronic_signature" bind:value={formData.electronic_signature} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_digital_seal">Sello Digital SAT</Label>
|
||||
<Textarea id="sat_digital_seal" bind:value={formData.sat_digital_seal} class="h-20" />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_original_chain">Cadena Original SAT</Label>
|
||||
<Textarea id="sat_original_chain" bind:value={formData.sat_original_chain} class="h-20" />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_doda_sent_path">Ruta XML Enviado</Label>
|
||||
<Input id="xml_doda_sent_path" bind:value={formData.xml_doda_sent_path} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="xml_doda_response_path">Ruta XML Respuesta</Label>
|
||||
<Input id="xml_doda_response_path" bind:value={formData.xml_doda_response_path} />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="other" class="space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch id="selected" bind:checked={formData.selected} />
|
||||
<Label for="selected">Seleccionado</Label>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="user_selected">Usuario Selección</Label>
|
||||
<Input id="user_selected" bind:value={formData.user_selected} maxlength={30} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="last_user">Último Usuario</Label>
|
||||
<Input id="last_user" bind:value={formData.last_user} maxlength={30} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="responsible">Responsable</Label>
|
||||
<Input id="responsible" bind:value={formData.responsible} maxlength={14} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="linq_sat_qr">LINQ SAT QR</Label>
|
||||
<Input id="linq_sat_qr" bind:value={formData.linq_sat_qr} maxlength={1000} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="sat_certificate">Certificado SAT</Label>
|
||||
<Input id="sat_certificate" bind:value={formData.sat_certificate} maxlength={2001} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.List class="grid grid-cols-4 fixed bottom-24 left-1/2 -translate-x-1/2 w-[90%] max-w-3xl z-40 shadow-xl bg-background border rounded-xl">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="transport">Aduana/Transp.</Tabs.Trigger>
|
||||
<Tabs.Trigger value="sat">SAT / Digital</Tabs.Trigger>
|
||||
<Tabs.Trigger value="other">Otros</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</Tabs.Root>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<div class="fixed bottom-0 right-0 left-0 md:left-64 p-4 border-t bg-background/95 backdrop-blur z-50 flex justify-end gap-4 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)]">
|
||||
<div class="w-full mx-auto flex justify-end gap-4 px-4">
|
||||
<Button type="button" variant="outline" onclick={handleCancel}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
<Save class="mr-2 h-4 w-4" />
|
||||
Guardar DODA
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -2,9 +2,9 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/electronic-notices/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/electronic-notices/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/electronic-notices/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/general_catalogs/electronic_notices/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/general_catalogs/electronic_notices/columns.js';
|
||||
import CreateEditDialog from '$lib/components/dashboard/general_catalogs/electronic_notices/create-edit-dialog.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
import { page } from '$app/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { browser } from '$app/environment';
|
||||
import { createColumns } from '$lib/components/dashboard/exchange-rate/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/exchange-rate/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/exchange-rate/data-table.svelte';
|
||||
import { createColumns } from '$lib/components/dashboard/exchange_rate/columns';
|
||||
import CreateEditDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
|
||||
import DataTable from '$lib/components/dashboard/exchange_rate/data-table.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Plus } from 'lucide-svelte';
|
||||
|
||||
Reference in New Issue
Block a user