feature/presets-invoices-and-teplates-partidas
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
"""create invoice_settings table
|
||||||
|
|
||||||
|
Revision ID: 703f96cf6025
|
||||||
|
Revises: 7937209f9718
|
||||||
|
Create Date: 2026-02-09 23:02:43.174682
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '703f96cf6025'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '7937209f9718'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
pass
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""create item_presets table
|
||||||
|
|
||||||
|
Revision ID: 8a607a5e88bf
|
||||||
|
Revises: 703f96cf6025
|
||||||
|
Create Date: 2026-02-10 15:49:59.617922
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '8a607a5e88bf'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '703f96cf6025'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
pass
|
||||||
28
backend/api/v1/modules/a76/invoice_settings/dto.py
Normal file
28
backend/api/v1/modules/a76/invoice_settings/dto.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class OperationType(str, Enum):
|
||||||
|
IMP = "imp" # Importación
|
||||||
|
EXP = "exp" # Exportación
|
||||||
|
SM_IN = "sm_in" # Entrada SM
|
||||||
|
SM_OUT = "sm_out" # Salida SM
|
||||||
|
CTM_SEND = "ctm_send" # Envío CTM
|
||||||
|
CTM_RECEIVE = "ctm_receive" # Recibo CTM
|
||||||
|
|
||||||
|
class InvoiceSettingsBase(BaseModel):
|
||||||
|
invoice_type: str
|
||||||
|
operation_type: OperationType
|
||||||
|
settings: Dict[str, Any]
|
||||||
|
|
||||||
|
class InvoiceSettingsRequest(InvoiceSettingsBase):
|
||||||
|
"""Schema for creating or updating invoice settings"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class InvoiceSettingsResponse(InvoiceSettingsBase):
|
||||||
|
"""Schema for returning invoice settings"""
|
||||||
|
id: int
|
||||||
|
tenant_id: int
|
||||||
|
company_id: int
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
47
backend/api/v1/modules/a76/invoice_settings/models.py
Normal file
47
backend/api/v1/modules/a76/invoice_settings/models.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from sqlalchemy import BigInteger, ForeignKey, String, UniqueConstraint
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from core.database import Base
|
||||||
|
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||||
|
import enum
|
||||||
|
|
||||||
|
class OperationType(str, enum.Enum):
|
||||||
|
IMP = "imp" # Importación
|
||||||
|
EXP = "exp" # Exportación
|
||||||
|
SM_IN = "sm_in" # Entrada SM
|
||||||
|
SM_OUT = "sm_out" # Salida SM
|
||||||
|
CTM_SEND = "ctm_send" # Envío CTM
|
||||||
|
CTM_RECEIVE = "ctm_receive" # Recibo CTM
|
||||||
|
|
||||||
|
class InvoiceSettings(Base, TenantScopedMixin, TimestampMixin):
|
||||||
|
__tablename__ = "invoice_settings"
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"tenant_id",
|
||||||
|
"company_id",
|
||||||
|
"invoice_type",
|
||||||
|
"operation_type",
|
||||||
|
name="uq_invoice_settings_tenant_company_type_op"
|
||||||
|
),
|
||||||
|
{"schema": "a76"},
|
||||||
|
)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||||
|
|
||||||
|
# Configuration Scope
|
||||||
|
invoice_type: Mapped[str] = mapped_column(
|
||||||
|
ForeignKey("public.invoice_types.key"),
|
||||||
|
nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
operation_type: Mapped[OperationType] = mapped_column(
|
||||||
|
String(11),
|
||||||
|
nullable=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# The actual settings payload
|
||||||
|
settings: Mapped[dict] = mapped_column(JSONB, nullable=False, default={})
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<InvoiceSettings(id={self.id}, type={self.invoice_type}, op={self.operation_type})>"
|
||||||
76
backend/api/v1/modules/a76/invoice_settings/routes.py
Normal file
76
backend/api/v1/modules/a76/invoice_settings/routes.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from typing import List, Dict, Any
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from core.database import get_core_db
|
||||||
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
from api.v1.modules.a76.invoice_settings import services
|
||||||
|
from api.v1.modules.a76.invoice_settings.dto import InvoiceSettingsRequest, InvoiceSettingsResponse, OperationType
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/a76/invoice-settings",
|
||||||
|
tags=["a76/invoice-settings"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.get("/{invoice_type}", response_model=InvoiceSettingsResponse)
|
||||||
|
def get_invoice_settings(
|
||||||
|
invoice_type: str,
|
||||||
|
operation_type: OperationType = Query(...),
|
||||||
|
company_id: int = Query(...),
|
||||||
|
db: Session = Depends(get_core_db),
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get settings for a specific invoice type and operation"""
|
||||||
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
|
|
||||||
|
settings = services.get_settings(
|
||||||
|
db,
|
||||||
|
tenant_id,
|
||||||
|
company_id,
|
||||||
|
invoice_type,
|
||||||
|
operation_type
|
||||||
|
)
|
||||||
|
|
||||||
|
if not settings:
|
||||||
|
# Return empty default if not found, to simplify frontend logic
|
||||||
|
return InvoiceSettingsResponse(
|
||||||
|
invoice_type=invoice_type,
|
||||||
|
operation_type=operation_type,
|
||||||
|
settings={},
|
||||||
|
id=0,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
company_id=company_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return settings
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[InvoiceSettingsResponse])
|
||||||
|
def list_invoice_settings(
|
||||||
|
company_id: int = Query(...),
|
||||||
|
db: Session = Depends(get_core_db),
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""List all configured settings for validation or overview"""
|
||||||
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
|
|
||||||
|
return services.list_settings(
|
||||||
|
db,
|
||||||
|
tenant_id,
|
||||||
|
company_id
|
||||||
|
)
|
||||||
|
|
||||||
|
@router.put("/", response_model=InvoiceSettingsResponse)
|
||||||
|
def save_invoice_settings(
|
||||||
|
settings_data: InvoiceSettingsRequest,
|
||||||
|
company_id: int = Query(...),
|
||||||
|
db: Session = Depends(get_core_db),
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Create or update invoice settings"""
|
||||||
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
|
|
||||||
|
return services.upsert_settings(
|
||||||
|
db,
|
||||||
|
tenant_id,
|
||||||
|
company_id,
|
||||||
|
settings_data
|
||||||
|
)
|
||||||
70
backend/api/v1/modules/a76/invoice_settings/services.py
Normal file
70
backend/api/v1/modules/a76/invoice_settings/services.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy import select
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from api.v1.modules.a76.invoice_settings.models import InvoiceSettings
|
||||||
|
from api.v1.modules.a76.invoice_settings.dto import InvoiceSettingsRequest, OperationType
|
||||||
|
|
||||||
|
def get_settings(
|
||||||
|
db: Session,
|
||||||
|
tenant_id: int,
|
||||||
|
company_id: int,
|
||||||
|
invoice_type: str,
|
||||||
|
operation_type: OperationType
|
||||||
|
) -> Optional[InvoiceSettings]:
|
||||||
|
"""Retrieve settings for a specific context"""
|
||||||
|
stmt = select(InvoiceSettings).where(
|
||||||
|
InvoiceSettings.tenant_id == tenant_id,
|
||||||
|
InvoiceSettings.company_id == company_id,
|
||||||
|
InvoiceSettings.invoice_type == invoice_type,
|
||||||
|
InvoiceSettings.operation_type == operation_type
|
||||||
|
)
|
||||||
|
return db.execute(stmt).scalar_one_or_none()
|
||||||
|
|
||||||
|
def list_settings(
|
||||||
|
db: Session,
|
||||||
|
tenant_id: int,
|
||||||
|
company_id: int
|
||||||
|
) -> List[InvoiceSettings]:
|
||||||
|
"""List all settings for a company"""
|
||||||
|
stmt = select(InvoiceSettings).where(
|
||||||
|
InvoiceSettings.tenant_id == tenant_id,
|
||||||
|
InvoiceSettings.company_id == company_id
|
||||||
|
)
|
||||||
|
return db.execute(stmt).scalars().all()
|
||||||
|
|
||||||
|
def upsert_settings(
|
||||||
|
db: Session,
|
||||||
|
tenant_id: int,
|
||||||
|
company_id: int,
|
||||||
|
settings_data: InvoiceSettingsRequest
|
||||||
|
) -> InvoiceSettings:
|
||||||
|
"""Create or update settings"""
|
||||||
|
# Check if exists
|
||||||
|
existing = get_settings(
|
||||||
|
db,
|
||||||
|
tenant_id,
|
||||||
|
company_id,
|
||||||
|
settings_data.invoice_type,
|
||||||
|
settings_data.operation_type
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
existing.settings = settings_data.settings
|
||||||
|
db.commit()
|
||||||
|
db.refresh(existing)
|
||||||
|
return existing
|
||||||
|
|
||||||
|
# Create new
|
||||||
|
new_settings = InvoiceSettings(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
company_id=company_id,
|
||||||
|
invoice_type=settings_data.invoice_type,
|
||||||
|
operation_type=settings_data.operation_type,
|
||||||
|
settings=settings_data.settings
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(new_settings)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_settings)
|
||||||
|
return new_settings
|
||||||
25
backend/api/v1/modules/a76/item_presets/dto.py
Normal file
25
backend/api/v1/modules/a76/item_presets/dto.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
|
class ItemPresetBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
items: List[Dict[str, Any]]
|
||||||
|
|
||||||
|
class ItemPresetCreate(ItemPresetBase):
|
||||||
|
"""Schema for creating a new item preset"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ItemPresetUpdate(BaseModel):
|
||||||
|
"""Schema for updating an existing item preset"""
|
||||||
|
name: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
items: Optional[List[Dict[str, Any]]] = None
|
||||||
|
|
||||||
|
class ItemPresetResponse(ItemPresetBase):
|
||||||
|
"""Schema for returning an item preset"""
|
||||||
|
id: int
|
||||||
|
tenant_id: int
|
||||||
|
company_id: int
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
18
backend/api/v1/modules/a76/item_presets/models.py
Normal file
18
backend/api/v1/modules/a76/item_presets/models.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from typing import Any, Dict, List
|
||||||
|
from sqlalchemy import String, Integer, JSON
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from core.database import Base
|
||||||
|
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ItemPreset(Base, TenantScopedMixin, TimestampMixin):
|
||||||
|
__tablename__ = "item_presets"
|
||||||
|
__table_args__ = {"schema": "a76"}
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
|
description: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
||||||
|
items: Mapped[List[Dict[str, Any]]] = mapped_column(JSON, nullable=False, default=[])
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<ItemPreset(id={self.id}, name='{self.name}')>"
|
||||||
72
backend/api/v1/modules/a76/item_presets/routes.py
Normal file
72
backend/api/v1/modules/a76/item_presets/routes.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
from typing import List, Any, Dict
|
||||||
|
from fastapi import APIRouter, Depends, Query, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from core.database import get_core_db
|
||||||
|
from core.security import get_current_user, validate_access_to_resource
|
||||||
|
from . import services, dto
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[dto.ItemPresetResponse])
|
||||||
|
def list_item_presets(
|
||||||
|
company_id: int = Query(...),
|
||||||
|
db: Session = Depends(get_core_db),
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""List all item presets for a company"""
|
||||||
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
|
return services.get_presets(db, tenant_id, company_id)
|
||||||
|
|
||||||
|
@router.post("/", response_model=dto.ItemPresetResponse)
|
||||||
|
def create_item_preset(
|
||||||
|
data: dto.ItemPresetCreate,
|
||||||
|
company_id: int = Query(...),
|
||||||
|
db: Session = Depends(get_core_db),
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Create a new item preset"""
|
||||||
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
|
return services.create_preset(db, tenant_id, company_id, data)
|
||||||
|
|
||||||
|
@router.get("/{preset_id}", response_model=dto.ItemPresetResponse)
|
||||||
|
def get_item_preset(
|
||||||
|
preset_id: int,
|
||||||
|
company_id: int = Query(...),
|
||||||
|
db: Session = Depends(get_core_db),
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Get a specific item preset"""
|
||||||
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
|
preset = services.get_preset_by_id(db, preset_id, tenant_id, company_id)
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(status_code=404, detail="Item preset not found")
|
||||||
|
return preset
|
||||||
|
|
||||||
|
@router.put("/{preset_id}", response_model=dto.ItemPresetResponse)
|
||||||
|
def update_item_preset(
|
||||||
|
preset_id: int,
|
||||||
|
data: dto.ItemPresetUpdate,
|
||||||
|
company_id: int = Query(...),
|
||||||
|
db: Session = Depends(get_core_db),
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Update an item preset"""
|
||||||
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
|
preset = services.update_preset(db, preset_id, tenant_id, company_id, data)
|
||||||
|
if not preset:
|
||||||
|
raise HTTPException(status_code=404, detail="Item preset not found")
|
||||||
|
return preset
|
||||||
|
|
||||||
|
@router.delete("/{preset_id}")
|
||||||
|
def delete_item_preset(
|
||||||
|
preset_id: int,
|
||||||
|
company_id: int = Query(...),
|
||||||
|
db: Session = Depends(get_core_db),
|
||||||
|
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""Delete an item preset"""
|
||||||
|
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||||
|
success = services.delete_preset(db, preset_id, tenant_id, company_id)
|
||||||
|
if not success:
|
||||||
|
raise HTTPException(status_code=404, detail="Item preset not found")
|
||||||
|
return {"status": "success", "message": "Preset deleted"}
|
||||||
63
backend/api/v1/modules/a76/item_presets/services.py
Normal file
63
backend/api/v1/modules/a76/item_presets/services.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
from typing import List, Optional
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from .models import ItemPreset
|
||||||
|
from .dto import ItemPresetCreate, ItemPresetUpdate
|
||||||
|
|
||||||
|
def get_presets(db: Session, tenant_id: int, company_id: int) -> List[ItemPreset]:
|
||||||
|
"""Get all presets for a specific company and tenant"""
|
||||||
|
return db.query(ItemPreset).filter(
|
||||||
|
ItemPreset.tenant_id == tenant_id,
|
||||||
|
ItemPreset.company_id == company_id
|
||||||
|
).all()
|
||||||
|
|
||||||
|
def get_preset_by_id(db: Session, preset_id: int, tenant_id: int, company_id: int) -> Optional[ItemPreset]:
|
||||||
|
"""Get a specific preset by ID"""
|
||||||
|
return db.query(ItemPreset).filter(
|
||||||
|
ItemPreset.id == preset_id,
|
||||||
|
ItemPreset.tenant_id == tenant_id,
|
||||||
|
ItemPreset.company_id == company_id
|
||||||
|
).first()
|
||||||
|
|
||||||
|
def create_preset(db: Session, tenant_id: int, company_id: int, data: ItemPresetCreate) -> ItemPreset:
|
||||||
|
"""Create a new item preset"""
|
||||||
|
new_preset = ItemPreset(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
company_id=company_id,
|
||||||
|
name=data.name,
|
||||||
|
description=data.description,
|
||||||
|
items=data.items
|
||||||
|
)
|
||||||
|
db.add(new_preset)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(new_preset)
|
||||||
|
return new_preset
|
||||||
|
|
||||||
|
def update_preset(
|
||||||
|
db: Session,
|
||||||
|
preset_id: int,
|
||||||
|
tenant_id: int,
|
||||||
|
company_id: int,
|
||||||
|
data: ItemPresetUpdate
|
||||||
|
) -> Optional[ItemPreset]:
|
||||||
|
"""Update an existing item preset"""
|
||||||
|
preset = get_preset_by_id(db, preset_id, tenant_id, company_id)
|
||||||
|
if not preset:
|
||||||
|
return None
|
||||||
|
|
||||||
|
update_data = data.model_dump(exclude_unset=True)
|
||||||
|
for key, value in update_data.items():
|
||||||
|
setattr(preset, key, value)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(preset)
|
||||||
|
return preset
|
||||||
|
|
||||||
|
def delete_preset(db: Session, preset_id: int, tenant_id: int, company_id: int) -> bool:
|
||||||
|
"""Delete an item preset"""
|
||||||
|
preset = get_preset_by_id(db, preset_id, tenant_id, company_id)
|
||||||
|
if not preset:
|
||||||
|
return False
|
||||||
|
|
||||||
|
db.delete(preset)
|
||||||
|
db.commit()
|
||||||
|
return True
|
||||||
@@ -12,6 +12,8 @@ from .invoices.routes import router as invoices_router
|
|||||||
from .items.routes import router as items_router
|
from .items.routes import router as items_router
|
||||||
from .classes import router as classes_router
|
from .classes import router as classes_router
|
||||||
from .clients_and_providers import router as client_and_provider_router
|
from .clients_and_providers import router as client_and_provider_router
|
||||||
|
from .invoice_settings.routes import router as invoice_settings_router
|
||||||
|
from .item_presets.routes import router as item_presets_router
|
||||||
from .general_catalogs.company import router as company_router
|
from .general_catalogs.company import router as company_router
|
||||||
from .country_rule_oct.routes import router as country_rule_oct_router
|
from .country_rule_oct.routes import router as country_rule_oct_router
|
||||||
from .transportation.drivers.routes import router as drivers_router
|
from .transportation.drivers.routes import router as drivers_router
|
||||||
@@ -66,6 +68,8 @@ router = APIRouter()
|
|||||||
# Registrar módulos
|
# Registrar módulos
|
||||||
router.include_router(invoices_router, prefix="/a76", tags=["a76 / invoices"])
|
router.include_router(invoices_router, prefix="/a76", tags=["a76 / invoices"])
|
||||||
router.include_router(items_router, prefix="/a76", tags=["a76 / items"])
|
router.include_router(items_router, prefix="/a76", tags=["a76 / items"])
|
||||||
|
router.include_router(invoice_settings_router)
|
||||||
|
router.include_router(item_presets_router, prefix="/a76/item-presets", tags=["a76 / item_presets"])
|
||||||
router.include_router(pedimentos_router, prefix="/a76")
|
router.include_router(pedimentos_router, prefix="/a76")
|
||||||
router.include_router(
|
router.include_router(
|
||||||
client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"]
|
client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"]
|
||||||
|
|||||||
77
frontend/src/lib/api/dashboard/a76/item-presets.ts
Normal file
77
frontend/src/lib/api/dashboard/a76/item-presets.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { api } from '$lib/api';
|
||||||
|
import type { Item } from './items';
|
||||||
|
|
||||||
|
export interface ItemPreset {
|
||||||
|
id: number;
|
||||||
|
tenant_id: number;
|
||||||
|
company_id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
items: Item[];
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemPresetCreate {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
items: Item[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ItemPresetUpdate {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
items?: Item[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const itemPresetsApi = {
|
||||||
|
/**
|
||||||
|
* List all item presets for a company
|
||||||
|
*/
|
||||||
|
list: (companyId: number) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
company_id: companyId.toString()
|
||||||
|
});
|
||||||
|
return api.get<ItemPreset[]>(`/v1/a76/item-presets/?${params.toString()}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific item preset
|
||||||
|
*/
|
||||||
|
get: (presetId: number, companyId: number) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
company_id: companyId.toString()
|
||||||
|
});
|
||||||
|
return api.get<ItemPreset>(`/v1/a76/item-presets/${presetId}/?${params.toString()}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new item preset
|
||||||
|
*/
|
||||||
|
create: (companyId: number, data: ItemPresetCreate) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
company_id: companyId.toString()
|
||||||
|
});
|
||||||
|
return api.post<ItemPreset>(`/v1/a76/item-presets/?${params.toString()}`, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing item preset
|
||||||
|
*/
|
||||||
|
update: (presetId: number, companyId: number, data: ItemPresetUpdate) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
company_id: companyId.toString()
|
||||||
|
});
|
||||||
|
return api.put<ItemPreset>(`/v1/a76/item-presets/${presetId}/?${params.toString()}`, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an item preset
|
||||||
|
*/
|
||||||
|
delete: (presetId: number, companyId: number) => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
company_id: companyId.toString()
|
||||||
|
});
|
||||||
|
return api.delete(`/v1/a76/item-presets/${presetId}/?${params.toString()}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -24,10 +24,11 @@
|
|||||||
|
|
||||||
const filteredClasses = $derived(
|
const filteredClasses = $derived(
|
||||||
searchQuery
|
searchQuery
|
||||||
? classes.filter(c =>
|
? classes.filter(
|
||||||
c.class_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
(c) =>
|
||||||
c.description_es?.toLowerCase().includes(searchQuery.toLowerCase())
|
c.class_code?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
)
|
c.description_es?.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
)
|
||||||
: classes
|
: classes
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -86,7 +87,8 @@
|
|||||||
function handleScroll(e: Event) {
|
function handleScroll(e: Event) {
|
||||||
const target = e.target as HTMLDivElement;
|
const target = e.target as HTMLDivElement;
|
||||||
const threshold = 100;
|
const threshold = 100;
|
||||||
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
const scrolledToBottom =
|
||||||
|
target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||||
|
|
||||||
if (scrolledToBottom && displayedClasses.length < filteredClasses.length) {
|
if (scrolledToBottom && displayedClasses.length < filteredClasses.length) {
|
||||||
currentPage++;
|
currentPage++;
|
||||||
@@ -106,9 +108,7 @@
|
|||||||
<Dialog.Content class="max-w-4xl max-h-[80vh] flex flex-col">
|
<Dialog.Content class="max-w-4xl max-h-[80vh] flex flex-col">
|
||||||
<Dialog.Header>
|
<Dialog.Header>
|
||||||
<Dialog.Title>Seleccionar Clase</Dialog.Title>
|
<Dialog.Title>Seleccionar Clase</Dialog.Title>
|
||||||
<Dialog.Description>
|
<Dialog.Description>Busca y selecciona una clase para la partida</Dialog.Description>
|
||||||
Busca y selecciona una clase para la partida
|
|
||||||
</Dialog.Description>
|
|
||||||
</Dialog.Header>
|
</Dialog.Header>
|
||||||
|
|
||||||
<div class="flex gap-2 mb-4">
|
<div class="flex gap-2 mb-4">
|
||||||
@@ -146,9 +146,14 @@
|
|||||||
</Table.Row>
|
</Table.Row>
|
||||||
{:else}
|
{:else}
|
||||||
{#each displayedClasses as classItem}
|
{#each displayedClasses as classItem}
|
||||||
<Table.Row class="cursor-pointer hover:bg-muted/50" onclick={() => handleSelect(classItem)}>
|
<Table.Row
|
||||||
|
class="cursor-pointer hover:bg-muted/50"
|
||||||
|
onclick={() => handleSelect(classItem)}
|
||||||
|
>
|
||||||
<Table.Cell class="font-medium">{classItem.class_code}</Table.Cell>
|
<Table.Cell class="font-medium">{classItem.class_code}</Table.Cell>
|
||||||
<Table.Cell>{classItem.description_es || classItem.description_en || '-'}</Table.Cell>
|
<Table.Cell
|
||||||
|
>{classItem.description_es || classItem.description_en || '-'}</Table.Cell
|
||||||
|
>
|
||||||
<Table.Cell>{classItem.unit_of_measure || '-'}</Table.Cell>
|
<Table.Cell>{classItem.unit_of_measure || '-'}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Button size="sm" variant="ghost" onclick={() => handleSelect(classItem)}>
|
<Button size="sm" variant="ghost" onclick={() => handleSelect(classItem)}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Sheet from '$lib/components/ui/sheet';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import * as Tabs from '$lib/components/ui/tabs';
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
invoice,
|
invoice,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
|
||||||
|
isTargetingPreset = false,
|
||||||
isSaving = false
|
isSaving = false
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -34,6 +36,8 @@
|
|||||||
invoice: Invoice | null;
|
invoice: Invoice | null;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
|
||||||
|
isTargetingPreset?: boolean;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -63,31 +67,56 @@
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sheet.Root bind:open={open}>
|
<Dialog.Root bind:open={open}>
|
||||||
<Sheet.Content side="right" class="w-full sm:max-w-[95vw] lg:max-w-[85vw] xl:max-w-[75vw] p-0 flex flex-col h-full bg-slate-50 dark:bg-black">
|
<Dialog.Content class="sm:max-w-6xl max-h-[90vh] p-0 overflow-hidden z-[100] [&>button]:hidden">
|
||||||
|
<div class="bg-white dark:bg-zinc-950 border-b dark:border-zinc-800 px-4 py-3 shadow-sm flex items-start justify-between gap-3">
|
||||||
<header class="bg-white dark:bg-zinc-950 border-b dark:border-zinc-800 px-3 py-1.5 shadow-sm shrink-0">
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="bg-zinc-900 p-1 rounded">
|
||||||
<div class="flex items-center gap-2">
|
<Package class="w-3.5 h-3.5 text-white" />
|
||||||
<div class="bg-zinc-900 p-1 rounded">
|
</div>
|
||||||
<Package class="w-3.5 h-3.5 text-white" />
|
<div>
|
||||||
</div>
|
<Dialog.Title class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 leading-tight flex items-center gap-2">
|
||||||
<div>
|
{#if isTargetingPreset}
|
||||||
<Sheet.Title class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 leading-tight">
|
{isEditMode ? 'Editar Item de Plantilla' : 'Nuevo Item para Plantilla'}
|
||||||
|
{:else}
|
||||||
{isEditMode ? 'Editar Partida' : 'Nueva Partida - Activo Fijo'}
|
{isEditMode ? 'Editar Partida' : 'Nueva Partida - Activo Fijo'}
|
||||||
</Sheet.Title>
|
{/if}
|
||||||
|
{#if editingItem.lines && editingItem.lines.length > 1}
|
||||||
|
<span class="px-1.5 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/30 text-[10px] text-blue-700 dark:text-blue-300 font-bold border border-blue-200 dark:border-blue-800">
|
||||||
|
{editingItem.lines.length} lines
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</Dialog.Title>
|
||||||
|
{#if !isTargetingPreset}
|
||||||
<p class="text-[10px] text-muted-foreground flex items-center gap-1.5">
|
<p class="text-[10px] text-muted-foreground flex items-center gap-1.5">
|
||||||
Factura: <span class="font-medium text-zinc-700 dark:text-zinc-300">{invoice?.invoice_number || 'N/A'}</span>
|
Factura: <span class="font-medium text-zinc-700 dark:text-zinc-300">{invoice?.invoice_number || 'N/A'}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
{:else}
|
||||||
|
<p class="text-[10px] text-muted-foreground flex items-center gap-1.5">
|
||||||
|
<span class="font-medium text-blue-600 dark:text-blue-400 uppercase tracking-wider">Modo Plantilla</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" onclick={() => onCancel?.()} class="h-7 w-7 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800">
|
|
||||||
<X class="w-3.5 h-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
<div class="flex items-center gap-2">
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto px-2 py-1.5">
|
<Button variant="outline" size="sm" onclick={() => onCancel?.()} disabled={isSaving} class="h-7 text-xs px-2">
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" onclick={onSave} disabled={isSaving} class="h-7 text-xs px-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white">
|
||||||
|
{#if isSaving}
|
||||||
|
<Loader2 class="w-3 h-3 mr-1 animate-spin" />
|
||||||
|
Guardando...
|
||||||
|
{:else}
|
||||||
|
<Save class="w-3 h-3 mr-1" />
|
||||||
|
{isEditMode ? 'Actualizar' : 'Crear'}
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-3 overflow-y-auto max-h-[calc(90vh-88px)] bg-slate-50/70 dark:bg-black">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|
||||||
{#if line}
|
{#if line}
|
||||||
@@ -187,23 +216,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
<footer class="bg-white dark:bg-zinc-950 border-t border-zinc-200 dark:border-zinc-800 px-3 py-1.5 shadow-sm shrink-0">
|
</Dialog.Root>
|
||||||
<div class="flex items-center justify-end gap-1.5">
|
|
||||||
<Button variant="outline" size="sm" onclick={() => onCancel?.()} disabled={isSaving} class="h-7 text-xs px-2">
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" onclick={onSave} disabled={isSaving} class="h-7 text-xs px-2 bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 text-white">
|
|
||||||
{#if isSaving}
|
|
||||||
<Loader2 class="w-3 h-3 mr-1 animate-spin" />
|
|
||||||
Guardando...
|
|
||||||
{:else}
|
|
||||||
<Save class="w-3 h-3 mr-1" />
|
|
||||||
{isEditMode ? 'Actualizar' : 'Crear'}
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</Sheet.Content>
|
|
||||||
</Sheet.Root>
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as Sheet from '$lib/components/ui/sheet';
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
import * as Tabs from '$lib/components/ui/tabs';
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
import { Input } from '$lib/components/ui/input';
|
import { Input } from '$lib/components/ui/input';
|
||||||
import { Label } from '$lib/components/ui/label';
|
import { Label } from '$lib/components/ui/label';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import { Loader2 } from 'lucide-svelte';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
|
import { Loader2, FileText } from 'lucide-svelte';
|
||||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||||
import type { Item } from '$lib/api/dashboard/a76/items';
|
import type { Item } from '$lib/api/dashboard/a76/items';
|
||||||
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
import { useShortcuts } from '$lib/hooks/use-shortcuts';
|
||||||
@@ -17,6 +18,8 @@
|
|||||||
invoice,
|
invoice,
|
||||||
onSave,
|
onSave,
|
||||||
onCancel,
|
onCancel,
|
||||||
|
|
||||||
|
isTargetingPreset = false,
|
||||||
isSaving = false
|
isSaving = false
|
||||||
}: {
|
}: {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -25,6 +28,8 @@
|
|||||||
invoice: Invoice | null;
|
invoice: Invoice | null;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
|
|
||||||
|
isTargetingPreset?: boolean;
|
||||||
isSaving?: boolean;
|
isSaving?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -48,257 +53,397 @@
|
|||||||
manejarCancelar: () => onCancel?.()
|
manejarCancelar: () => onCancel?.()
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Derived state for easier binding and safety
|
||||||
|
let line = $derived(editingItem.lines?.[0]);
|
||||||
|
|
||||||
|
// Initialize missing nested objects if they don't exist
|
||||||
|
$effect(() => {
|
||||||
|
if (open && editingItem) {
|
||||||
|
if (!editingItem.lines) editingItem.lines = [{}];
|
||||||
|
if (!editingItem.lines[0].quantity) editingItem.lines[0].quantity = {};
|
||||||
|
if (!editingItem.lines[0].financial) editingItem.lines[0].financial = {};
|
||||||
|
if (!editingItem.lines[0].customs) editingItem.lines[0].customs = {};
|
||||||
|
if (!editingItem.lines[0].description) editingItem.lines[0].description = {};
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sheet.Root bind:open>
|
<Dialog.Root bind:open>
|
||||||
<Sheet.Content side="right" class="w-full sm:max-w-2xl overflow-y-auto">
|
<Dialog.Content class="sm:max-w-5xl max-h-[90vh] p-0 overflow-hidden z-[100] [&>button]:hidden">
|
||||||
<Sheet.Header>
|
<div
|
||||||
<Sheet.Title
|
class="px-6 py-4 border-b bg-white dark:bg-zinc-950 flex items-start justify-between gap-3"
|
||||||
>{isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - SCAII (Inventario)</Sheet.Title
|
>
|
||||||
>
|
<div class="space-y-1">
|
||||||
<Sheet.Description>
|
<Dialog.Title class="text-lg font-semibold">
|
||||||
{isEditMode
|
{isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - SCAII (Inventario)
|
||||||
? 'Modifica los campos del inventario y guarda los cambios.'
|
</Dialog.Title>
|
||||||
: 'Completa la información del nuevo item de inventario.'}
|
<Dialog.Description class="text-sm text-muted-foreground">
|
||||||
</Sheet.Description>
|
{isEditMode
|
||||||
</Sheet.Header>
|
? 'Modifica los campos del inventario y guarda los cambios.'
|
||||||
|
: 'Completa la información del nuevo item de inventario.'}
|
||||||
<Tabs.Root bind:value={activeTab} class="mt-6">
|
{#if editingItem.lines && editingItem.lines.length > 1}
|
||||||
<Tabs.List class="grid w-full grid-cols-4">
|
<Badge variant="secondary" class="ml-2">
|
||||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
{editingItem.lines.length} items en esta partida
|
||||||
<Tabs.Trigger value="clasificacion">Clasificación</Tabs.Trigger>
|
</Badge>
|
||||||
<Tabs.Trigger value="cantidades">Cantidades</Tabs.Trigger>
|
{/if}
|
||||||
<Tabs.Trigger value="otros">Otros</Tabs.Trigger>
|
</Dialog.Description>
|
||||||
</Tabs.List>
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
<!-- Tab: General -->
|
<Button variant="outline" onclick={() => onCancel?.()} disabled={isSaving}>Cancelar</Button>
|
||||||
<Tabs.Content value="general" class="space-y-4 mt-4">
|
<Button onclick={onSave} disabled={isSaving}>
|
||||||
<!-- Información de la Factura (Solo lectura) -->
|
{#if isSaving}
|
||||||
<div class="rounded-lg border bg-muted/50 p-4 space-y-3">
|
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
|
||||||
<h4 class="text-sm font-medium">Información de la Factura (SCAII - Inventario)</h4>
|
Guardando...
|
||||||
{#if !invoice?.id}
|
|
||||||
<div class="text-sm text-amber-600 bg-amber-50 dark:bg-amber-950/20 p-3 rounded">
|
|
||||||
⚠️ Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la
|
|
||||||
factura.
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
{isEditMode ? 'Guardar Cambios' : 'Agregar Item'}
|
||||||
<div>
|
{/if}
|
||||||
<span class="text-muted-foreground">ID Factura:</span>
|
</Button>
|
||||||
<span class="ml-2 font-medium">{invoice.id}</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span class="text-muted-foreground">Tipo Operación:</span>
|
<div class="p-6 overflow-auto max-h-[calc(90vh-96px)] bg-slate-50/60 dark:bg-black">
|
||||||
<span class="ml-2 font-medium uppercase">{invoice.operation_type || 'N/A'}</span>
|
<Tabs.Root bind:value={activeTab} class="mt-0">
|
||||||
</div>
|
<Tabs.List class="grid w-full grid-cols-4">
|
||||||
<div class="col-span-2">
|
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||||
<span class="text-muted-foreground">Número de Factura:</span>
|
<Tabs.Trigger value="clasificacion">Clasificación</Tabs.Trigger>
|
||||||
<span class="ml-2 font-medium">{invoice.invoice_number || 'Pendiente'}</span>
|
<Tabs.Trigger value="cantidades">Cantidades</Tabs.Trigger>
|
||||||
</div>
|
<Tabs.Trigger value="otros">Otros</Tabs.Trigger>
|
||||||
<div class="col-span-2">
|
</Tabs.List>
|
||||||
<span class="text-muted-foreground">Sistema:</span>
|
|
||||||
<span class="ml-2 font-medium bg-blue-100 dark:bg-blue-900/30 px-2 py-1 rounded"
|
<!-- Tab: General -->
|
||||||
>SCAII (Inventory)</span
|
<Tabs.Content value="general" class="space-y-4 mt-4">
|
||||||
>
|
<!-- Información de la Factura (Solo lectura) -->
|
||||||
</div>
|
{#if !isTargetingPreset}
|
||||||
|
<div class="rounded-lg border bg-muted/50 p-4 space-y-3">
|
||||||
|
<h4 class="text-sm font-medium">Información de la Factura (SCAII - Inventario)</h4>
|
||||||
|
{#if !invoice?.id}
|
||||||
|
<div class="text-sm text-amber-600 bg-amber-50 dark:bg-amber-950/20 p-3 rounded">
|
||||||
|
⚠️ Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la
|
||||||
|
factura.
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-muted-foreground">ID Factura:</span>
|
||||||
|
<span class="ml-2 font-medium">{invoice.id}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-muted-foreground">Tipo Operación:</span>
|
||||||
|
<span class="ml-2 font-medium uppercase">{invoice.operation_type || 'N/A'}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<span class="text-muted-foreground">Número de Factura:</span>
|
||||||
|
<span class="ml-2 font-medium">{invoice.invoice_number || 'Pendiente'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<span class="text-muted-foreground">Sistema:</span>
|
||||||
|
<span class="ml-2 font-medium bg-blue-100 dark:bg-blue-900/30 px-2 py-1 rounded"
|
||||||
|
>SCAII (Inventory)</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="reference_number">Número de Referencia</Label>
|
|
||||||
<Input id="reference_number" bind:value={editingItem.reference_number} />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="order">Orden de Compra/Venta</Label>
|
|
||||||
<Input
|
|
||||||
id="order"
|
|
||||||
bind:value={editingItem.order}
|
|
||||||
placeholder={invoice?.purchase_order || ''}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="warehouse">Almacén</Label>
|
|
||||||
<Input id="warehouse" bind:value={editingItem.warehouse} />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="location">Ubicación</Label>
|
|
||||||
<Input id="location" bind:value={editingItem.location} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Campos específicos de SCAII -->
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="product_description">Descripción del Producto</Label>
|
|
||||||
<Input id="product_description" placeholder="Descripción detallada del producto" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="sku">SKU</Label>
|
|
||||||
<Input id="sku" placeholder="Código SKU del producto" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="batch">Lote</Label>
|
|
||||||
<Input id="batch" placeholder="Número de lote" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
|
|
||||||
<!-- Tab: Clasificación -->
|
|
||||||
<Tabs.Content value="clasificacion" class="space-y-4 mt-4">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="tariff_fraction">Fracción Arancelaria</Label>
|
|
||||||
<Input id="tariff_fraction" placeholder="8 dígitos" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="product_type">Tipo de Producto</Label>
|
<Label for="reference_number">Número de Referencia</Label>
|
||||||
<Input id="product_type" placeholder="Materia prima, producto terminado, etc." />
|
<Input id="reference_number" bind:value={editingItem.reference_number} />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="material_type">Tipo de Material</Label>
|
<Label for="order">Orden de Compra/Venta</Label>
|
||||||
<Input id="material_type" placeholder="Metal, plástico, etc." />
|
<Input
|
||||||
</div>
|
id="order"
|
||||||
</div>
|
bind:value={editingItem.order}
|
||||||
|
placeholder={invoice?.purchase_order || ''}
|
||||||
<div class="space-y-2">
|
/>
|
||||||
<Label for="product_code">Código de Producto</Label>
|
|
||||||
<Input id="product_code" placeholder="Código interno" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="country_origin">País de Origen</Label>
|
|
||||||
<Input id="country_origin" placeholder="Código del país" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="merchandise_category">Categoría de Mercancía</Label>
|
|
||||||
<Input id="merchandise_category" placeholder="Categoría" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
|
|
||||||
<!-- Tab: Cantidades -->
|
|
||||||
<Tabs.Content value="cantidades" class="space-y-4 mt-4">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="quantity">Cantidad</Label>
|
|
||||||
<Input id="quantity" type="number" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="unit">Unidad de Medida</Label>
|
|
||||||
<Input id="unit" placeholder="PZA, KG, M, etc." />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="net_weight">Peso Neto (KG)</Label>
|
<Label for="warehouse">Almacén</Label>
|
||||||
<Input id="net_weight" type="number" step="0.01" placeholder="0.00" />
|
<Input id="warehouse" bind:value={editingItem.warehouse} />
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="gross_weight">Peso Bruto (KG)</Label>
|
<Label for="location">Ubicación</Label>
|
||||||
<Input id="gross_weight" type="number" step="0.01" placeholder="0.00" />
|
<Input id="location" bind:value={editingItem.location} />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="unit_value">Valor Unitario (USD)</Label>
|
|
||||||
<Input id="unit_value" type="number" step="0.01" placeholder="0.00" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="total_value">Valor Total (USD)</Label>
|
|
||||||
<Input id="total_value" type="number" step="0.01" placeholder="0.00" disabled />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Campos específicos de SCAII -->
|
<!-- Campos específicos de SCAII -->
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="packages">Número de Bultos</Label>
|
|
||||||
<Input id="packages" type="number" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="package_type">Tipo de Empaque</Label>
|
|
||||||
<Input id="package_type" placeholder="Caja, pallet, etc." />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="imported_quantity">Cantidad Importada</Label>
|
|
||||||
<Input id="imported_quantity" type="number" placeholder="0" />
|
|
||||||
</div>
|
|
||||||
<div class="space-y-2">
|
|
||||||
<Label for="remaining_quantity">Cantidad Remanente</Label>
|
|
||||||
<Input id="remaining_quantity" type="number" placeholder="0" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Tabs.Content>
|
|
||||||
|
|
||||||
<!-- Tab: Otros -->
|
|
||||||
<Tabs.Content value="otros" class="space-y-4 mt-4">
|
|
||||||
<div class="space-y-4">
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="brand">Marca</Label>
|
<Label for="product_description">Descripción del Producto</Label>
|
||||||
<Input id="brand" placeholder="Marca del producto" />
|
{#if line?.description}
|
||||||
|
<Input
|
||||||
|
id="product_description"
|
||||||
|
placeholder="Descripción detallada del producto"
|
||||||
|
bind:value={line.description.description_spanish}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="expiration_date">Fecha de Caducidad</Label>
|
<Label for="sku">SKU</Label>
|
||||||
<Input id="expiration_date" type="date" />
|
{#if line}
|
||||||
|
<Input
|
||||||
|
id="sku"
|
||||||
|
placeholder="Código SKU del producto"
|
||||||
|
bind:value={line.part_number}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="production_date">Fecha de Producción</Label>
|
<Label for="batch">Lote</Label>
|
||||||
<Input id="production_date" type="date" />
|
{#if line?.description}
|
||||||
|
<Input id="batch" placeholder="Número de lote" bind:value={line.description.lot} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<!-- Tab: Clasificación -->
|
||||||
|
<Tabs.Content value="clasificacion" class="space-y-4 mt-4">
|
||||||
|
<div class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="min_stock">Stock Mínimo</Label>
|
<Label for="tariff_fraction">Fracción Arancelaria</Label>
|
||||||
<Input id="min_stock" type="number" placeholder="0" />
|
{#if line?.customs}
|
||||||
|
<Input
|
||||||
|
id="tariff_fraction"
|
||||||
|
placeholder="8 dígitos"
|
||||||
|
bind:value={line.customs.fraction}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="product_type">Tipo de Producto</Label>
|
||||||
|
{#if line}
|
||||||
|
<Input
|
||||||
|
id="product_type"
|
||||||
|
placeholder="Materia prima, producto terminado, etc."
|
||||||
|
bind:value={line.description.extra_description_2}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="material_type">Tipo de Material</Label>
|
||||||
|
{#if line}
|
||||||
|
<Input
|
||||||
|
id="material_type"
|
||||||
|
placeholder="Metal, plástico, etc."
|
||||||
|
bind:value={line.description.extra_description_3}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="max_stock">Stock Máximo</Label>
|
<Label for="product_code">Código de Producto</Label>
|
||||||
<Input id="max_stock" type="number" placeholder="0" />
|
<Input id="product_code" placeholder="Código interno" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="country_origin">País de Origen</Label>
|
||||||
|
<Input id="country_origin" placeholder="Código del país" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="merchandise_category">Categoría de Mercancía</Label>
|
||||||
|
<Input id="merchandise_category" placeholder="Categoría" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<!-- Tab: Cantidades -->
|
||||||
<Label for="observations">Observaciones</Label>
|
<Tabs.Content value="cantidades" class="space-y-4 mt-4">
|
||||||
<textarea
|
<div class="space-y-4">
|
||||||
id="observations"
|
<div class="grid grid-cols-2 gap-4">
|
||||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
<div class="space-y-2">
|
||||||
placeholder="Notas adicionales sobre el inventario..."
|
<Label for="quantity">Cantidad</Label>
|
||||||
></textarea>
|
{#if line?.quantity}
|
||||||
|
<Input
|
||||||
|
id="quantity"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
bind:value={line.quantity.quantity}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="unit">Unidad de Medida</Label>
|
||||||
|
{#if line?.quantity}
|
||||||
|
<Input
|
||||||
|
id="unit"
|
||||||
|
placeholder="PZA, KG, M, etc."
|
||||||
|
bind:value={line.quantity.unit_of_measure}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="net_weight">Peso Neto (KG)</Label>
|
||||||
|
{#if line?.quantity}
|
||||||
|
<Input
|
||||||
|
id="net_weight"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="0.00"
|
||||||
|
bind:value={line.quantity.net_weight}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="gross_weight">Peso Bruto (KG)</Label>
|
||||||
|
{#if line?.quantity}
|
||||||
|
<Input
|
||||||
|
id="gross_weight"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="0.00"
|
||||||
|
bind:value={line.quantity.gross_weight}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="unit_cost_usd">Costo Unitario (USD)</Label>
|
||||||
|
{#if line?.financial}
|
||||||
|
<Input
|
||||||
|
id="unit_cost_usd"
|
||||||
|
type="number"
|
||||||
|
step="0.0001"
|
||||||
|
placeholder="0.00"
|
||||||
|
bind:value={line.financial.unit_cost_usd}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="total_value">Valor Total (USD)</Label>
|
||||||
|
<Input
|
||||||
|
id="total_value"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="0.00"
|
||||||
|
value={(line?.quantity?.quantity || 0) * (line?.financial?.unit_cost_usd || 0)}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Campos específicos de SCAII -->
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="packages">Número de Bultos</Label>
|
||||||
|
{#if line?.quantity}
|
||||||
|
<Input
|
||||||
|
id="packages"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
bind:value={line.quantity.packages}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="package_type">Tipo de Empaque</Label>
|
||||||
|
{#if line?.quantity}
|
||||||
|
<Input
|
||||||
|
id="package_type"
|
||||||
|
placeholder="Caja, pallet, etc."
|
||||||
|
bind:value={line.quantity.package_type}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="imported_quantity">Cantidad Importada</Label>
|
||||||
|
{#if line?.quantity}
|
||||||
|
<Input
|
||||||
|
id="imported_quantity"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
bind:value={line.quantity.quantity_imported}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="remaining_quantity">Cantidad Remanente</Label>
|
||||||
|
{#if line?.quantity}
|
||||||
|
<Input
|
||||||
|
id="remaining_quantity"
|
||||||
|
type="number"
|
||||||
|
placeholder="0"
|
||||||
|
value={(line.quantity.quantity || 0) - (line.quantity.quantity_imported || 0)}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tabs.Content>
|
||||||
</Tabs.Content>
|
|
||||||
</Tabs.Root>
|
|
||||||
|
|
||||||
<Sheet.Footer class="mt-6 gap-2">
|
<!-- Tab: Otros -->
|
||||||
<Button variant="outline" onclick={() => onCancel?.()} disabled={isSaving}>Cancelar</Button>
|
<Tabs.Content value="otros" class="space-y-4 mt-4">
|
||||||
<Button onclick={onSave} disabled={isSaving}>
|
<div class="space-y-4">
|
||||||
{#if isSaving}
|
<div class="space-y-2">
|
||||||
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
|
<Label for="brand">Marca</Label>
|
||||||
Guardando...
|
{#if line?.description}
|
||||||
{:else}
|
<Input
|
||||||
{isEditMode ? 'Guardar Cambios' : 'Agregar Item'}
|
id="brand"
|
||||||
{/if}
|
placeholder="Marca del producto"
|
||||||
</Button>
|
bind:value={line.description.brand}
|
||||||
</Sheet.Footer>
|
/>
|
||||||
</Sheet.Content>
|
{/if}
|
||||||
</Sheet.Root>
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="expiration_date">Fecha de Caducidad</Label>
|
||||||
|
<Input id="expiration_date" type="date" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="production_date">Fecha de Producción</Label>
|
||||||
|
<Input id="production_date" type="date" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="min_stock">Stock Mínimo</Label>
|
||||||
|
<Input id="min_stock" type="number" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="max_stock">Stock Máximo</Label>
|
||||||
|
<Input id="max_stock" type="number" placeholder="0" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="observations">Observaciones</Label>
|
||||||
|
{#if line?.description}
|
||||||
|
<textarea
|
||||||
|
id="observations"
|
||||||
|
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
placeholder="Notas adicionales sobre el inventario..."
|
||||||
|
bind:value={line.description.extra_description}
|
||||||
|
></textarea>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,49 +22,54 @@
|
|||||||
transportModes?: any[];
|
transportModes?: any[];
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
if (!formData && invoice) {
|
// Inicializar formData si está vacío o null
|
||||||
formData = {
|
$effect(() => {
|
||||||
// Campo de comentario estatus
|
if ((!formData || Object.keys(formData).length === 0)) {
|
||||||
comments_status: invoice.comments_status || '',
|
if (invoice) {
|
||||||
// Campos que van en diferentes recursos pero se editan aquí
|
formData = {
|
||||||
transport_mode: invoice.logistics?.transport_mode || null,
|
// Campo de comentario estatus
|
||||||
is_mixed: invoice.compliance_mx?.is_mixed || null,
|
comments_status: invoice.comments_status || '',
|
||||||
print_stamp: invoice.financials?.seal_value_2500 || false,
|
// Campos que van en diferentes recursos pero se editan aquí
|
||||||
rule_3121_parties_ii: false,
|
transport_mode: invoice.logistics?.transport_mode || 'TRUCK', // Default to TRUCK if null
|
||||||
related_doc_id: invoice.related_doc_id || null,
|
is_mixed: invoice.compliance_mx?.is_mixed || null,
|
||||||
code_signature: invoice.compliance_mx?.code_signature || '',
|
print_stamp: invoice.financials?.seal_value_2500 || false,
|
||||||
electronic_signature: invoice.compliance_mx?.electronic_signature || '',
|
rule_3121_parties_ii: false, // Default false or logic?
|
||||||
mandatory_person: '',
|
related_doc_id: invoice.related_doc_id || null,
|
||||||
contingency_mode: invoice.compliance_mx?.contingency_mode || false,
|
code_signature: invoice.compliance_mx?.code_signature || '',
|
||||||
cove: invoice.compliance_mx?.origin_destination_cove || '',
|
electronic_signature: invoice.compliance_mx?.electronic_signature || '',
|
||||||
operation_num: invoice.compliance_mx?.vucem_operation_num || '',
|
mandatory_person: '',
|
||||||
adendas: invoice.compliance_mx?.addendum_vu || '',
|
contingency_mode: invoice.compliance_mx?.contingency_mode || false,
|
||||||
observations_vu: invoice.vu_observations || '',
|
cove: invoice.compliance_mx?.origin_destination_cove || '',
|
||||||
certified_number: invoice.compliance_mx?.certificate_number || '',
|
operation_num: invoice.compliance_mx?.vucem_operation_num || '',
|
||||||
};
|
adendas: invoice.compliance_mx?.addendum_vu || '',
|
||||||
exists = true;
|
observations_vu: invoice.vu_observations || '',
|
||||||
} else if (!formData) {
|
certified_number: invoice.compliance_mx?.certificate_number || '',
|
||||||
formData = {
|
};
|
||||||
// Campo de comentario estatus
|
exists = true;
|
||||||
comments_status: '',
|
} else {
|
||||||
// Campos que van en diferentes recursos pero se editan aquí
|
formData = {
|
||||||
transport_mode: 'TRUCK',
|
// Campo de comentario estatus
|
||||||
is_mixed: null,
|
comments_status: '',
|
||||||
print_stamp: false,
|
// Campos que van en diferentes recursos pero se editan aquí
|
||||||
rule_3121_parties_ii: false,
|
transport_mode: 'TRUCK',
|
||||||
related_doc_id: null,
|
is_mixed: null,
|
||||||
code_signature: '',
|
print_stamp: false,
|
||||||
electronic_signature: '',
|
rule_3121_parties_ii: false,
|
||||||
mandatory_person: '',
|
related_doc_id: null,
|
||||||
contingency_mode: false,
|
code_signature: '',
|
||||||
cove: '',
|
electronic_signature: '',
|
||||||
operation_num: '',
|
mandatory_person: '',
|
||||||
adendas: '',
|
contingency_mode: false,
|
||||||
observations_vu: '',
|
cove: '',
|
||||||
certified_number: '',
|
operation_num: '',
|
||||||
};
|
adendas: '',
|
||||||
exists = false;
|
observations_vu: '',
|
||||||
}
|
certified_number: '',
|
||||||
|
};
|
||||||
|
exists = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Campos que no están en el backend
|
// Campos que no están en el backend
|
||||||
let rfc = $state('');
|
let rfc = $state('');
|
||||||
|
|||||||
107
frontend/src/lib/utils/items-logic.ts
Normal file
107
frontend/src/lib/utils/items-logic.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
|
||||||
|
export interface Item {
|
||||||
|
id?: number;
|
||||||
|
lines?: any[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to check if an object has any meaningful values
|
||||||
|
export function hasValues(obj: any): boolean {
|
||||||
|
if (!obj || typeof obj !== 'object') return false;
|
||||||
|
return Object.values(obj).some(
|
||||||
|
(val) =>
|
||||||
|
val !== undefined &&
|
||||||
|
val !== null &&
|
||||||
|
val !== '' &&
|
||||||
|
!(typeof val === 'object' && !hasValues(val))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean nested data before sending to API
|
||||||
|
export function cleanLineData(line: any) {
|
||||||
|
const cleaned: any = { ...line };
|
||||||
|
|
||||||
|
// Helper function to convert to number or undefined
|
||||||
|
const toNumberOrUndefined = (value: any): number | undefined => {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const numValue = Number(value);
|
||||||
|
return !isNaN(numValue) && isFinite(numValue) ? numValue : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert integer fields
|
||||||
|
cleaned.part_number = toNumberOrUndefined(cleaned.part_number);
|
||||||
|
cleaned.component_part_number = toNumberOrUndefined(cleaned.component_part_number);
|
||||||
|
cleaned.class_id = toNumberOrUndefined(cleaned.class_id);
|
||||||
|
cleaned.unit_of_measure = toNumberOrUndefined(cleaned.unit_of_measure);
|
||||||
|
cleaned.alternate_unit = toNumberOrUndefined(cleaned.alternate_unit);
|
||||||
|
|
||||||
|
// Remove display-only fields
|
||||||
|
delete cleaned.class_code;
|
||||||
|
delete cleaned.class_unit_of_measure;
|
||||||
|
delete cleaned.class_description;
|
||||||
|
|
||||||
|
// UI specific fields that shouldn't be in the payload
|
||||||
|
delete cleaned.part_description_es;
|
||||||
|
delete cleaned.part_description_en;
|
||||||
|
delete cleaned.unit_code;
|
||||||
|
delete cleaned.unit_description;
|
||||||
|
|
||||||
|
// Only delete part_number if it's the string code from UI, but schema expects int ID.
|
||||||
|
// In this codebase, if part_number is populated from existing data, it's an ID.
|
||||||
|
// If it's a new item, it might be cleaned.
|
||||||
|
|
||||||
|
// Remove display-only fields from nested objects
|
||||||
|
if (cleaned.customs) {
|
||||||
|
delete cleaned.customs.origin_country_name;
|
||||||
|
delete cleaned.customs.fraction_description;
|
||||||
|
if (!hasValues(cleaned.customs)) delete cleaned.customs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove empty nested objects
|
||||||
|
if (cleaned.financial && !hasValues(cleaned.financial)) delete cleaned.financial;
|
||||||
|
if (cleaned.quantity && !hasValues(cleaned.quantity)) delete cleaned.quantity;
|
||||||
|
if (cleaned.description && !hasValues(cleaned.description)) delete cleaned.description;
|
||||||
|
if (cleaned.reference && !hasValues(cleaned.reference)) delete cleaned.reference;
|
||||||
|
if (cleaned.fa_data && !hasValues(cleaned.fa_data)) delete cleaned.fa_data;
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize numeric values from strings to numbers (for editing)
|
||||||
|
export function normalizeItemData(item: Partial<Item>): Partial<Item> {
|
||||||
|
if (item.lines && item.lines.length > 0) {
|
||||||
|
item.lines = item.lines.map((line) => {
|
||||||
|
const normalizedLine = { ...line };
|
||||||
|
|
||||||
|
// Normalize financials
|
||||||
|
if (normalizedLine.financial) {
|
||||||
|
const f = normalizedLine.financial;
|
||||||
|
normalizedLine.financial = {
|
||||||
|
...f,
|
||||||
|
unit_cost_usd: f.unit_cost_usd != null ? Number(f.unit_cost_usd) : undefined,
|
||||||
|
unit_cost_mxn: f.unit_cost_mxn != null ? Number(f.unit_cost_mxn) : undefined,
|
||||||
|
value_usd: f.value_usd != null ? Number(f.value_usd) : undefined,
|
||||||
|
value_mxn: f.value_mxn != null ? Number(f.value_mxn) : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize quantities
|
||||||
|
if (normalizedLine.quantity) {
|
||||||
|
const q = normalizedLine.quantity;
|
||||||
|
normalizedLine.quantity = {
|
||||||
|
...q,
|
||||||
|
quantity: q.quantity != null ? Number(q.quantity) : undefined,
|
||||||
|
net_weight: q.net_weight != null ? Number(q.net_weight) : undefined,
|
||||||
|
gross_weight: q.gross_weight != null ? Number(q.gross_weight) : undefined,
|
||||||
|
package_quantity: q.package_quantity != null ? Number(q.package_quantity) : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedLine;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ export const load: LayoutServerLoad = async ({ cookies, url, fetch }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
user: userData,
|
user: { ...userData, token: accessToken },
|
||||||
companies, // Pasar las compañías al cliente
|
companies, // Pasar las compañías al cliente
|
||||||
error: undefined // Agregar error opcional para compatibilidad con error-handler
|
error: undefined // Agregar error opcional para compatibilidad con error-handler
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,16 @@
|
|||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
import { Plus, RefreshCw, FileText, RotateCcw, Boxes, Package, ClipboardList } from 'lucide-svelte';
|
import {
|
||||||
|
Plus,
|
||||||
|
RefreshCw,
|
||||||
|
FileText,
|
||||||
|
RotateCcw,
|
||||||
|
Boxes,
|
||||||
|
Package,
|
||||||
|
ClipboardList,
|
||||||
|
Settings
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
// IMPORTANTE: Asegúrate de tener instalada svelte-sonner para las notificaciones
|
// IMPORTANTE: Asegúrate de tener instalada svelte-sonner para las notificaciones
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
@@ -39,7 +48,7 @@
|
|||||||
year: data.filters?.year || ''
|
year: data.filters?.year || ''
|
||||||
});
|
});
|
||||||
|
|
||||||
let isDownloadModalOpen = $state(false);
|
let isDownloadModalOpen = $state(false);
|
||||||
|
|
||||||
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
|
// Efecto reactivo para actualizar filtros cuando cambian los query parameters en la URL
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -611,32 +620,36 @@
|
|||||||
currentStatusFunction = null;
|
currentStatusFunction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS ---
|
// --- AQUÍ PASAMOS LA FUNCIÓN DE DESCARGA A LAS COLUMNAS ---
|
||||||
const columns = createColumns(handleSuccess);
|
const columns = createColumns(handleSuccess);
|
||||||
async function handleModalConfirm(type: string, format: string, currency: string, uomSource: string, weightUnit: string) {
|
async function handleModalConfirm(
|
||||||
if (!selectedInvoice || !companyStore.activeCompany) return;
|
type: string,
|
||||||
|
format: string,
|
||||||
|
currency: string,
|
||||||
|
uomSource: string,
|
||||||
|
weightUnit: string
|
||||||
|
) {
|
||||||
|
if (!selectedInvoice || !companyStore.activeCompany) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 1. Trigger: Start celery task with selected options
|
// 1. Trigger: Start celery task with selected options
|
||||||
// Note: uomSource and weightUnit are UI-only for now
|
// Note: uomSource and weightUnit are UI-only for now
|
||||||
const { task_id } = await invoicesReportsApi.triggerPdfGeneration(
|
const { task_id } = await invoicesReportsApi.triggerPdfGeneration(
|
||||||
selectedInvoice.id,
|
selectedInvoice.id,
|
||||||
companyStore.activeCompany.id,
|
companyStore.activeCompany.id,
|
||||||
type,
|
type,
|
||||||
currency
|
currency
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Open Progress Dialog
|
|
||||||
currentTaskId = task_id;
|
|
||||||
currentStatusFunction = invoicesReportsApi.getTaskStatus;
|
|
||||||
showProgressDialog = true;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
toast.error("No se pudo iniciar la descarga");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 2. Open Progress Dialog
|
||||||
|
currentTaskId = task_id;
|
||||||
|
currentStatusFunction = invoicesReportsApi.getTaskStatus;
|
||||||
|
showProgressDialog = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
toast.error('No se pudo iniciar la descarga');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
@@ -645,10 +658,16 @@
|
|||||||
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
|
<h1 class="text-3xl font-bold tracking-tight">Facturas</h1>
|
||||||
<p class="text-muted-foreground">Gestiona las facturas del sistema</p>
|
<p class="text-muted-foreground">Gestiona las facturas del sistema</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onclick={handleCreateClick}>
|
<div class="flex items-center gap-2">
|
||||||
<Plus class="mr-2" size={16} />
|
<Button variant="outline" onclick={() => goto('/dashboard/invoices/settings')}>
|
||||||
Nueva Factura
|
<Settings class="mr-2" size={16} />
|
||||||
</Button>
|
Parámetros
|
||||||
|
</Button>
|
||||||
|
<Button onclick={handleCreateClick}>
|
||||||
|
<Plus class="mr-2" size={16} />
|
||||||
|
Nueva Factura
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
@@ -775,7 +794,7 @@
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={() => isDownloadModalOpen = true}
|
onclick={() => (isDownloadModalOpen = true)}
|
||||||
disabled={!selectedInvoice}
|
disabled={!selectedInvoice}
|
||||||
>
|
>
|
||||||
<FileText class="h-4 w-4 mr-2" />
|
<FileText class="h-4 w-4 mr-2" />
|
||||||
@@ -792,49 +811,44 @@
|
|||||||
Consolidado
|
Consolidado
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => selectedInvoice && handleDownloadAvisoConsolidado(selectedInvoice)}
|
|
||||||
disabled={!selectedInvoice}
|
|
||||||
>
|
|
||||||
<Boxes class="mr-2 h-4 w-4" />
|
|
||||||
Aviso Consolidado
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => selectedInvoice && handleDownloadPackingList(selectedInvoice)}
|
|
||||||
disabled={!selectedInvoice}
|
|
||||||
>
|
|
||||||
<Package class="mr-2 h-4 w-4" />
|
|
||||||
Packing List
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{#if selectedInvoice?.operation_type === 'exp'}
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onclick={() => selectedInvoice && handleDownloadDescargo(selectedInvoice)}
|
onclick={() => selectedInvoice && handleDownloadAvisoConsolidado(selectedInvoice)}
|
||||||
disabled={!selectedInvoice}
|
disabled={!selectedInvoice}
|
||||||
>
|
>
|
||||||
<ClipboardList class="mr-2 h-4 w-4" />
|
<Boxes class="mr-2 h-4 w-4" />
|
||||||
Descargo PEPS
|
Aviso Consolidado
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => selectedInvoice && handleDownloadPackingList(selectedInvoice)}
|
||||||
|
disabled={!selectedInvoice}
|
||||||
|
>
|
||||||
|
<Package class="mr-2 h-4 w-4" />
|
||||||
|
Packing List
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{#if selectedInvoice?.operation_type === 'exp'}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => selectedInvoice && handleDownloadDescargo(selectedInvoice)}
|
||||||
|
disabled={!selectedInvoice}
|
||||||
|
>
|
||||||
|
<ClipboardList class="mr-2 h-4 w-4" />
|
||||||
|
Descargo PEPS
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ... -->
|
||||||
|
|
||||||
|
{#if selectedInvoice && companyStore.activeCompany}
|
||||||
<!-- ... -->
|
<InvoiceDownloadModal bind:open={isDownloadModalOpen} onConfirm={handleModalConfirm} />
|
||||||
|
{/if}
|
||||||
{#if selectedInvoice && companyStore.activeCompany}
|
|
||||||
<InvoiceDownloadModal
|
|
||||||
bind:open={isDownloadModalOpen}
|
|
||||||
onConfirm={handleModalConfirm}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,6 +144,19 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
// Si el ID es "new", es una creación
|
// Si el ID es "new", es una creación
|
||||||
if (params.id === 'new') {
|
if (params.id === 'new') {
|
||||||
try {
|
try {
|
||||||
|
// Fetch default settings if type, operation and company are present
|
||||||
|
const settingsPromise = (parsedOperationType && invoiceTypeParam && companyId)
|
||||||
|
? authenticatedFetch(
|
||||||
|
`v1/a76/invoice-settings/${invoiceTypeParam}?operation_type=${parsedOperationType}&company_id=${companyId}`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
).catch((err) => {
|
||||||
|
console.error('Error fetching defaults in server load:', err);
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
: Promise.resolve(null);
|
||||||
|
|
||||||
const [
|
const [
|
||||||
invoiceTypesResponse,
|
invoiceTypesResponse,
|
||||||
customsBrokersResponse,
|
customsBrokersResponse,
|
||||||
@@ -160,7 +173,8 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
sealsResponse,
|
sealsResponse,
|
||||||
incotermsResponse,
|
incotermsResponse,
|
||||||
pedimentosResponse,
|
pedimentosResponse,
|
||||||
transportModesResponse
|
transportModesResponse,
|
||||||
|
settingsResponse
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
invoiceTypesPromise,
|
invoiceTypesPromise,
|
||||||
customsBrokersPromise,
|
customsBrokersPromise,
|
||||||
@@ -177,7 +191,8 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
sealsPromise,
|
sealsPromise,
|
||||||
incotermsPromise,
|
incotermsPromise,
|
||||||
pedimentosPromise,
|
pedimentosPromise,
|
||||||
transportModesPromise
|
transportModesPromise,
|
||||||
|
settingsPromise
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
|
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
|
||||||
@@ -197,6 +212,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
||||||
const transportModes = transportModesResponse.ok ? await transportModesResponse.json() : { items: [] };
|
const transportModes = transportModesResponse.ok ? await transportModesResponse.json() : { items: [] };
|
||||||
|
|
||||||
|
let defaultSettings = null;
|
||||||
|
if (settingsResponse && settingsResponse.ok) {
|
||||||
|
const settingsData = await settingsResponse.json();
|
||||||
|
defaultSettings = settingsData.settings || null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invoice: null,
|
invoice: null,
|
||||||
invoiceId: null,
|
invoiceId: null,
|
||||||
@@ -217,6 +238,7 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
incoterms: incoterms.items || [],
|
incoterms: incoterms.items || [],
|
||||||
pedimentos: pedimentos.items || [],
|
pedimentos: pedimentos.items || [],
|
||||||
transportModes: transportModes.items || [],
|
transportModes: transportModes.items || [],
|
||||||
|
defaultSettings,
|
||||||
// Filtros desde query parameters para preselección
|
// Filtros desde query parameters para preselección
|
||||||
filters: {
|
filters: {
|
||||||
operation_type: parsedOperationType,
|
operation_type: parsedOperationType,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
invoice_type?: string | null;
|
invoice_type?: string | null;
|
||||||
};
|
};
|
||||||
pedimentos?: any[];
|
pedimentos?: any[];
|
||||||
|
defaultSettings?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data }: { data: ExtendedPageData } = $props();
|
let { data }: { data: ExtendedPageData } = $props();
|
||||||
@@ -91,19 +92,135 @@
|
|||||||
// ID de la factura
|
// ID de la factura
|
||||||
let invoiceId = $state<number | null>(data.invoiceId ?? null);
|
let invoiceId = $state<number | null>(data.invoiceId ?? null);
|
||||||
|
|
||||||
|
// Form Skeletons for robust initialization
|
||||||
|
const topFieldsSkeleton = {
|
||||||
|
is_pedimento_pending: false,
|
||||||
|
pedimento_id: '',
|
||||||
|
remesa: '',
|
||||||
|
invoice_number: '',
|
||||||
|
invoice_date: new Date().toISOString().split('T')[0],
|
||||||
|
emission_date: new Date().toISOString().split('T')[0],
|
||||||
|
operation_type: data.filters?.operation_type || '',
|
||||||
|
invoice_type: data.filters?.invoice_type || '',
|
||||||
|
fecha_pedimento_del: '',
|
||||||
|
fecha_pedimento_al: '',
|
||||||
|
clave_pedimento: '',
|
||||||
|
regimen_pedimento: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const generalSkeleton = {
|
||||||
|
provider_header: 'proveedor',
|
||||||
|
provider_id: null,
|
||||||
|
sold_to_header: 'consignado_a',
|
||||||
|
sold_to_id: null,
|
||||||
|
shipped_to_header: 'enviado_a',
|
||||||
|
shipped_to_id: null,
|
||||||
|
customs_broker_id: null,
|
||||||
|
customs_broker_us_id: null,
|
||||||
|
currency_type: '',
|
||||||
|
currency: 'foreign',
|
||||||
|
exchange_rate: null,
|
||||||
|
weight_type: 'kgs',
|
||||||
|
iva_factor: null,
|
||||||
|
carrier_id: null,
|
||||||
|
transport_id: '',
|
||||||
|
driver_name: '',
|
||||||
|
transport_type: '',
|
||||||
|
transport_num: '',
|
||||||
|
aduana: '',
|
||||||
|
document_type: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const observationSkeleton = {
|
||||||
|
observation_es: '',
|
||||||
|
observation_en: '',
|
||||||
|
freight: null,
|
||||||
|
insurance_value: null,
|
||||||
|
insurance: null,
|
||||||
|
packaging: null,
|
||||||
|
other_increments: null,
|
||||||
|
total_increments_mn: null,
|
||||||
|
total_increments_me: null,
|
||||||
|
incoterm: null,
|
||||||
|
enclosure: null,
|
||||||
|
num_seals: null,
|
||||||
|
movement_type: '',
|
||||||
|
alternate_invoice: '',
|
||||||
|
valuation_method: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const othersSkeleton = {
|
||||||
|
comments_status: '',
|
||||||
|
transport_mode: 'TRUCK',
|
||||||
|
is_mixed: null,
|
||||||
|
print_stamp: false,
|
||||||
|
rule_3121_parties_ii: false,
|
||||||
|
related_doc_id: null,
|
||||||
|
code_signature: '',
|
||||||
|
electronic_signature: '',
|
||||||
|
mandatory_person: '',
|
||||||
|
contingency_mode: false,
|
||||||
|
cove: '',
|
||||||
|
operation_num: '',
|
||||||
|
adendas: '',
|
||||||
|
observations_vu: '',
|
||||||
|
certified_number: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const continuationSkeleton = {
|
||||||
|
numero_tipo_transporte: '',
|
||||||
|
es_ferrocarril: 'no',
|
||||||
|
numero_bl: '',
|
||||||
|
cantidad_guias_embarque: null,
|
||||||
|
destino_origen: '',
|
||||||
|
puerto_entrada: '',
|
||||||
|
fue_revisado_equipo: false,
|
||||||
|
sub_division: false,
|
||||||
|
funge_como_cd: false,
|
||||||
|
llego_pedimento: false,
|
||||||
|
errores_facturacion: [],
|
||||||
|
semaforo_verde_aduana_mexicana: false,
|
||||||
|
semaforo_verde_aduana_americana: false,
|
||||||
|
semaforo_rojo_aduana_mexicana: false,
|
||||||
|
semaforo_rojo_aduana_americana: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function ensureItemsFormData(initial?: any) {
|
||||||
|
const base = initial ? { ...initial } : {};
|
||||||
|
if (!Array.isArray(base.items)) {
|
||||||
|
base.items = [];
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeDefaults(skeleton: any, partial: any) {
|
||||||
|
if (!partial) return skeleton;
|
||||||
|
return { ...skeleton, ...partial };
|
||||||
|
}
|
||||||
|
|
||||||
// Referencias a los componentes de formulario para obtener sus datos
|
// Referencias a los componentes de formulario para obtener sus datos
|
||||||
let InvoiceTopFieldsFormData = $state<any>(null);
|
let InvoiceTopFieldsFormData = $state<any>(
|
||||||
let generalFormData = $state<any>(null);
|
mergeDefaults(topFieldsSkeleton, data.defaultSettings?.InvoiceTopFieldsFormData)
|
||||||
let observationFormData = $state<any>(null);
|
);
|
||||||
let itemsFormData = $state<any>(null);
|
let generalFormData = $state<any>(
|
||||||
let othersFormData = $state<any>(null);
|
mergeDefaults(generalSkeleton, data.defaultSettings?.generalFormData)
|
||||||
let continuationFormData = $state<any>(null);
|
);
|
||||||
|
let observationFormData = $state<any>(
|
||||||
|
mergeDefaults(observationSkeleton, data.defaultSettings?.observationFormData)
|
||||||
|
);
|
||||||
|
let itemsFormData = $state<any>(ensureItemsFormData(data.defaultSettings?.itemsFormData));
|
||||||
|
let othersFormData = $state<any>(
|
||||||
|
mergeDefaults(othersSkeleton, data.defaultSettings?.othersFormData)
|
||||||
|
);
|
||||||
|
let continuationFormData = $state<any>(
|
||||||
|
mergeDefaults(continuationSkeleton, data.defaultSettings?.continuationFormData)
|
||||||
|
);
|
||||||
|
|
||||||
// Estados para saber si existen datos previos
|
// Estados para saber si existen datos previos
|
||||||
let observationExists = $state(false);
|
let observationExists = $state(!!data.defaultSettings?.observationFormData);
|
||||||
let itemsExists = $state(false);
|
let itemsExists = $state(!!(data.defaultSettings?.itemsFormData?.items?.length));
|
||||||
let othersExists = $state(false);
|
let othersExists = $state(!!data.defaultSettings?.othersFormData);
|
||||||
let continuationExists = $state(false);
|
let continuationExists = $state(!!data.defaultSettings?.continuationFormData);
|
||||||
|
|
||||||
let calculatedExchangeRate = $state<number | null>(
|
let calculatedExchangeRate = $state<number | null>(
|
||||||
data.invoice?.financials?.exchange_rate ?? null
|
data.invoice?.financials?.exchange_rate ?? null
|
||||||
@@ -196,12 +313,12 @@
|
|||||||
isCreate: data.isCreate || false,
|
isCreate: data.isCreate || false,
|
||||||
companyId: companyStore?.activeCompany?.id || 0,
|
companyId: companyStore?.activeCompany?.id || 0,
|
||||||
formData: {
|
formData: {
|
||||||
InvoiceTopFieldsFormData,
|
InvoiceTopFieldsFormData: $state.snapshot(InvoiceTopFieldsFormData),
|
||||||
generalFormData,
|
generalFormData: $state.snapshot(generalFormData),
|
||||||
observationFormData,
|
observationFormData: $state.snapshot(observationFormData),
|
||||||
itemsFormData,
|
itemsFormData: $state.snapshot(itemsFormData),
|
||||||
othersFormData,
|
othersFormData: $state.snapshot(othersFormData),
|
||||||
continuationFormData
|
continuationFormData: $state.snapshot(continuationFormData)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -267,6 +384,97 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isLoadingDefaults = $state(false);
|
||||||
|
let lastLoadedKey = $state(
|
||||||
|
data.defaultSettings
|
||||||
|
? `${InvoiceTopFieldsFormData.invoice_type}-${InvoiceTopFieldsFormData.operation_type}`
|
||||||
|
: ''
|
||||||
|
);
|
||||||
|
|
||||||
|
async function loadDefaults() {
|
||||||
|
if (
|
||||||
|
!data.isCreate ||
|
||||||
|
!InvoiceTopFieldsFormData.invoice_type ||
|
||||||
|
!InvoiceTopFieldsFormData.operation_type ||
|
||||||
|
!companyStore?.activeCompany?.id
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const currentKey = `${InvoiceTopFieldsFormData.invoice_type}-${InvoiceTopFieldsFormData.operation_type}`;
|
||||||
|
if (currentKey === lastLoadedKey) return;
|
||||||
|
|
||||||
|
isLoadingDefaults = true;
|
||||||
|
try {
|
||||||
|
const token = data.user?.token;
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/v1/a76/invoice-settings/${InvoiceTopFieldsFormData.invoice_type}?operation_type=${InvoiceTopFieldsFormData.operation_type}&company_id=${companyStore.activeCompany.id}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const settingsData = await res.json();
|
||||||
|
const settings = settingsData.settings || {};
|
||||||
|
|
||||||
|
// Mark as loaded even if empty to prevent retries for the same combination
|
||||||
|
lastLoadedKey = currentKey;
|
||||||
|
|
||||||
|
// Only notify and apply if there are actually settings
|
||||||
|
if (Object.keys(settings).length === 0) return;
|
||||||
|
|
||||||
|
// Re-apply merges with new defaults
|
||||||
|
InvoiceTopFieldsFormData = mergeDefaults(
|
||||||
|
topFieldsSkeleton,
|
||||||
|
settings.InvoiceTopFieldsFormData
|
||||||
|
);
|
||||||
|
generalFormData = mergeDefaults(generalSkeleton, settings.generalFormData);
|
||||||
|
observationFormData = mergeDefaults(observationSkeleton, settings.observationFormData);
|
||||||
|
itemsFormData = ensureItemsFormData(settings.itemsFormData);
|
||||||
|
othersFormData = mergeDefaults(othersSkeleton, settings.othersFormData);
|
||||||
|
continuationFormData = mergeDefaults(continuationSkeleton, settings.continuationFormData);
|
||||||
|
|
||||||
|
// Ensure types remain as selected
|
||||||
|
InvoiceTopFieldsFormData.invoice_type =
|
||||||
|
InvoiceTopFieldsFormData.invoice_type || data.filters?.invoice_type;
|
||||||
|
InvoiceTopFieldsFormData.operation_type =
|
||||||
|
InvoiceTopFieldsFormData.operation_type || data.filters?.operation_type;
|
||||||
|
|
||||||
|
observationExists = !!settings.observationFormData;
|
||||||
|
itemsExists = !!(itemsFormData.items?.length);
|
||||||
|
othersExists = !!settings.othersFormData;
|
||||||
|
continuationExists = !!settings.continuationFormData;
|
||||||
|
|
||||||
|
toast.info(
|
||||||
|
'Valores predeterminados cargados para ' + InvoiceTopFieldsFormData.invoice_type
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading defaults:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingDefaults = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic defaults loading when types change in "New" mode
|
||||||
|
$effect(() => {
|
||||||
|
if (
|
||||||
|
data.isCreate &&
|
||||||
|
mounted &&
|
||||||
|
companyStore?.activeCompany?.id &&
|
||||||
|
InvoiceTopFieldsFormData.invoice_type &&
|
||||||
|
InvoiceTopFieldsFormData.operation_type
|
||||||
|
) {
|
||||||
|
// Trigger only if types actually changed from initial load or previous selection
|
||||||
|
loadDefaults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useShortcuts(
|
useShortcuts(
|
||||||
'Invoice Edit',
|
'Invoice Edit',
|
||||||
obtenerAtajosEdicionFactura({
|
obtenerAtajosEdicionFactura({
|
||||||
|
|||||||
@@ -0,0 +1,546 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
Loader2,
|
||||||
|
Save,
|
||||||
|
ArrowLeft,
|
||||||
|
Package,
|
||||||
|
FileText,
|
||||||
|
Search,
|
||||||
|
Sparkles
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { itemPresetsApi, type ItemPreset } from '$lib/api/dashboard/a76/item-presets';
|
||||||
|
import { itemsApi, type Item } from '$lib/api/dashboard/a76/items';
|
||||||
|
import { companyStore } from '$lib/stores/company.svelte';
|
||||||
|
import * as Table from '$lib/components/ui/table';
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
|
import ItemSheetFa from '$lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte';
|
||||||
|
import ItemSheetInv from '$lib/components/dashboard/invoices/edit/items/inv/item-sheet-inv.svelte';
|
||||||
|
|
||||||
|
let presets = $state<ItemPreset[]>([]);
|
||||||
|
let searchTerm = $state('');
|
||||||
|
let isLoading = $state(true);
|
||||||
|
let isSaving = $state(false);
|
||||||
|
let showEditor = $state(false);
|
||||||
|
let isEditMode = $state(false);
|
||||||
|
let selectedPreset = $state<Partial<ItemPreset>>({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
items: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Item Editor State (mimicking ItemsTabForm)
|
||||||
|
let showItemSheet = $state(false);
|
||||||
|
let isItemEditMode = $state(false);
|
||||||
|
let editingItemIndex = $state<number | null>(null);
|
||||||
|
let editingItem = $state<Partial<Item>>({});
|
||||||
|
let itemSystem = $state<'fixed_asset' | 'scaii'>('scaii');
|
||||||
|
|
||||||
|
const activeCompanyId = $derived(companyStore.activeCompany?.id);
|
||||||
|
|
||||||
|
const filteredPresets = $derived(
|
||||||
|
presets.filter((preset) => {
|
||||||
|
const term = searchTerm.trim().toLowerCase();
|
||||||
|
if (!term) return true;
|
||||||
|
return (
|
||||||
|
preset.name.toLowerCase().includes(term) ||
|
||||||
|
preset.description?.toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalPresetItems = $derived(
|
||||||
|
presets.reduce((acc, preset) => acc + (preset.items?.length || 0), 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (activeCompanyId) {
|
||||||
|
loadPresets();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadPresets() {
|
||||||
|
if (!activeCompanyId) return;
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
const response = await itemPresetsApi.list(activeCompanyId);
|
||||||
|
presets = response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading presets:', error);
|
||||||
|
toast.error('Error al cargar presets');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAdd() {
|
||||||
|
isEditMode = false;
|
||||||
|
selectedPreset = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
showEditor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditPreset(preset: ItemPreset) {
|
||||||
|
isEditMode = true;
|
||||||
|
selectedPreset = JSON.parse(JSON.stringify(preset));
|
||||||
|
// Basic inference of system from first item
|
||||||
|
if (selectedPreset.items && selectedPreset.items.length > 0) {
|
||||||
|
// This is a bit arbitrary, but scaii is default
|
||||||
|
itemSystem = 'scaii';
|
||||||
|
}
|
||||||
|
showEditor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeletePreset(id: number) {
|
||||||
|
if (!activeCompanyId || !confirm('¿Estás seguro de eliminar esta plantilla?')) return;
|
||||||
|
try {
|
||||||
|
await itemPresetsApi.delete(id, activeCompanyId);
|
||||||
|
toast.success('Plantilla eliminada');
|
||||||
|
loadPresets();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting preset:', error);
|
||||||
|
toast.error('Error al eliminar plantilla');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSavePreset() {
|
||||||
|
if (!activeCompanyId || !selectedPreset.name) {
|
||||||
|
toast.warning('El nombre es obligatorio');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving = true;
|
||||||
|
try {
|
||||||
|
if (isEditMode && selectedPreset.id) {
|
||||||
|
await itemPresetsApi.update(selectedPreset.id, activeCompanyId, {
|
||||||
|
name: selectedPreset.name,
|
||||||
|
description: selectedPreset.description,
|
||||||
|
items: selectedPreset.items
|
||||||
|
});
|
||||||
|
toast.success('Plantilla actualizada');
|
||||||
|
} else {
|
||||||
|
await itemPresetsApi.create(activeCompanyId, {
|
||||||
|
name: selectedPreset.name!,
|
||||||
|
description: selectedPreset.description,
|
||||||
|
items: selectedPreset.items || []
|
||||||
|
});
|
||||||
|
toast.success('Plantilla creada');
|
||||||
|
}
|
||||||
|
showEditor = false;
|
||||||
|
loadPresets();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving preset:', error);
|
||||||
|
toast.error('Error al guardar plantilla');
|
||||||
|
} finally {
|
||||||
|
isSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Item Management Logic (within Preset) ---
|
||||||
|
|
||||||
|
function handleAddItem() {
|
||||||
|
isItemEditMode = false;
|
||||||
|
editingItemIndex = null;
|
||||||
|
editingItem = {
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
line_number: (selectedPreset.items?.length || 0) + 1,
|
||||||
|
quantity: { quantity: 1 },
|
||||||
|
financial: { unit_cost_usd: 0 },
|
||||||
|
customs: {},
|
||||||
|
description: {},
|
||||||
|
reference: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
showItemSheet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEditItem(index: number) {
|
||||||
|
isItemEditMode = true;
|
||||||
|
editingItemIndex = index;
|
||||||
|
editingItem = JSON.parse(JSON.stringify(selectedPreset.items![index]));
|
||||||
|
showItemSheet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteItem(index: number) {
|
||||||
|
selectedPreset.items!.splice(index, 1);
|
||||||
|
selectedPreset.items = [...selectedPreset.items!]; // Trigger reactivity
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveItemToPreset() {
|
||||||
|
if (isItemEditMode && editingItemIndex !== null) {
|
||||||
|
selectedPreset.items![editingItemIndex] = editingItem as any;
|
||||||
|
} else {
|
||||||
|
selectedPreset.items = [...(selectedPreset.items || []), editingItem as any];
|
||||||
|
}
|
||||||
|
showItemSheet = false;
|
||||||
|
toast.success(
|
||||||
|
isItemEditMode ? 'Partida actualizada en plantilla' : 'Partida añadida a plantilla'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock invoice to satisfy ItemSheets
|
||||||
|
const mockInvoice = $derived({
|
||||||
|
system: itemSystem,
|
||||||
|
invoice_number: 'PLANTILLA',
|
||||||
|
operation_type: 'exp',
|
||||||
|
purchase_order: ''
|
||||||
|
} as any);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto py-6">
|
||||||
|
<div class="mb-6 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Button variant="ghost" size="icon" onclick={() => goto('/dashboard/invoices')}>
|
||||||
|
<ArrowLeft class="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight">Plantillas de Partidas</h1>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Gestiona, edita y prueba plantillas reutilizables sin perderte en la UI.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<Button variant="outline" size="sm" onclick={loadPresets} disabled={isLoading}>
|
||||||
|
<Loader2 class={`mr-2 h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} />
|
||||||
|
Refrescar
|
||||||
|
</Button>
|
||||||
|
<Button onclick={handleAdd}>
|
||||||
|
<Plus class="mr-2 h-4 w-4" />
|
||||||
|
Nueva Plantilla
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-3 md:grid-cols-3 mb-6">
|
||||||
|
<div class="bg-white dark:bg-zinc-950 border rounded-lg p-4 shadow-sm">
|
||||||
|
<p class="text-xs uppercase text-muted-foreground font-semibold">Total plantillas</p>
|
||||||
|
<p class="text-2xl font-bold mt-1">{presets.length}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white dark:bg-zinc-950 border rounded-lg p-4 shadow-sm">
|
||||||
|
<p class="text-xs uppercase text-muted-foreground font-semibold">Partidas totales</p>
|
||||||
|
<p class="text-2xl font-bold mt-1">{totalPresetItems}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-lg p-4 shadow-sm flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-xs uppercase font-semibold opacity-80">Tip</p>
|
||||||
|
<p class="text-sm mt-1 opacity-90">Usa plantillas para acelerar la captura.</p>
|
||||||
|
</div>
|
||||||
|
<Sparkles class="h-6 w-6 opacity-80" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header class="pb-3">
|
||||||
|
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Card.Title class="text-base">Listado de plantillas</Card.Title>
|
||||||
|
<Card.Description>Mira, filtra y edita tus plantillas guardadas.</Card.Description>
|
||||||
|
</div>
|
||||||
|
<div class="relative w-full lg:w-64">
|
||||||
|
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar por nombre o descripción"
|
||||||
|
class="pl-9"
|
||||||
|
bind:value={searchTerm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="p-0">
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header>
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Head>Nombre</Table.Head>
|
||||||
|
<Table.Head>Descripción</Table.Head>
|
||||||
|
<Table.Head class="text-center">Items</Table.Head>
|
||||||
|
<Table.Head>Fecha</Table.Head>
|
||||||
|
<Table.Head class="text-right">Acciones</Table.Head>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{#if isLoading}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={5} class="text-center py-10">
|
||||||
|
<Loader2 class="h-8 w-8 animate-spin mx-auto text-primary" />
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{:else}
|
||||||
|
{#each filteredPresets as preset}
|
||||||
|
<Table.Row class="hover:bg-muted/40 transition-colors">
|
||||||
|
<Table.Cell class="font-medium">{preset.name}</Table.Cell>
|
||||||
|
<Table.Cell class="max-w-[280px] text-sm text-muted-foreground">
|
||||||
|
<span class="line-clamp-2">{preset.description || 'Sin descripción'}</span>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-center text-sm font-semibold">
|
||||||
|
{preset.items?.length || 0} partidas
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-sm text-muted-foreground">
|
||||||
|
{preset.created_at
|
||||||
|
? new Date(preset.created_at).toLocaleDateString()
|
||||||
|
: '-'}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-right">
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button size="icon" variant="ghost" onclick={() => handleEditPreset(preset)}>
|
||||||
|
<Pencil class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onclick={() => handleDeletePreset(preset.id)}
|
||||||
|
>
|
||||||
|
<Trash2 class="h-4 w-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
{#if filteredPresets.length === 0}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={5} class="text-center py-12 text-muted-foreground">
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<Package class="h-8 w-8 opacity-30" />
|
||||||
|
<p class="text-sm">No hay plantillas que coincidan con tu búsqueda.</p>
|
||||||
|
<Button variant="outline" size="sm" onclick={handleAdd}>
|
||||||
|
<Plus class="w-4 h-4 mr-2" /> Crear la primera
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Editor como popup modal -->
|
||||||
|
<Dialog.Root bind:open={showEditor}>
|
||||||
|
<Dialog.Content class="sm:max-w-5xl max-h-[90vh] p-0 overflow-hidden">
|
||||||
|
<div class="bg-white dark:bg-zinc-950 border-b dark:border-zinc-800 px-6 py-4 shadow-sm flex items-start justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="bg-primary/10 p-2 rounded-lg text-primary">
|
||||||
|
<Package class="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Dialog.Title class="text-lg font-bold">
|
||||||
|
{isEditMode ? 'Editar Plantilla' : 'Nueva Plantilla'}
|
||||||
|
</Dialog.Title>
|
||||||
|
<Dialog.Description class="text-sm text-muted-foreground">
|
||||||
|
Configura los items que compondrán esta plantilla.
|
||||||
|
</Dialog.Description>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button variant="outline" onclick={() => (showEditor = false)} disabled={isSaving}
|
||||||
|
>Cancelar</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onclick={handleSavePreset}
|
||||||
|
disabled={isSaving}
|
||||||
|
class="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{#if isSaving}
|
||||||
|
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Guardando...
|
||||||
|
{:else}
|
||||||
|
<Save class="mr-2 h-4 w-4" />
|
||||||
|
{isEditMode ? 'Actualizar Plantilla' : 'Crear Plantilla'}
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 overflow-auto max-h-[calc(90vh-88px)] bg-slate-50/70 dark:bg-black">
|
||||||
|
<div class="max-w-6xl mx-auto space-y-6">
|
||||||
|
<!-- Metadata Card -->
|
||||||
|
<div class="bg-white dark:bg-zinc-950 p-6 rounded-lg border shadow-sm space-y-4">
|
||||||
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-zinc-500">
|
||||||
|
Información General
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="name">Nombre de la Plantilla</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
bind:value={selectedPreset.name}
|
||||||
|
placeholder="Ej. Aceros Estándar"
|
||||||
|
class="bg-slate-50/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label for="description">Descripción (Opcional)</Label>
|
||||||
|
<Input
|
||||||
|
id="description"
|
||||||
|
bind:value={selectedPreset.description}
|
||||||
|
placeholder="Breve descripción..."
|
||||||
|
class="bg-slate-50/50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Items Management Card -->
|
||||||
|
<div class="bg-white dark:bg-zinc-950 p-6 rounded-lg border shadow-sm space-y-4">
|
||||||
|
<div class="flex justify-between items-start gap-3 flex-wrap">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-zinc-500">
|
||||||
|
Partidas de la Plantilla
|
||||||
|
</h3>
|
||||||
|
<p class="text-[11px] text-muted-foreground mt-1">
|
||||||
|
Estas partidas se inyectarán en la factura al cargar la plantilla.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex items-center gap-2 mr-2">
|
||||||
|
<Label class="text-[11px] text-zinc-400">Sistema:</Label>
|
||||||
|
<select
|
||||||
|
bind:value={itemSystem}
|
||||||
|
class="text-[11px] border rounded-md px-2 py-1 bg-slate-50 dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800"
|
||||||
|
>
|
||||||
|
<option value="scaii">SCAII (Inventario)</option>
|
||||||
|
<option value="fixed_asset">Fixed Asset</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" onclick={handleAddItem} class="h-8">
|
||||||
|
<Plus class="w-3.5 h-3.5 mr-2" />
|
||||||
|
Agregar Partida
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border rounded-md overflow-hidden bg-white dark:bg-zinc-950">
|
||||||
|
<Table.Root>
|
||||||
|
<Table.Header class="bg-zinc-50 dark:bg-zinc-900/50">
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Head class="w-12 text-center">#</Table.Head>
|
||||||
|
<Table.Head>Descripción (ES)</Table.Head>
|
||||||
|
<Table.Head class="text-center">Cantidad</Table.Head>
|
||||||
|
<Table.Head class="text-center">Precio (USD)</Table.Head>
|
||||||
|
<Table.Head class="text-right pr-6">Acciones</Table.Head>
|
||||||
|
</Table.Row>
|
||||||
|
</Table.Header>
|
||||||
|
<Table.Body>
|
||||||
|
{#if !selectedPreset.items || selectedPreset.items.length === 0}
|
||||||
|
<Table.Row>
|
||||||
|
<Table.Cell colspan={5} class="text-center py-16 text-muted-foreground">
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<Package class="h-8 w-8 opacity-20" />
|
||||||
|
<p class="text-sm italic">No hay partidas agregadas aún.</p>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onclick={handleAddItem}
|
||||||
|
class="mt-2 text-primary"
|
||||||
|
>
|
||||||
|
Haz clic aquí para agregar la primera
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{:else}
|
||||||
|
{#each selectedPreset.items as item, i}
|
||||||
|
<Table.Row class="hover:bg-slate-50/50 dark:hover:bg-zinc-900/30">
|
||||||
|
<Table.Cell class="text-center font-mono text-[11px] text-zinc-400"
|
||||||
|
>{i + 1}</Table.Cell
|
||||||
|
>
|
||||||
|
<Table.Cell>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-medium text-sm">
|
||||||
|
{item.lines?.[0]?.description?.description_spanish || 'Sin descripción'}
|
||||||
|
</span>
|
||||||
|
{#if item.reference_number}
|
||||||
|
<span class="text-[10px] text-muted-foreground flex items-center gap-1">
|
||||||
|
<FileText class="w-3 h-3" /> Ref: {item.reference_number}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-center tabular-nums font-medium"
|
||||||
|
>{item.lines?.[0]?.quantity?.quantity || 0}</Table.Cell
|
||||||
|
>
|
||||||
|
<Table.Cell
|
||||||
|
class="text-center tabular-nums font-medium text-emerald-600 dark:text-emerald-400"
|
||||||
|
>
|
||||||
|
${(item.lines?.[0]?.financial?.unit_cost_usd || 0).toLocaleString()}
|
||||||
|
</Table.Cell>
|
||||||
|
<Table.Cell class="text-right pr-6">
|
||||||
|
<div class="flex justify-end gap-1">
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onclick={() => handleEditItem(i)}
|
||||||
|
class="h-8 w-8 text-zinc-500 hover:text-primary"
|
||||||
|
>
|
||||||
|
<Pencil class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="ghost"
|
||||||
|
onclick={() => handleDeleteItem(i)}
|
||||||
|
class="h-8 w-8 text-zinc-500 hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-3.5 h-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Table.Cell>
|
||||||
|
</Table.Row>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</Table.Body>
|
||||||
|
</Table.Root>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if selectedPreset.items && selectedPreset.items.length > 0}
|
||||||
|
<div class="flex justify-between items-center text-[11px] text-muted-foreground px-1">
|
||||||
|
<p>
|
||||||
|
Total de partidas: <span class="font-bold text-zinc-900 dark:text-zinc-100"
|
||||||
|
>{selectedPreset.items.length}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
|
||||||
|
<!-- Reuse ItemSheets -->
|
||||||
|
{#if itemSystem === 'fixed_asset'}
|
||||||
|
<ItemSheetFa
|
||||||
|
bind:open={showItemSheet}
|
||||||
|
isEditMode={isItemEditMode}
|
||||||
|
bind:editingItem
|
||||||
|
invoice={mockInvoice}
|
||||||
|
onSave={saveItemToPreset}
|
||||||
|
onCancel={() => (showItemSheet = false)}
|
||||||
|
isSaving={false}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<ItemSheetInv
|
||||||
|
bind:open={showItemSheet}
|
||||||
|
isEditMode={isItemEditMode}
|
||||||
|
bind:editingItem
|
||||||
|
invoice={mockInvoice}
|
||||||
|
onSave={saveItemToPreset}
|
||||||
|
onCancel={() => (showItemSheet = false)}
|
||||||
|
isSaving={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
208
frontend/src/routes/dashboard/invoices/settings/+page.server.ts
Normal file
208
frontend/src/routes/dashboard/invoices/settings/+page.server.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { error, redirect } from '@sveltejs/kit';
|
||||||
|
import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ cookies, fetch, url }) => {
|
||||||
|
const { accessToken } = getAuthTokens(cookies);
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
throw redirect(302, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyId = await getActiveCompanyId(cookies, fetch);
|
||||||
|
if (!companyId) {
|
||||||
|
throw error(400, 'No se encontró una compañía seleccionada');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load necessary reference data for the settings form
|
||||||
|
// We need: InvoiceTypes, CustomsBrokers, Incoterms, etc. to populate the reusable forms
|
||||||
|
|
||||||
|
// We load minimal data initially, but to support all tabs we need more.
|
||||||
|
// Reuse the same calls as in edit/[id] to ensure we have data for the dropdowns
|
||||||
|
|
||||||
|
const invoiceTypesPromise = authenticatedFetch(
|
||||||
|
'v1/public/reference_data/invoice-types/?page=1&page_size=100',
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const customsBrokersPromise = authenticatedFetch(
|
||||||
|
`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const clientsPromise = authenticatedFetch(
|
||||||
|
`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const providersPromise = authenticatedFetch(
|
||||||
|
`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const currencyTypesPromise = authenticatedFetch(
|
||||||
|
'v1/public/reference_data/currency-types/?page=1&page_size=100',
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const transportTypesPromise = authenticatedFetch(
|
||||||
|
'v1/public/reference_data/transport-types/?page=1&page_size=100',
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const transportersPromise = authenticatedFetch(
|
||||||
|
`v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const vehiclesPromise = authenticatedFetch(
|
||||||
|
`v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const driversPromise = authenticatedFetch(
|
||||||
|
`v1/a76/transportation/drivers/?company_id=${companyId}`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const trailersPromise = authenticatedFetch(
|
||||||
|
`v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const incotermsPromise = authenticatedFetch(
|
||||||
|
'v1/public/reference_data/incoterms/?page=1&page_size=100',
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const customsSectionsPromise = authenticatedFetch(
|
||||||
|
'v1/public/reference_data/customs-sections/?page=1&page_size=100',
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const codePedimentoRegimensPromise = authenticatedFetch(
|
||||||
|
'v1/public/reference_data/code-pedimento-regimens/?page=1&page_size=1000',
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const transportModesPromise = authenticatedFetch(
|
||||||
|
'v1/public/reference_data/transport-modes/?page=1&page_size=100',
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
const pedimentosPromise = authenticatedFetch(
|
||||||
|
`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`,
|
||||||
|
{},
|
||||||
|
cookies,
|
||||||
|
fetch
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [
|
||||||
|
invoiceTypesResponse,
|
||||||
|
customsBrokersResponse,
|
||||||
|
clientsResponse,
|
||||||
|
providersResponse,
|
||||||
|
currencyTypesResponse,
|
||||||
|
transportTypesResponse,
|
||||||
|
transportersResponse,
|
||||||
|
vehiclesResponse,
|
||||||
|
driversResponse,
|
||||||
|
trailersResponse,
|
||||||
|
incotermsResponse,
|
||||||
|
customsSectionsResponse,
|
||||||
|
codePedimentoRegimensResponse,
|
||||||
|
transportModesResponse,
|
||||||
|
pedimentosResponse
|
||||||
|
] = await Promise.all([
|
||||||
|
invoiceTypesPromise,
|
||||||
|
customsBrokersPromise,
|
||||||
|
clientsPromise,
|
||||||
|
providersPromise,
|
||||||
|
currencyTypesPromise,
|
||||||
|
transportTypesPromise,
|
||||||
|
transportersPromise,
|
||||||
|
vehiclesPromise,
|
||||||
|
driversPromise,
|
||||||
|
trailersPromise,
|
||||||
|
incotermsPromise,
|
||||||
|
customsSectionsPromise,
|
||||||
|
codePedimentoRegimensPromise,
|
||||||
|
transportModesPromise,
|
||||||
|
pedimentosPromise
|
||||||
|
]);
|
||||||
|
|
||||||
|
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
|
||||||
|
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
|
||||||
|
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
|
||||||
|
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
|
||||||
|
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
|
||||||
|
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
|
||||||
|
const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] };
|
||||||
|
const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] };
|
||||||
|
const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] };
|
||||||
|
const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] };
|
||||||
|
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
||||||
|
const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] };
|
||||||
|
const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] };
|
||||||
|
const transportModes = transportModesResponse.ok ? await transportModesResponse.json() : { items: [] };
|
||||||
|
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
||||||
|
|
||||||
|
return {
|
||||||
|
invoiceTypes: invoiceTypes.items || [],
|
||||||
|
customsBrokers: customsBrokers.items || [],
|
||||||
|
clients: clients.items || [],
|
||||||
|
providers: providers.items || [],
|
||||||
|
currencyTypes: currencyTypes.items || [],
|
||||||
|
transportTypes: transportTypes.items || [],
|
||||||
|
transporters: transporters.items || [],
|
||||||
|
vehicles: vehicles.items || [],
|
||||||
|
drivers: drivers.items || [],
|
||||||
|
trailers: trailers.items || [],
|
||||||
|
incoterms: incoterms.items || [],
|
||||||
|
customsSections: customsSections.items || [],
|
||||||
|
codePedimentoRegimens: codePedimentoRegimens.items || [],
|
||||||
|
transportModes: transportModes.items || [],
|
||||||
|
pedimentos: pedimentos.items || [],
|
||||||
|
companyId
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading settings data:', err);
|
||||||
|
return {
|
||||||
|
invoiceTypes: [],
|
||||||
|
customsBrokers: [],
|
||||||
|
incoterms: [],
|
||||||
|
customsSections: [],
|
||||||
|
companyId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
585
frontend/src/routes/dashboard/invoices/settings/+page.svelte
Normal file
585
frontend/src/routes/dashboard/invoices/settings/+page.svelte
Normal file
@@ -0,0 +1,585 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { LoaderCircle, Save, FileText, Eye, DollarSign, Truck, Package } from 'lucide-svelte';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '$lib/components/ui/select';
|
||||||
|
// Import Form Components
|
||||||
|
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
|
||||||
|
import ObservationsTabForm from '$lib/components/dashboard/invoices/edit/observations-tab-form.svelte';
|
||||||
|
import ItemsTabForm from '$lib/components/dashboard/invoices/edit/items/items-tab-form.svelte';
|
||||||
|
import OthersTabForm from '$lib/components/dashboard/invoices/edit/others-tab-form.svelte';
|
||||||
|
import InvoiceTopFields from '$lib/components/dashboard/invoices/edit/invoice-top-fields.svelte';
|
||||||
|
import ContinuationTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
|
// State
|
||||||
|
let selectedInvoiceType = $state<string>('');
|
||||||
|
let selectedOperationType = $state<string>('');
|
||||||
|
let isLoading = $state(false);
|
||||||
|
let isSaving = $state(false);
|
||||||
|
let activeTab = $state('general');
|
||||||
|
let companyStore: any = $state(undefined);
|
||||||
|
|
||||||
|
// Form Data State
|
||||||
|
let InvoiceTopFieldsFormData = $state<any>({});
|
||||||
|
let generalFormData = $state<any>({});
|
||||||
|
let observationFormData = $state<any>({});
|
||||||
|
let itemsFormData = $state<any>({});
|
||||||
|
let othersFormData = $state<any>({
|
||||||
|
comments_status: '',
|
||||||
|
transport_mode: 'TRUCK',
|
||||||
|
is_mixed: null,
|
||||||
|
print_stamp: false,
|
||||||
|
rule_3121_parties_ii: false,
|
||||||
|
related_doc_id: null,
|
||||||
|
code_signature: '',
|
||||||
|
electronic_signature: '',
|
||||||
|
mandatory_person: '',
|
||||||
|
contingency_mode: false,
|
||||||
|
cove: '',
|
||||||
|
operation_num: '',
|
||||||
|
adendas: '',
|
||||||
|
observations_vu: '',
|
||||||
|
certified_number: ''
|
||||||
|
});
|
||||||
|
let continuationFormData = $state<any>({
|
||||||
|
numero_tipo_transporte: '',
|
||||||
|
es_ferrocarril: 'no',
|
||||||
|
numero_bl: '',
|
||||||
|
cantidad_guias_embarque: null,
|
||||||
|
destino_origen: '',
|
||||||
|
puerto_entrada: '',
|
||||||
|
fue_revisado_equipo: false,
|
||||||
|
sub_division: false,
|
||||||
|
funge_como_cd: false,
|
||||||
|
llego_pedimento: false,
|
||||||
|
errores_facturacion: [],
|
||||||
|
semaforo_verde_aduana_mexicana: false,
|
||||||
|
semaforo_verde_aduana_americana: false,
|
||||||
|
semaforo_rojo_aduana_mexicana: false,
|
||||||
|
semaforo_rojo_aduana_americana: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dummy objects for components
|
||||||
|
let invoice = $state<any>({
|
||||||
|
financials: { exchange_rate: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Existence flags (not strictly needed for settings but required by components)
|
||||||
|
let observationExists = $state(false);
|
||||||
|
let itemsExists = $state(false);
|
||||||
|
let othersExists = $state(false);
|
||||||
|
let continuationExists = $state(false);
|
||||||
|
|
||||||
|
const operationTypes = [
|
||||||
|
{ value: 'imp', label: 'Importación' },
|
||||||
|
{ value: 'exp', label: 'Exportación' }
|
||||||
|
];
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const companyStoreModule = await import('$lib/stores/company.svelte');
|
||||||
|
companyStore = companyStoreModule.companyStore;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading company store:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadSettings() {
|
||||||
|
if (!selectedInvoiceType || !selectedOperationType || !companyStore?.activeCompany?.id) return;
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
try {
|
||||||
|
const token = $page.data.user?.token;
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/v1/a76/invoice-settings/${selectedInvoiceType}?operation_type=${selectedOperationType}&company_id=${companyStore.activeCompany.id}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const settingsData = await res.json();
|
||||||
|
if (settingsData && settingsData.settings) {
|
||||||
|
applySettings(settingsData.settings);
|
||||||
|
} else {
|
||||||
|
resetForms();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resetForms();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading settings', error);
|
||||||
|
resetForms();
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySettings(settings: any) {
|
||||||
|
InvoiceTopFieldsFormData = {
|
||||||
|
is_pedimento_pending: false,
|
||||||
|
pedimento_id: '',
|
||||||
|
remesa: '',
|
||||||
|
invoice_number: '',
|
||||||
|
invoice_date: new Date().toISOString().split('T')[0],
|
||||||
|
emission_date: new Date().toISOString().split('T')[0],
|
||||||
|
operation_type: selectedOperationType,
|
||||||
|
invoice_type: selectedInvoiceType,
|
||||||
|
fecha_pedimento_del: '',
|
||||||
|
fecha_pedimento_al: '',
|
||||||
|
clave_pedimento: '',
|
||||||
|
regimen_pedimento: '',
|
||||||
|
...(settings.InvoiceTopFieldsFormData || {})
|
||||||
|
};
|
||||||
|
generalFormData = {
|
||||||
|
provider_header: 'proveedor',
|
||||||
|
provider_id: null,
|
||||||
|
sold_to_header: 'consignado_a',
|
||||||
|
sold_to_id: null,
|
||||||
|
shipped_to_header: 'enviado_a',
|
||||||
|
shipped_to_id: null,
|
||||||
|
customs_broker_id: null,
|
||||||
|
customs_broker_us_id: null,
|
||||||
|
currency_type: '',
|
||||||
|
currency: 'foreign',
|
||||||
|
exchange_rate: null,
|
||||||
|
weight_type: 'kgs',
|
||||||
|
iva_factor: null,
|
||||||
|
carrier_id: null,
|
||||||
|
transport_id: '',
|
||||||
|
driver_name: '',
|
||||||
|
transport_type: '',
|
||||||
|
transport_num: '',
|
||||||
|
aduana: '',
|
||||||
|
document_type: '',
|
||||||
|
...(settings.generalFormData || {})
|
||||||
|
};
|
||||||
|
observationFormData = {
|
||||||
|
observation_es: '',
|
||||||
|
observation_en: '',
|
||||||
|
freight: null,
|
||||||
|
insurance_value: null,
|
||||||
|
insurance: null,
|
||||||
|
packaging: null,
|
||||||
|
other_increments: null,
|
||||||
|
total_increments_mn: null,
|
||||||
|
total_increments_me: null,
|
||||||
|
incoterm: null,
|
||||||
|
enclosure: null,
|
||||||
|
num_seals: null,
|
||||||
|
movement_type: '',
|
||||||
|
alternate_invoice: '',
|
||||||
|
valuation_method: null,
|
||||||
|
...(settings.observationFormData || {})
|
||||||
|
};
|
||||||
|
itemsFormData = settings.itemsFormData || {};
|
||||||
|
othersFormData = {
|
||||||
|
comments_status: '',
|
||||||
|
transport_mode: 'TRUCK',
|
||||||
|
is_mixed: null,
|
||||||
|
print_stamp: false,
|
||||||
|
rule_3121_parties_ii: false,
|
||||||
|
related_doc_id: null,
|
||||||
|
code_signature: '',
|
||||||
|
electronic_signature: '',
|
||||||
|
mandatory_person: '',
|
||||||
|
contingency_mode: false,
|
||||||
|
cove: '',
|
||||||
|
operation_num: '',
|
||||||
|
adendas: '',
|
||||||
|
observations_vu: '',
|
||||||
|
certified_number: '',
|
||||||
|
...(settings.othersFormData || {})
|
||||||
|
};
|
||||||
|
continuationFormData = {
|
||||||
|
numero_tipo_transporte: '',
|
||||||
|
es_ferrocarril: 'no',
|
||||||
|
numero_bl: '',
|
||||||
|
cantidad_guias_embarque: null,
|
||||||
|
destino_origen: '',
|
||||||
|
puerto_entrada: '',
|
||||||
|
fue_revisado_equipo: false,
|
||||||
|
sub_division: false,
|
||||||
|
funge_como_cd: false,
|
||||||
|
llego_pedimento: false,
|
||||||
|
errores_facturacion: [],
|
||||||
|
semaforo_verde_aduana_mexicana: false,
|
||||||
|
semaforo_verde_aduana_americana: false,
|
||||||
|
semaforo_rojo_aduana_mexicana: false,
|
||||||
|
semaforo_rojo_aduana_americana: false,
|
||||||
|
...(settings.continuationFormData || {})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the 'invoice' dummy object to reflect some top fields if needed for display
|
||||||
|
invoice = {
|
||||||
|
...invoice,
|
||||||
|
...InvoiceTopFieldsFormData,
|
||||||
|
operation_type: selectedOperationType,
|
||||||
|
invoice_type: selectedInvoiceType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForms() {
|
||||||
|
InvoiceTopFieldsFormData = {
|
||||||
|
is_pedimento_pending: false,
|
||||||
|
pedimento_id: '',
|
||||||
|
remesa: '',
|
||||||
|
invoice_number: '',
|
||||||
|
invoice_date: new Date().toISOString().split('T')[0],
|
||||||
|
emission_date: new Date().toISOString().split('T')[0],
|
||||||
|
operation_type: selectedOperationType,
|
||||||
|
invoice_type: selectedInvoiceType,
|
||||||
|
fecha_pedimento_del: '',
|
||||||
|
fecha_pedimento_al: '',
|
||||||
|
clave_pedimento: '',
|
||||||
|
regimen_pedimento: ''
|
||||||
|
};
|
||||||
|
generalFormData = {
|
||||||
|
provider_header: 'proveedor',
|
||||||
|
provider_id: null,
|
||||||
|
sold_to_header: 'consignado_a',
|
||||||
|
sold_to_id: null,
|
||||||
|
shipped_to_header: 'enviado_a',
|
||||||
|
shipped_to_id: null,
|
||||||
|
customs_broker_id: null,
|
||||||
|
customs_broker_us_id: null,
|
||||||
|
currency_type: '',
|
||||||
|
currency: 'foreign',
|
||||||
|
exchange_rate: null,
|
||||||
|
weight_type: 'kgs',
|
||||||
|
iva_factor: null,
|
||||||
|
carrier_id: null,
|
||||||
|
transport_id: '',
|
||||||
|
driver_name: '',
|
||||||
|
transport_type: '',
|
||||||
|
transport_num: '',
|
||||||
|
aduana: '',
|
||||||
|
document_type: ''
|
||||||
|
};
|
||||||
|
observationFormData = {
|
||||||
|
observation_es: '',
|
||||||
|
observation_en: '',
|
||||||
|
freight: null,
|
||||||
|
insurance_value: null,
|
||||||
|
insurance: null,
|
||||||
|
packaging: null,
|
||||||
|
other_increments: null,
|
||||||
|
total_increments_mn: null,
|
||||||
|
total_increments_me: null,
|
||||||
|
incoterm: null,
|
||||||
|
enclosure: null,
|
||||||
|
num_seals: null,
|
||||||
|
movement_type: '',
|
||||||
|
alternate_invoice: '',
|
||||||
|
valuation_method: null
|
||||||
|
};
|
||||||
|
itemsFormData = {};
|
||||||
|
othersFormData = {
|
||||||
|
comments_status: '',
|
||||||
|
transport_mode: 'TRUCK',
|
||||||
|
is_mixed: null,
|
||||||
|
print_stamp: false,
|
||||||
|
rule_3121_parties_ii: false,
|
||||||
|
related_doc_id: null,
|
||||||
|
code_signature: '',
|
||||||
|
electronic_signature: '',
|
||||||
|
mandatory_person: '',
|
||||||
|
contingency_mode: false,
|
||||||
|
cove: '',
|
||||||
|
operation_num: '',
|
||||||
|
adendas: '',
|
||||||
|
observations_vu: '',
|
||||||
|
certified_number: ''
|
||||||
|
};
|
||||||
|
continuationFormData = {
|
||||||
|
numero_tipo_transporte: '',
|
||||||
|
es_ferrocarril: 'no',
|
||||||
|
numero_bl: '',
|
||||||
|
cantidad_guias_embarque: null,
|
||||||
|
destino_origen: '',
|
||||||
|
puerto_entrada: '',
|
||||||
|
fue_revisado_equipo: false,
|
||||||
|
sub_division: false,
|
||||||
|
funge_como_cd: false,
|
||||||
|
llego_pedimento: false,
|
||||||
|
errores_facturacion: [],
|
||||||
|
semaforo_verde_aduana_mexicana: false,
|
||||||
|
semaforo_verde_aduana_americana: false,
|
||||||
|
semaforo_rojo_aduana_mexicana: false,
|
||||||
|
semaforo_rojo_aduana_americana: false
|
||||||
|
};
|
||||||
|
invoice = { financials: { exchange_rate: 0 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanObject(obj: any): any {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return obj.map(cleanObject).filter((v) => v !== null && v !== undefined && v !== '');
|
||||||
|
}
|
||||||
|
if (obj !== null && typeof obj === 'object') {
|
||||||
|
return Object.entries(obj).reduce((acc: any, [key, value]) => {
|
||||||
|
const cleaned = cleanObject(value);
|
||||||
|
if (
|
||||||
|
cleaned !== null &&
|
||||||
|
cleaned !== undefined &&
|
||||||
|
cleaned !== '' &&
|
||||||
|
!(typeof cleaned === 'object' && Object.keys(cleaned).length === 0)
|
||||||
|
) {
|
||||||
|
acc[key] = cleaned;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveSettings() {
|
||||||
|
if (!selectedInvoiceType || !selectedOperationType || !companyStore?.activeCompany?.id) {
|
||||||
|
toast.error('Por favor selecciona tipo de factura y operación');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving = true;
|
||||||
|
|
||||||
|
// Use snaphot to get clean data from Svelte 5 proxies
|
||||||
|
const rawSettings = {
|
||||||
|
InvoiceTopFieldsFormData: $state.snapshot(InvoiceTopFieldsFormData),
|
||||||
|
generalFormData: $state.snapshot(generalFormData),
|
||||||
|
observationFormData: $state.snapshot(observationFormData),
|
||||||
|
itemsFormData: $state.snapshot(itemsFormData),
|
||||||
|
othersFormData: $state.snapshot(othersFormData),
|
||||||
|
continuationFormData: $state.snapshot(continuationFormData)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean the settings to only save what's configured
|
||||||
|
const cleanedSettings = cleanObject(rawSettings);
|
||||||
|
|
||||||
|
const settingsPayload = {
|
||||||
|
invoice_type: selectedInvoiceType,
|
||||||
|
operation_type: selectedOperationType,
|
||||||
|
settings: cleanedSettings
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = $page.data.user?.token;
|
||||||
|
const headers: HeadersInit = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`/api/v1/a76/invoice-settings/?company_id=${companyStore.activeCompany.id}`,
|
||||||
|
{
|
||||||
|
method: 'PUT',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(settingsPayload)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
toast.success('Configuración guardada correctamente.');
|
||||||
|
} else {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
console.error('Save error details:', errorData);
|
||||||
|
toast.error('Error al guardar la configuración.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving settings:', error);
|
||||||
|
toast.error('Ocurrió un error al guardar.');
|
||||||
|
} finally {
|
||||||
|
isSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for selection changes
|
||||||
|
$effect(() => {
|
||||||
|
if (selectedInvoiceType && selectedOperationType && companyStore?.activeCompany?.id) {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container mx-auto py-6 pb-32">
|
||||||
|
<!-- Added padding bottom for footer -->
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold tracking-tight">Configuración de Facturas</h1>
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
Define los valores predeterminados para la creación de facturas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card.Root class="mb-6">
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>Contexto de Configuración</Card.Title>
|
||||||
|
<Card.Description
|
||||||
|
>Selecciona el contexto para editar sus valores por defecto.</Card.Description
|
||||||
|
>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content class="flex gap-4">
|
||||||
|
<div class="w-[250px]">
|
||||||
|
<Label>Tipo de Operación</Label>
|
||||||
|
<Select type="single" bind:value={selectedOperationType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{operationTypes.find((t) => t.value === selectedOperationType)?.label ||
|
||||||
|
'Seleccionar...'}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each operationTypes as type}
|
||||||
|
<SelectItem value={type.value}>{type.label}</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-[350px]">
|
||||||
|
<Label>Tipo de Factura</Label>
|
||||||
|
<Select type="single" bind:value={selectedInvoiceType}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{@const type = data.invoiceTypes.find((t) => t.key === selectedInvoiceType)}
|
||||||
|
{type ? `${type.key} - ${type.description}` : 'Seleccionar...'}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent class="max-h-[300px]">
|
||||||
|
{#each data.invoiceTypes as type}
|
||||||
|
<SelectItem value={type.key}>{type.key} - {type.description}</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
|
||||||
|
{#if selectedInvoiceType && selectedOperationType}
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="flex justify-center py-12">
|
||||||
|
<LoaderCircle class="animate-spin h-8 w-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="grid gap-6">
|
||||||
|
<!-- Top Fields (Header) -->
|
||||||
|
<div class="bg-background rounded-lg border p-4">
|
||||||
|
<h3 class="font-medium mb-4">Encabezado</h3>
|
||||||
|
<InvoiceTopFields
|
||||||
|
{invoice}
|
||||||
|
bind:formData={InvoiceTopFieldsFormData}
|
||||||
|
invoiceTypes={data.invoiceTypes || []}
|
||||||
|
pedimentos={data.pedimentos || []}
|
||||||
|
defaultOperationType={selectedOperationType}
|
||||||
|
defaultInvoiceType={selectedInvoiceType}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs for detailed sections -->
|
||||||
|
<Tabs.Root bind:value={activeTab}>
|
||||||
|
<Tabs.List class="grid w-full grid-cols-5 bg-muted rounded-md p-1 mb-4">
|
||||||
|
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="observations">Observaciones</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="items">Partidas</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="others">Otros</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="continuation">Continuación</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
|
<Tabs.Content value="general">
|
||||||
|
<GeneralTabForm
|
||||||
|
{invoice}
|
||||||
|
bind:formData={generalFormData}
|
||||||
|
invoiceTypes={data.invoiceTypes || []}
|
||||||
|
customsBrokers={data.customsBrokers || []}
|
||||||
|
clients={data.clients || []}
|
||||||
|
providers={data.providers || []}
|
||||||
|
currencyTypes={data.currencyTypes || []}
|
||||||
|
transportTypes={data.transportTypes || []}
|
||||||
|
transporters={data.transporters || []}
|
||||||
|
vehicles={data.vehicles || []}
|
||||||
|
drivers={data.drivers || []}
|
||||||
|
trailers={data.trailers || []}
|
||||||
|
customsSections={data.customsSections || []}
|
||||||
|
codePedimentoRegimens={data.codePedimentoRegimens || []}
|
||||||
|
operationType={selectedOperationType === 'exp' ? 1 : 2}
|
||||||
|
exchangeRate={0}
|
||||||
|
/>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="observations">
|
||||||
|
<ObservationsTabForm
|
||||||
|
{invoice}
|
||||||
|
bind:formData={observationFormData}
|
||||||
|
bind:exists={observationExists}
|
||||||
|
seals={[]}
|
||||||
|
incoterms={data.incoterms || []}
|
||||||
|
enclosure={[]}
|
||||||
|
/>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="items">
|
||||||
|
<div class="p-4 border rounded bg-muted/20 text-center">
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
La configuración predeterminada de partidas es limitada.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="others">
|
||||||
|
<OthersTabForm
|
||||||
|
{invoice}
|
||||||
|
bind:formData={othersFormData}
|
||||||
|
bind:exists={othersExists}
|
||||||
|
transportModes={data.transportModes || []}
|
||||||
|
/>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="continuation">
|
||||||
|
<ContinuationTabForm
|
||||||
|
{invoice}
|
||||||
|
bind:formData={continuationFormData}
|
||||||
|
bind:exists={continuationExists}
|
||||||
|
/>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-12 text-muted-foreground"
|
||||||
|
>
|
||||||
|
<FileText class="h-10 w-10 mb-2 opacity-20" />
|
||||||
|
<p>Selecciona un tipo de operación y factura para comenzar.</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sticky Footer -->
|
||||||
|
<div
|
||||||
|
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur border-t shadow-lg z-10 p-4 transition-all duration-300 group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
|
||||||
|
>
|
||||||
|
<div class="container mx-auto max-w-[1400px] flex justify-end gap-3">
|
||||||
|
<Button variant="outline" onclick={resetForms} disabled={isSaving || !selectedInvoiceType}>
|
||||||
|
Restablecer
|
||||||
|
</Button>
|
||||||
|
<Button onclick={handleSaveSettings} disabled={isSaving || !selectedInvoiceType}>
|
||||||
|
{#if isSaving}
|
||||||
|
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Guardando...
|
||||||
|
{:else}
|
||||||
|
<Save class="mr-2 h-4 w-4" />
|
||||||
|
Guardar Configuración
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -11,6 +11,13 @@ export default defineConfig({
|
|||||||
'anexo76-dev.aduanasoft.com',
|
'anexo76-dev.aduanasoft.com',
|
||||||
// 'otro-host.com' si necesitas más
|
// 'otro-host.com' si necesitas más
|
||||||
],
|
],
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://backend:8000',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
|
|||||||
Reference in New Issue
Block a user