Merge pull request 'feature/production' (#33) from feature/production into development
Reviewed-on: ADUANASOFT/anexo76#33
This commit is contained in:
15
README.md
15
README.md
@@ -132,6 +132,21 @@ python -c "from core.database import init_db; init_db()"
|
|||||||
- **Frontend**: http://localhost:5173
|
- **Frontend**: http://localhost:5173
|
||||||
- **Keycloak Admin**: http://localhost:8080
|
- **Keycloak Admin**: http://localhost:8080
|
||||||
|
|
||||||
|
## 🛠️ Produccion
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t dev.aduanasoft.com/anexo76/backend:latest -f ./backend/Dockerfile ./backend
|
||||||
|
docker build -t dev.aduanasoft.com/anexo76/frontend:latest -f ./frontend/Dockerfile.prod ./frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
Publica en el registro (ajusta el registry si corresponde):
|
||||||
|
|
||||||
|
```
|
||||||
|
docker login dev.aduanasoft.com
|
||||||
|
docker push dev.aduanasoft.com/anexo76/backend:latest
|
||||||
|
docker push dev.aduanasoft.com/anexo76/frontend:latest
|
||||||
|
```
|
||||||
|
|
||||||
## 🛠️ Desarrollo Local
|
## 🛠️ Desarrollo Local
|
||||||
|
|
||||||
### Backend
|
### Backend
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ CORE_DB_USER=postgres
|
|||||||
CORE_DB_PASSWORD=postgres
|
CORE_DB_PASSWORD=postgres
|
||||||
|
|
||||||
# Keycloak
|
# Keycloak
|
||||||
KEYCLOAK_SERVER_URL=http://localhost:8080
|
KEYCLOAK_SERVER_URL=http://localhost:8080/kcauth
|
||||||
KEYCLOAK_REALM=master
|
KEYCLOAK_REALM=master
|
||||||
KEYCLOAK_CLIENT_ID=anexo76-backend
|
KEYCLOAK_CLIENT_ID=anexo76-backend
|
||||||
KEYCLOAK_CLIENT_SECRET=your-client-secret
|
KEYCLOAK_CLIENT_SECRET=your-client-secret
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class AuthService:
|
|||||||
def __init__(self, db: Session):
|
def __init__(self, db: Session):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.keycloak_openid = KeycloakOpenID(
|
self.keycloak_openid = KeycloakOpenID(
|
||||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
|
||||||
client_id=settings.KEYCLOAK_CLIENT_ID,
|
client_id=settings.KEYCLOAK_CLIENT_ID,
|
||||||
realm_name=settings.KEYCLOAK_REALM,
|
realm_name=settings.KEYCLOAK_REALM,
|
||||||
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
|
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
|
||||||
@@ -64,7 +64,7 @@ class AuthService:
|
|||||||
|
|
||||||
# Crear nueva instancia de KeycloakOpenID con el realm del tenant
|
# Crear nueva instancia de KeycloakOpenID con el realm del tenant
|
||||||
keycloak_client = KeycloakOpenID(
|
keycloak_client = KeycloakOpenID(
|
||||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
|
||||||
client_id=settings.KEYCLOAK_CLIENT_ID,
|
client_id=settings.KEYCLOAK_CLIENT_ID,
|
||||||
realm_name=tenant.keycloak_realm,
|
realm_name=tenant.keycloak_realm,
|
||||||
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
|
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
|
||||||
@@ -78,7 +78,7 @@ class AuthService:
|
|||||||
# o buscar el usuario por username
|
# o buscar el usuario por username
|
||||||
try:
|
try:
|
||||||
keycloak_admin = KeycloakAdmin(
|
keycloak_admin = KeycloakAdmin(
|
||||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
|
||||||
username=settings.KEYCLOAK_ADMIN_USERNAME,
|
username=settings.KEYCLOAK_ADMIN_USERNAME,
|
||||||
password=settings.KEYCLOAK_ADMIN_PASSWORD,
|
password=settings.KEYCLOAK_ADMIN_PASSWORD,
|
||||||
realm_name=tenant.keycloak_realm,
|
realm_name=tenant.keycloak_realm,
|
||||||
@@ -279,7 +279,7 @@ class AuthService:
|
|||||||
|
|
||||||
# Crear instancia de KeycloakAdmin para gestión de usuarios
|
# Crear instancia de KeycloakAdmin para gestión de usuarios
|
||||||
keycloak_admin = KeycloakAdmin(
|
keycloak_admin = KeycloakAdmin(
|
||||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
|
||||||
username=settings.KEYCLOAK_ADMIN_USERNAME,
|
username=settings.KEYCLOAK_ADMIN_USERNAME,
|
||||||
password=settings.KEYCLOAK_ADMIN_PASSWORD,
|
password=settings.KEYCLOAK_ADMIN_PASSWORD,
|
||||||
realm_name=tenant.keycloak_realm,
|
realm_name=tenant.keycloak_realm,
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
from sqlalchemy import create_engine, text, inspect
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
# Add the backend directory to the python path
|
|
||||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
|
||||||
|
|
||||||
from core.config import settings
|
|
||||||
|
|
||||||
def check_equivalencies_columns():
|
|
||||||
engine = create_engine(str(settings.core_database_url))
|
|
||||||
inspector = inspect(engine)
|
|
||||||
|
|
||||||
try:
|
|
||||||
print("Checking equivalencies table columns...")
|
|
||||||
columns = inspector.get_columns('equivalencies', schema='a76')
|
|
||||||
for column in columns:
|
|
||||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
|
||||||
|
|
||||||
print("\nChecking equivalency_items table columns...")
|
|
||||||
columns = inspector.get_columns('equivalency_items', schema='a76')
|
|
||||||
for column in columns:
|
|
||||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
check_equivalencies_columns()
|
|
||||||
@@ -24,7 +24,7 @@ class Settings(BaseSettings):
|
|||||||
CORE_DB_PASSWORD: str = "postgres"
|
CORE_DB_PASSWORD: str = "postgres"
|
||||||
|
|
||||||
# Keycloak
|
# Keycloak
|
||||||
KEYCLOAK_SERVER_URL: str = "http://localhost:8080"
|
KEYCLOAK_SERVER_URL: str = "http://localhost:8080/kcauth"
|
||||||
KEYCLOAK_REALM: str = "master"
|
KEYCLOAK_REALM: str = "master"
|
||||||
KEYCLOAK_CLIENT_ID: str = "anexo76-backend"
|
KEYCLOAK_CLIENT_ID: str = "anexo76-backend"
|
||||||
KEYCLOAK_CLIENT_SECRET: str = ""
|
KEYCLOAK_CLIENT_SECRET: str = ""
|
||||||
@@ -43,7 +43,7 @@ class Settings(BaseSettings):
|
|||||||
LICENSE_CHECK_ENABLED: bool = True
|
LICENSE_CHECK_ENABLED: bool = True
|
||||||
|
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=".env", case_sensitive=True, extra="ignore"
|
env_file=".env", case_sensitive=True, extra="ignore", env_file_encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
|
|||||||
db = CoreSessionLocal()
|
db = CoreSessionLocal()
|
||||||
try:
|
try:
|
||||||
# Importar aquí para evitar imports circulares
|
# Importar aquí para evitar imports circulares
|
||||||
from api.v1.modules.a76.licenses.service import LicenseService
|
from api.v1.modules.core.licenses.service import LicenseService
|
||||||
|
|
||||||
license_service = LicenseService(db)
|
license_service = LicenseService(db)
|
||||||
license_info = license_service.validate_license(tenant_id)
|
license_info = license_service.validate_license(tenant_id)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# Configuración de Keycloak
|
# Configuración de Keycloak
|
||||||
keycloak_openid = KeycloakOpenID(
|
keycloak_openid = KeycloakOpenID(
|
||||||
server_url=settings.KEYCLOAK_SERVER_URL,
|
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
|
||||||
client_id=settings.KEYCLOAK_CLIENT_ID,
|
client_id=settings.KEYCLOAK_CLIENT_ID,
|
||||||
realm_name=settings.KEYCLOAK_REALM,
|
realm_name=settings.KEYCLOAK_REALM,
|
||||||
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
|
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
|
||||||
|
|||||||
30
check_db.py
30
check_db.py
@@ -1,30 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
from sqlalchemy import create_engine, text, inspect
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
# Add the backend directory to the python path
|
|
||||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
|
||||||
|
|
||||||
from core.config import settings
|
|
||||||
|
|
||||||
def check_equivalencies_columns():
|
|
||||||
engine = create_engine(str(settings.core_database_url))
|
|
||||||
inspector = inspect(engine)
|
|
||||||
|
|
||||||
try:
|
|
||||||
print("Checking equivalencies table columns...")
|
|
||||||
columns = inspector.get_columns('equivalencies', schema='a76')
|
|
||||||
for column in columns:
|
|
||||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
|
||||||
|
|
||||||
print("\nChecking equivalency_items table columns...")
|
|
||||||
columns = inspector.get_columns('equivalency_items', schema='a76')
|
|
||||||
for column in columns:
|
|
||||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
check_equivalencies_columns()
|
|
||||||
23
check_uom.py
23
check_uom.py
@@ -1,23 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
# Add backend to path
|
|
||||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
|
||||||
|
|
||||||
from core.database import CoreSessionLocal
|
|
||||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
|
||||||
|
|
||||||
def check_units():
|
|
||||||
db = CoreSessionLocal()
|
|
||||||
try:
|
|
||||||
units = db.query(UnitOfMeasure).limit(10).all()
|
|
||||||
print(f"Found {len(units)} units:")
|
|
||||||
for u in units:
|
|
||||||
print(f"Code: {u.code}, Description: {u.description}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
check_units()
|
|
||||||
266
docker-compose.prod.yml
Normal file
266
docker-compose.prod.yml
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
services:
|
||||||
|
# PostgreSQL - Base de datos core (app)
|
||||||
|
postgres-a76:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
container_name: anexo76-postgres-a76
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: anexo76_core
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_APP_PASSWORD:-postgres}
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
|
||||||
|
ports:
|
||||||
|
- "5939:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_app_data:/var/lib/postgresql/data
|
||||||
|
- ./scripts/postgres-app-entrypoint.sh:/docker-entrypoint-initdb.d/init-app.sh:ro
|
||||||
|
networks:
|
||||||
|
- backend-net
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d anexo76_core || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
start_period: 20s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
shm_size: 128mb
|
||||||
|
|
||||||
|
# PostgreSQL - Base de datos Keycloak
|
||||||
|
postgres-keycloak:
|
||||||
|
image: postgres:18-alpine
|
||||||
|
container_name: anexo76-postgres-keycloak
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: keycloak
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres}
|
||||||
|
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
|
||||||
|
ports:
|
||||||
|
- "5233:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_keycloak_data:/var/lib/postgresql/data
|
||||||
|
- ./scripts/postgres-keycloak-entrypoint.sh:/docker-entrypoint-initdb.d/init-keycloak.sh:ro
|
||||||
|
networks:
|
||||||
|
- auth-net
|
||||||
|
- backend-net
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d keycloak || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
start_period: 20s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
shm_size: 128mb
|
||||||
|
|
||||||
|
# Keycloak - Servidor de autenticación
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:26.4
|
||||||
|
container_name: anexo76-keycloak
|
||||||
|
environment:
|
||||||
|
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
|
||||||
|
KC_DB: postgres
|
||||||
|
KC_DB_URL_HOST: postgres-keycloak
|
||||||
|
KC_DB_URL_PORT: "5432"
|
||||||
|
KC_DB_URL_DATABASE: keycloak
|
||||||
|
KC_DB_URL: jdbc:postgresql://postgres-keycloak:5432/keycloak
|
||||||
|
KC_DB_USERNAME: postgres
|
||||||
|
KC_DB_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres}
|
||||||
|
KC_DB_SCHEMA: public
|
||||||
|
KC_HOSTNAME: localhost
|
||||||
|
KC_HTTP_ENABLED: "true"
|
||||||
|
KC_HOSTNAME_STRICT: "false"
|
||||||
|
KC_HOSTNAME_STRICT_HTTPS: "false"
|
||||||
|
KC_PROXY_HEADERS: "xforwarded"
|
||||||
|
KC_HEALTH_ENABLED: "true"
|
||||||
|
KC_METRICS_ENABLED: "true"
|
||||||
|
KC_HOSTNAME_PATH: /kcauth
|
||||||
|
KC_LOG_LEVEL: INFO
|
||||||
|
JAVA_OPTS_APPEND: "-Xms256m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true"
|
||||||
|
command:
|
||||||
|
- start-dev
|
||||||
|
- --db=postgres
|
||||||
|
- --db-url-host=postgres-keycloak
|
||||||
|
- --http-relative-path=/kcauth
|
||||||
|
- --db-url-port=5432
|
||||||
|
- --db-url-database=keycloak
|
||||||
|
- --db-username=postgres
|
||||||
|
- --db-password=${POSTGRES_KEYCLOAK_PASSWORD:-postgres}
|
||||||
|
- --http-enabled=true
|
||||||
|
- --hostname-strict=false
|
||||||
|
- --proxy-headers=xforwarded
|
||||||
|
ports:
|
||||||
|
- "8880:8080"
|
||||||
|
- "9000:9000"
|
||||||
|
depends_on:
|
||||||
|
postgres-keycloak:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- keycloak_data:/opt/keycloak/data
|
||||||
|
networks:
|
||||||
|
- auth-net
|
||||||
|
- backend-net
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /kcauth/health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 30
|
||||||
|
start_period: 30s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 768M
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
# Backend - FastAPI
|
||||||
|
backend:
|
||||||
|
image: dev.aduanasoft.com/anexo76/backend:latest
|
||||||
|
container_name: anexo76-backend
|
||||||
|
environment:
|
||||||
|
- DEBUG=${DEBUG:-True}
|
||||||
|
- ENVIRONMENT=${ENVIRONMENT:-development}
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
|
- PYTHONDONTWRITEBYTECODE=1
|
||||||
|
- CORE_DB_HOST=${CORE_DB_HOST:-postgres-a76}
|
||||||
|
- CORE_DB_PORT=${CORE_DB_PORT:-5432}
|
||||||
|
- CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core}
|
||||||
|
- CORE_DB_USER=${CORE_DB_USER:-postgres}
|
||||||
|
- CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres}
|
||||||
|
- KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080/kcauth}
|
||||||
|
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
|
||||||
|
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
||||||
|
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret}
|
||||||
|
- CORS_ORIGINS=${CORS_ORIGINS:-https://anexo76-dev.aduanasoft.com,http://localhost:3000}
|
||||||
|
ports:
|
||||||
|
- "3467:8000"
|
||||||
|
depends_on:
|
||||||
|
postgres-a76:
|
||||||
|
condition: service_healthy
|
||||||
|
keycloak:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
- backend_cache:/app/__pycache__
|
||||||
|
- ./scripts/backend-entrypoint.sh:/entrypoint.sh:ro
|
||||||
|
networks:
|
||||||
|
- backend-net
|
||||||
|
- frontend-net
|
||||||
|
restart: unless-stopped
|
||||||
|
entrypoint: ["/entrypoint.sh"]
|
||||||
|
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload", "--log-level", "info"]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "curl -f http://localhost:8000/api/health || exit 1"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# Frontend - SvelteKit
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
|
args:
|
||||||
|
- BUILDKIT_INLINE_CACHE=1
|
||||||
|
- VITE_API_URL=https://anexo76-dev.aduanasoft.com/api/
|
||||||
|
- VITE_KEYCLOAK_URL=https://anexo76-dev.aduanasoft.com/kcauth/
|
||||||
|
container_name: anexo76-frontend
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=${NODE_ENV:-development}
|
||||||
|
- VITE_API_URL=${VITE_API_URL:-https://anexo76-dev.aduanasoft.com/api}
|
||||||
|
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/}
|
||||||
|
- VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-https://anexo76-dev.aduanasoft.com/kcauth/}
|
||||||
|
- VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master}
|
||||||
|
- VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend}
|
||||||
|
- KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080/kcauth}
|
||||||
|
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
|
||||||
|
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
||||||
|
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9}
|
||||||
|
ports:
|
||||||
|
- "5111:5173"
|
||||||
|
depends_on:
|
||||||
|
backend:
|
||||||
|
condition: service_healthy
|
||||||
|
entrypoint: ["/frontend-entrypoint.sh"]
|
||||||
|
volumes:
|
||||||
|
- ./scripts/frontend-entrypoint.sh:/frontend-entrypoint.sh:ro
|
||||||
|
networks:
|
||||||
|
- frontend-net
|
||||||
|
- auth-net
|
||||||
|
restart: unless-stopped
|
||||||
|
command: ["node", "build"]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5173/ || exit 1"]
|
||||||
|
interval: 15s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 45s
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
memory: 1G
|
||||||
|
reservations:
|
||||||
|
memory: 512M
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_app_data:
|
||||||
|
driver: local
|
||||||
|
postgres_keycloak_data:
|
||||||
|
driver: local
|
||||||
|
keycloak_data:
|
||||||
|
driver: local
|
||||||
|
frontend_node_modules:
|
||||||
|
driver: local
|
||||||
|
backend_cache:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend-net:
|
||||||
|
driver: bridge
|
||||||
|
auth-net:
|
||||||
|
driver: bridge
|
||||||
|
frontend-net:
|
||||||
|
driver: bridge
|
||||||
@@ -94,10 +94,12 @@ services:
|
|||||||
KC_PROXY_HEADERS: "xforwarded"
|
KC_PROXY_HEADERS: "xforwarded"
|
||||||
KC_HEALTH_ENABLED: "true"
|
KC_HEALTH_ENABLED: "true"
|
||||||
KC_METRICS_ENABLED: "true"
|
KC_METRICS_ENABLED: "true"
|
||||||
|
KC_HOSTNAME_PATH: /kcauth
|
||||||
KC_LOG_LEVEL: INFO
|
KC_LOG_LEVEL: INFO
|
||||||
JAVA_OPTS_APPEND: "-Xms256m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true"
|
JAVA_OPTS_APPEND: "-Xms256m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true"
|
||||||
command:
|
command:
|
||||||
- start-dev
|
- start-dev
|
||||||
|
- --http-relative-path=/kcauth
|
||||||
- --db=postgres
|
- --db=postgres
|
||||||
- --db-url-host=postgres-keycloak
|
- --db-url-host=postgres-keycloak
|
||||||
- --db-url-port=5432
|
- --db-url-port=5432
|
||||||
@@ -120,7 +122,7 @@ services:
|
|||||||
- backend-net
|
- backend-net
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"]
|
test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /kcauth/health/ready HTTP/1.1\r\nhost: 127.0.0.1\r\nConnection: close\r\n\r\n' >&3; grep -q 'HTTP/1.1 200' <&3 || exit 1"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 30
|
retries: 30
|
||||||
@@ -156,13 +158,13 @@ services:
|
|||||||
- CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core}
|
- CORE_DB_NAME=${CORE_DB_NAME:-anexo76_core}
|
||||||
- CORE_DB_USER=${CORE_DB_USER:-postgres}
|
- CORE_DB_USER=${CORE_DB_USER:-postgres}
|
||||||
- CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres}
|
- CORE_DB_PASSWORD=${POSTGRES_APP_PASSWORD:-postgres}
|
||||||
- KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080}
|
- KEYCLOAK_SERVER_URL=${KEYCLOAK_SERVER_URL:-http://keycloak:8080/kcauth}
|
||||||
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
|
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
|
||||||
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
||||||
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret}
|
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret}
|
||||||
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000}
|
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000}
|
||||||
ports:
|
ports:
|
||||||
- "5050:8000"
|
- "8000:8000"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres-a76:
|
postgres-a76:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -209,10 +211,10 @@ services:
|
|||||||
- NODE_ENV=${NODE_ENV:-development}
|
- NODE_ENV=${NODE_ENV:-development}
|
||||||
- VITE_API_URL=${VITE_API_URL:-http://localhost:8000/api/}
|
- VITE_API_URL=${VITE_API_URL:-http://localhost:8000/api/}
|
||||||
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/}
|
- INTERNAL_API_URL=${INTERNAL_API_URL:-http://backend:8000/api/}
|
||||||
- VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-http://localhost:8080}
|
- VITE_KEYCLOAK_URL=${VITE_KEYCLOAK_URL:-http://localhost:8080/kcauth}
|
||||||
- VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master}
|
- VITE_KEYCLOAK_REALM=${VITE_KEYCLOAK_REALM:-master}
|
||||||
- VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend}
|
- VITE_KEYCLOAK_CLIENT_ID=${VITE_KEYCLOAK_CLIENT_ID:-anexo76-frontend}
|
||||||
- KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080}
|
- KEYCLOAK_URL=${KEYCLOAK_URL:-http://keycloak:8080/kcauth}
|
||||||
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
|
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
|
||||||
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
|
||||||
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9}
|
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9}
|
||||||
|
|||||||
59
frontend/Dockerfile.prod
Normal file
59
frontend/Dockerfile.prod
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# ==========================
|
||||||
|
# Etapa de build
|
||||||
|
# ==========================
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
# Directorio de trabajo
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Configurar npm para trabajar con certificados autofirmados e instalar pnpm
|
||||||
|
RUN npm config set strict-ssl false && \
|
||||||
|
npm install -g pnpm
|
||||||
|
|
||||||
|
# Copiar archivos de dependencias
|
||||||
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
|
||||||
|
# Instalar dependencias con pnpm
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
ARG VITE_API_URL
|
||||||
|
ENV VITE_API_URL=${VITE_API_URL}
|
||||||
|
|
||||||
|
# Copiar el resto del código
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Construir el proyecto
|
||||||
|
RUN pnpm run build
|
||||||
|
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# Etapa de ejecución con Node.js
|
||||||
|
# ==========================
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN npm config set strict-ssl false && \
|
||||||
|
npm install -g pnpm
|
||||||
|
|
||||||
|
# Crear usuario no-root para seguridad antes de copiar con --chown
|
||||||
|
RUN addgroup -g 1001 -S nodejs
|
||||||
|
RUN adduser -S svelte -u 1001
|
||||||
|
|
||||||
|
# Copiar solo archivos necesarios para producción y aplicar propietario en la copia
|
||||||
|
COPY --from=build --chown=svelte:nodejs /app/build ./build
|
||||||
|
COPY --from=build --chown=svelte:nodejs /app/package.json ./
|
||||||
|
COPY --from=build --chown=svelte:nodejs /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
USER svelte
|
||||||
|
|
||||||
|
# Puerto para SvelteKit con adapter-node
|
||||||
|
EXPOSE 5173
|
||||||
|
|
||||||
|
# Variables de entorno
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV PORT=5173
|
||||||
|
ENV HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Ejecutar aplicación con Node.js
|
||||||
|
CMD ["node", "build"]
|
||||||
@@ -56,6 +56,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"keycloak-js": "^26.2.1",
|
"keycloak-js": "^26.2.1",
|
||||||
"lucide-svelte": "^0.553.0"
|
"lucide-svelte": "^0.553.0",
|
||||||
|
"svelte-sonner": "^1.0.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
frontend/pnpm-lock.yaml
generated
23
frontend/pnpm-lock.yaml
generated
@@ -14,6 +14,9 @@ importers:
|
|||||||
lucide-svelte:
|
lucide-svelte:
|
||||||
specifier: ^0.553.0
|
specifier: ^0.553.0
|
||||||
version: 0.553.0(svelte@5.40.2)
|
version: 0.553.0(svelte@5.40.2)
|
||||||
|
svelte-sonner:
|
||||||
|
specifier: ^1.0.7
|
||||||
|
version: 1.0.7(svelte@5.40.2)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/compat':
|
'@eslint/compat':
|
||||||
specifier: ^1.4.0
|
specifier: ^1.4.0
|
||||||
@@ -1675,6 +1678,11 @@ packages:
|
|||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
|
runed@0.28.0:
|
||||||
|
resolution: {integrity: sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.7.0
|
||||||
|
|
||||||
runed@0.35.1:
|
runed@0.35.1:
|
||||||
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
|
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1761,6 +1769,11 @@ packages:
|
|||||||
svelte:
|
svelte:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
svelte-sonner@1.0.7:
|
||||||
|
resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^5.0.0
|
||||||
|
|
||||||
svelte-toolbelt@0.10.6:
|
svelte-toolbelt@0.10.6:
|
||||||
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
|
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
|
||||||
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
engines: {node: '>=18', pnpm: '>=8.7.0'}
|
||||||
@@ -3379,6 +3392,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
|
|
||||||
|
runed@0.28.0(svelte@5.40.2):
|
||||||
|
dependencies:
|
||||||
|
esm-env: 1.2.2
|
||||||
|
svelte: 5.40.2
|
||||||
|
|
||||||
runed@0.35.1(@sveltejs/kit@2.47.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2):
|
runed@0.35.1(@sveltejs/kit@2.47.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
dequal: 2.0.3
|
dequal: 2.0.3
|
||||||
@@ -3460,6 +3478,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
svelte: 5.40.2
|
svelte: 5.40.2
|
||||||
|
|
||||||
|
svelte-sonner@1.0.7(svelte@5.40.2):
|
||||||
|
dependencies:
|
||||||
|
runed: 0.28.0(svelte@5.40.2)
|
||||||
|
svelte: 5.40.2
|
||||||
|
|
||||||
svelte-toolbelt@0.10.6(@sveltejs/kit@2.47.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2):
|
svelte-toolbelt@0.10.6(@sveltejs/kit@2.47.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2)(vite@7.1.10(@types/node@20.19.22)(jiti@2.6.1)(lightningcss@1.30.1)))(svelte@5.40.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
import { getToken } from './auth';
|
import { getToken } from './auth';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_URL;
|
// Normalize API_BASE_URL to remove trailing slash
|
||||||
|
const API_BASE_URL = (import.meta.env.VITE_API_URL || '').replace(/\/+$/, '');
|
||||||
|
|
||||||
export interface ApiResponse<T = any> {
|
export interface ApiResponse<T = any> {
|
||||||
data?: T;
|
data?: T;
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ export const invoicesApi = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.get<InvoiceListResponse>(`/v1/a76/invoices?${params.toString()}`);
|
return api.get<InvoiceListResponse>(`/v1/a76/invoices/?${params.toString()}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -345,7 +345,7 @@ export const invoicesApi = {
|
|||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.post<Invoice>(`/v1/a76/invoices?${params.toString()}`, data);
|
return api.post<Invoice>(`/v1/a76/invoices/?${params.toString()}`, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -378,21 +378,21 @@ export const invoicesApi = {
|
|||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.get<InvoiceLogistics[]>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`);
|
return api.get<InvoiceLogistics[]>(`/v1/a76/invoices/${invoiceId}/logistics/?${params.toString()}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceLogistics, 'id' | 'invoice_id'>) => {
|
create: (invoiceId: number, companyId: number, data: Omit<InvoiceLogistics, 'id' | 'invoice_id'>) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.post<InvoiceLogistics>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`, data);
|
return api.post<InvoiceLogistics>(`/v1/a76/invoices/${invoiceId}/logistics/?${params.toString()}`, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: (invoiceId: number, logisticsId: number, companyId: number) => {
|
delete: (invoiceId: number, logisticsId: number, companyId: number) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.delete(`/v1/a76/invoices/${invoiceId}/logistics/${logisticsId}?${params.toString()}`);
|
return api.delete(`/v1/a76/invoices/${invoiceId}/logistics/${logisticsId}/?${params.toString()}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -404,21 +404,21 @@ export const invoicesApi = {
|
|||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.get<InvoiceSalesDetails[]>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`);
|
return api.get<InvoiceSalesDetails[]>(`/v1/a76/invoices/${invoiceId}/details/?${params.toString()}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>) => {
|
create: (invoiceId: number, companyId: number, data: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.post<InvoiceSalesDetails>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`, data);
|
return api.post<InvoiceSalesDetails>(`/v1/a76/invoices/${invoiceId}/details/?${params.toString()}`, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: (invoiceId: number, detailId: number, companyId: number) => {
|
delete: (invoiceId: number, detailId: number, companyId: number) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.delete(`/v1/a76/invoices/${invoiceId}/details/${detailId}?${params.toString()}`);
|
return api.delete(`/v1/a76/invoices/${invoiceId}/details/${detailId}/?${params.toString()}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -430,21 +430,21 @@ export const invoicesApi = {
|
|||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.get<InvoiceCollections[]>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`);
|
return api.get<InvoiceCollections[]>(`/v1/a76/invoices/${invoiceId}/collections/?${params.toString()}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceCollections, 'id' | 'invoice_id'>) => {
|
create: (invoiceId: number, companyId: number, data: Omit<InvoiceCollections, 'id' | 'invoice_id'>) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.post<InvoiceCollections>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`, data);
|
return api.post<InvoiceCollections>(`/v1/a76/invoices/${invoiceId}/collections/?${params.toString()}`, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: (invoiceId: number, collectionId: number, companyId: number) => {
|
delete: (invoiceId: number, collectionId: number, companyId: number) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
company_id: companyId.toString()
|
company_id: companyId.toString()
|
||||||
});
|
});
|
||||||
return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}?${params.toString()}`);
|
return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}/?${params.toString()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -134,48 +134,63 @@ export async function authenticatedFetch(
|
|||||||
fetch: typeof globalThis.fetch,
|
fetch: typeof globalThis.fetch,
|
||||||
redirectUrl?: string
|
redirectUrl?: string
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
const baseUrl = getServerApiUrl();
|
try {
|
||||||
let { accessToken } = getAuthTokens(cookies);
|
const baseUrl = getServerApiUrl();
|
||||||
|
let { accessToken } = getAuthTokens(cookies);
|
||||||
|
|
||||||
// Si no hay token, redirigir o lanzar error
|
// Si no hay token, redirigir o lanzar error
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
if (redirectUrl) {
|
|
||||||
throw redirect(303, redirectUrl);
|
|
||||||
}
|
|
||||||
throw new Error('No access token available');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construir URL completa
|
|
||||||
const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`;
|
|
||||||
|
|
||||||
// Realizar la petición inicial
|
|
||||||
const headers = createAuthHeaders(accessToken, options.headers as Record<string, string>);
|
|
||||||
let response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
|
|
||||||
// Si es 401, intentar refrescar el token
|
|
||||||
if (response.status === 401) {
|
|
||||||
const newToken = await refreshAccessToken(cookies, fetch);
|
|
||||||
|
|
||||||
if (newToken) {
|
|
||||||
// Reintentar la petición con el nuevo token
|
|
||||||
const newHeaders = createAuthHeaders(newToken, options.headers as Record<string, string>);
|
|
||||||
response = await fetch(url, {
|
|
||||||
...options,
|
|
||||||
headers: newHeaders
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// No se pudo refrescar, limpiar y redirigir
|
|
||||||
clearAuthTokens(cookies);
|
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
throw redirect(303, redirectUrl);
|
throw redirect(303, redirectUrl);
|
||||||
}
|
}
|
||||||
|
throw new Error('No access token available');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
// Construir URL completa
|
||||||
|
const url = endpoint.startsWith('http') ? endpoint : `${baseUrl}${endpoint}`;
|
||||||
|
|
||||||
|
// Realizar la petición inicial
|
||||||
|
const headers = createAuthHeaders(accessToken, options.headers as Record<string, string>);
|
||||||
|
let response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si es 401, intentar refrescar el token
|
||||||
|
if (response.status === 401) {
|
||||||
|
const newToken = await refreshAccessToken(cookies, fetch);
|
||||||
|
|
||||||
|
if (newToken) {
|
||||||
|
// Reintentar la petición con el nuevo token
|
||||||
|
const newHeaders = createAuthHeaders(newToken, options.headers as Record<string, string>);
|
||||||
|
response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: newHeaders
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No se pudo refrescar, limpiar y redirigir
|
||||||
|
clearAuthTokens(cookies);
|
||||||
|
if (redirectUrl) {
|
||||||
|
throw redirect(303, redirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
// Si es un redirect, re-lanzarlo
|
||||||
|
if (error && typeof error === 'object' && 'status' in error && 'location' in error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('🔴 [API] Error en authenticatedFetch:', endpoint, error);
|
||||||
|
|
||||||
|
// Retornar una respuesta de error simulada en lugar de lanzar
|
||||||
|
return new Response(JSON.stringify({ error: 'Network error', details: String(error) }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { json, error } from '@sveltejs/kit';
|
||||||
|
import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ params, cookies, fetch }) => {
|
||||||
|
const { accessToken } = getAuthTokens(cookies);
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
throw error(401, 'Not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyId = await getActiveCompanyId(cookies, fetch);
|
||||||
|
|
||||||
|
if (!companyId) {
|
||||||
|
throw error(400, 'No company selected');
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoiceId = parseInt(params.id);
|
||||||
|
if (isNaN(invoiceId)) {
|
||||||
|
throw error(400, 'Invalid invoice ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cargar la factura y datos de referencia en paralelo
|
||||||
|
const [
|
||||||
|
invoiceResponse,
|
||||||
|
invoiceTypesResponse,
|
||||||
|
customsBrokersResponse,
|
||||||
|
clientsResponse,
|
||||||
|
providersResponse,
|
||||||
|
currencyTypesResponse,
|
||||||
|
transportTypesResponse,
|
||||||
|
sealsResponse,
|
||||||
|
incotermsResponse,
|
||||||
|
pedimentosResponse
|
||||||
|
] = await Promise.all([
|
||||||
|
authenticatedFetch(`v1/a76/invoices/${invoiceId}?company_id=${companyId}`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch('v1/public/refrence_data/invoice-types/?page=1&page_size=100', {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch),
|
||||||
|
authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch)
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!invoiceResponse.ok) {
|
||||||
|
throw error(invoiceResponse.status, 'Error loading invoice');
|
||||||
|
}
|
||||||
|
|
||||||
|
const invoice = await invoiceResponse.json();
|
||||||
|
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 seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
|
||||||
|
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
||||||
|
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
||||||
|
|
||||||
|
return json({
|
||||||
|
invoice,
|
||||||
|
invoiceTypes: invoiceTypes.items || [],
|
||||||
|
customsBrokers: customsBrokers.items || [],
|
||||||
|
clients: clients.items || [],
|
||||||
|
providers: providers.items || [],
|
||||||
|
currencyTypes: currencyTypes.items || [],
|
||||||
|
transportTypes: transportTypes.items || [],
|
||||||
|
seals: seals.items || [],
|
||||||
|
incoterms: incoterms.items || [],
|
||||||
|
pedimentos: pedimentos.items || []
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading invoice edit data:', err);
|
||||||
|
throw error(500, 'Error loading invoice');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import { getAuthTokens, getActiveCompanyId, authenticatedFetch } from '$lib/server/api';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async ({ cookies, fetch }) => {
|
||||||
|
const { accessToken } = getAuthTokens(cookies);
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
return json({ error: 'Not authenticated' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const companyId = await getActiveCompanyId(cookies, fetch);
|
||||||
|
|
||||||
|
if (!companyId) {
|
||||||
|
return json({ error: 'No company selected' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cargar todos los datos de referencia en paralelo
|
||||||
|
const [
|
||||||
|
invoiceTypesResponse,
|
||||||
|
customsBrokersResponse,
|
||||||
|
clientsResponse,
|
||||||
|
providersResponse,
|
||||||
|
currencyTypesResponse,
|
||||||
|
transportTypesResponse,
|
||||||
|
sealsResponse,
|
||||||
|
incotermsResponse,
|
||||||
|
pedimentosResponse
|
||||||
|
] = await Promise.all([
|
||||||
|
authenticatedFetch('v1/public/refrence_data/invoice-types/?page=1&page_size=100', {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch),
|
||||||
|
authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch),
|
||||||
|
authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch),
|
||||||
|
authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch)
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
|
||||||
|
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
||||||
|
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
||||||
|
|
||||||
|
return json({
|
||||||
|
invoiceTypes: invoiceTypes.items || [],
|
||||||
|
customsBrokers: customsBrokers.items || [],
|
||||||
|
clients: clients.items || [],
|
||||||
|
providers: providers.items || [],
|
||||||
|
currencyTypes: currencyTypes.items || [],
|
||||||
|
transportTypes: transportTypes.items || [],
|
||||||
|
seals: seals.items || [],
|
||||||
|
incoterms: incoterms.items || [],
|
||||||
|
pedimentos: pedimentos.items || []
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading reference data:', err);
|
||||||
|
return json({ error: 'Error loading reference data' }, { status: 500 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -31,14 +31,14 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
|
|
||||||
// Cargar datos de referencia necesarios
|
// Cargar datos de referencia necesarios
|
||||||
const invoiceTypesPromise = authenticatedFetch(
|
const invoiceTypesPromise = authenticatedFetch(
|
||||||
'v1/public/refrence_data/invoice-types?page=1&page_size=100',
|
'v1/public/refrence_data/invoice-types/?page=1&page_size=100',
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
);
|
);
|
||||||
|
|
||||||
const customsBrokersPromise = authenticatedFetch(
|
const customsBrokersPromise = authenticatedFetch(
|
||||||
`v1/a76/customs-brokers?company_id=${companyId}&page=1&page_size=1000`,
|
`v1/a76/customs-brokers/?company_id=${companyId}&page=1&page_size=100`,
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
@@ -46,14 +46,14 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
|
|
||||||
// Cargar clientes y proveedores
|
// Cargar clientes y proveedores
|
||||||
const clientsPromise = authenticatedFetch(
|
const clientsPromise = authenticatedFetch(
|
||||||
`v1/a76/clients-providers?company_id=${companyId}&type=client&page=1&page_size=1000`,
|
`v1/a76/clients-providers/?company_id=${companyId}&type=client&page=1&page_size=1000`,
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
);
|
);
|
||||||
|
|
||||||
const providersPromise = authenticatedFetch(
|
const providersPromise = authenticatedFetch(
|
||||||
`v1/a76/clients-providers?company_id=${companyId}&type=provider&page=1&page_size=1000`,
|
`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`,
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
@@ -61,42 +61,35 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
|
|
||||||
// Cargar tipos de moneda, transporte, etc.
|
// Cargar tipos de moneda, transporte, etc.
|
||||||
const currencyTypesPromise = authenticatedFetch(
|
const currencyTypesPromise = authenticatedFetch(
|
||||||
'v1/public/refrence_data/currency-types?page=1&page_size=100',
|
'v1/public/refrence_data/currency-types/?page=1&page_size=100',
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
);
|
);
|
||||||
|
|
||||||
const transportTypesPromise = authenticatedFetch(
|
const transportTypesPromise = authenticatedFetch(
|
||||||
'v1/public/refrence_data/transport-types?page=1&page_size=100',
|
'v1/public/refrence_data/transport-types/?page=1&page_size=100',
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
);
|
);
|
||||||
|
|
||||||
const sealsPromise = authenticatedFetch(
|
const sealsPromise = authenticatedFetch(
|
||||||
'v1/public/refrence_data/seals?page=1&page_size=100',
|
`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`,
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
);
|
);
|
||||||
|
|
||||||
const incotermsPromise = authenticatedFetch(
|
const incotermsPromise = authenticatedFetch(
|
||||||
'v1/public/refrence_data/incoterms?page=1&page_size=100',
|
'v1/public/refrence_data/incoterms/?page=1&page_size=100',
|
||||||
{},
|
|
||||||
cookies,
|
|
||||||
fetch
|
|
||||||
);
|
|
||||||
|
|
||||||
const enclosurePromise = authenticatedFetch(
|
|
||||||
'v1/public/refrence_data/enclosure?page=1&page_size=100',
|
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
);
|
);
|
||||||
|
|
||||||
const pedimentosPromise = authenticatedFetch(
|
const pedimentosPromise = authenticatedFetch(
|
||||||
`v1/a76/pedimentos?company_id=${companyId}&page=1&page_size=100`,
|
`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`,
|
||||||
{},
|
{},
|
||||||
cookies,
|
cookies,
|
||||||
fetch
|
fetch
|
||||||
@@ -104,61 +97,80 @@ 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') {
|
||||||
const [
|
try {
|
||||||
invoiceTypesResponse,
|
const [
|
||||||
customsBrokersResponse,
|
invoiceTypesResponse,
|
||||||
clientsResponse,
|
customsBrokersResponse,
|
||||||
providersResponse,
|
clientsResponse,
|
||||||
currencyTypesResponse,
|
providersResponse,
|
||||||
transportTypesResponse,
|
currencyTypesResponse,
|
||||||
sealsResponse,
|
transportTypesResponse,
|
||||||
incotermsResponse,
|
sealsResponse,
|
||||||
enclosureResponse,
|
incotermsResponse,
|
||||||
pedimentosResponse
|
pedimentosResponse
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
invoiceTypesPromise,
|
invoiceTypesPromise,
|
||||||
customsBrokersPromise,
|
customsBrokersPromise,
|
||||||
clientsPromise,
|
clientsPromise,
|
||||||
providersPromise,
|
providersPromise,
|
||||||
currencyTypesPromise,
|
currencyTypesPromise,
|
||||||
transportTypesPromise,
|
transportTypesPromise,
|
||||||
sealsPromise,
|
sealsPromise,
|
||||||
incotermsPromise,
|
incotermsPromise,
|
||||||
enclosurePromise,
|
pedimentosPromise
|
||||||
pedimentosPromise
|
]);
|
||||||
]);
|
|
||||||
|
|
||||||
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
|
const invoiceTypes = invoiceTypesResponse.ok ? await invoiceTypesResponse.json() : { items: [] };
|
||||||
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
|
const customsBrokers = customsBrokersResponse.ok ? await customsBrokersResponse.json() : { items: [] };
|
||||||
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
|
const clients = clientsResponse.ok ? await clientsResponse.json() : { items: [] };
|
||||||
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
|
const providers = providersResponse.ok ? await providersResponse.json() : { items: [] };
|
||||||
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
|
const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] };
|
||||||
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
|
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
|
||||||
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
|
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
|
||||||
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
||||||
const enclosure = enclosureResponse.ok ? await enclosureResponse.json() : { items: [] };
|
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
||||||
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invoice: null,
|
invoice: null,
|
||||||
invoiceId: null,
|
invoiceId: null,
|
||||||
isCreate: true,
|
isCreate: true,
|
||||||
invoiceTypes: invoiceTypes.items || [],
|
invoiceTypes: invoiceTypes.items || [],
|
||||||
customsBrokers: customsBrokers.items || [],
|
customsBrokers: customsBrokers.items || [],
|
||||||
clients: clients.items || [],
|
clients: clients.items || [],
|
||||||
providers: providers.items || [],
|
providers: providers.items || [],
|
||||||
currencyTypes: currencyTypes.items || [],
|
currencyTypes: currencyTypes.items || [],
|
||||||
transportTypes: transportTypes.items || [],
|
transportTypes: transportTypes.items || [],
|
||||||
seals: seals.items || [],
|
seals: seals.items || [],
|
||||||
incoterms: incoterms.items || [],
|
incoterms: incoterms.items || [],
|
||||||
enclosure: enclosure.items || [],
|
pedimentos: pedimentos.items || [],
|
||||||
pedimentos: pedimentos.items || [],
|
// Filtros desde query parameters para preselección
|
||||||
// Filtros desde query parameters para preselección
|
filters: {
|
||||||
filters: {
|
operation_type: parsedOperationType,
|
||||||
operation_type: parsedOperationType,
|
invoice_type: invoiceTypeParam || null
|
||||||
invoice_type: invoiceTypeParam || null
|
}
|
||||||
}
|
};
|
||||||
};
|
} catch (err) {
|
||||||
|
console.error('Error loading data for new invoice:', err);
|
||||||
|
// En caso de error, devolver estructura mínima para que la página pueda cargar
|
||||||
|
return {
|
||||||
|
invoice: null,
|
||||||
|
invoiceId: null,
|
||||||
|
isCreate: true,
|
||||||
|
invoiceTypes: [],
|
||||||
|
customsBrokers: [],
|
||||||
|
clients: [],
|
||||||
|
providers: [],
|
||||||
|
currencyTypes: [],
|
||||||
|
transportTypes: [],
|
||||||
|
seals: [],
|
||||||
|
incoterms: [],
|
||||||
|
pedimentos: [],
|
||||||
|
filters: {
|
||||||
|
operation_type: parsedOperationType,
|
||||||
|
invoice_type: invoiceTypeParam || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const invoiceId = parseInt(params.id);
|
const invoiceId = parseInt(params.id);
|
||||||
@@ -191,7 +203,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
transportTypesResponse,
|
transportTypesResponse,
|
||||||
sealsResponse,
|
sealsResponse,
|
||||||
incotermsResponse,
|
incotermsResponse,
|
||||||
enclosureResponse,
|
|
||||||
pedimentosResponse
|
pedimentosResponse
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
invoiceTypesPromise,
|
invoiceTypesPromise,
|
||||||
@@ -202,7 +213,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
transportTypesPromise,
|
transportTypesPromise,
|
||||||
sealsPromise,
|
sealsPromise,
|
||||||
incotermsPromise,
|
incotermsPromise,
|
||||||
enclosurePromise,
|
|
||||||
pedimentosPromise
|
pedimentosPromise
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -214,7 +224,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
|
const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] };
|
||||||
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
|
const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] };
|
||||||
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] };
|
||||||
const enclosure = enclosureResponse.ok ? await enclosureResponse.json() : { items: [] };
|
|
||||||
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -229,7 +238,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
|
|||||||
transportTypes: transportTypes.items || [],
|
transportTypes: transportTypes.items || [],
|
||||||
seals: seals.items || [],
|
seals: seals.items || [],
|
||||||
incoterms: incoterms.items || [],
|
incoterms: incoterms.items || [],
|
||||||
enclosure: enclosure.items || [],
|
|
||||||
pedimentos: pedimentos.items || [],
|
pedimentos: pedimentos.items || [],
|
||||||
// Filtros desde query parameters para preselección
|
// Filtros desde query parameters para preselección
|
||||||
filters: {
|
filters: {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import * as Tabs from '$lib/components/ui/tabs';
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
import * as Alert from '$lib/components/ui/alert';
|
import * as Alert from '$lib/components/ui/alert';
|
||||||
@@ -17,8 +19,6 @@
|
|||||||
LoaderCircle,
|
LoaderCircle,
|
||||||
Save
|
Save
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
import { useSidebar } from '$lib/components/ui/sidebar/context.svelte';
|
|
||||||
import { companyStore } from '$lib/stores/company.svelte';
|
|
||||||
|
|
||||||
// Importar los componentes de cada pestaña
|
// Importar los componentes de cada pestaña
|
||||||
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
|
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
|
||||||
@@ -34,8 +34,20 @@
|
|||||||
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
|
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
|
||||||
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
|
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
|
||||||
|
|
||||||
// Get sidebar context
|
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
|
||||||
const sidebar = useSidebar();
|
let companyStore: any = $state(undefined);
|
||||||
|
let mounted = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
try {
|
||||||
|
const companyStoreModule = await import('$lib/stores/company.svelte');
|
||||||
|
companyStore = companyStoreModule.companyStore;
|
||||||
|
mounted = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading client modules:', err);
|
||||||
|
mounted = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
interface ExtendedPageData {
|
interface ExtendedPageData {
|
||||||
invoiceId?: number | null;
|
invoiceId?: number | null;
|
||||||
@@ -471,7 +483,7 @@
|
|||||||
|
|
||||||
if (data.isCreate) {
|
if (data.isCreate) {
|
||||||
// Crear nueva factura con todos sus sub-recursos
|
// Crear nueva factura con todos sus sub-recursos
|
||||||
const response = await invoicesApi.create(companyStore.activeCompany?.id || 0, payload as CreateInvoiceData);
|
const response = await invoicesApi.create(companyStore?.activeCompany?.id || 0, payload as CreateInvoiceData);
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
|
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
|
||||||
throw new Error(errorMsg);
|
throw new Error(errorMsg);
|
||||||
@@ -484,7 +496,7 @@
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Actualizar factura existente con todos sus sub-recursos
|
// Actualizar factura existente con todos sus sub-recursos
|
||||||
const response = await invoicesApi.update(invoiceId!, companyStore.activeCompany?.id || 0, payload as UpdateInvoiceData);
|
const response = await invoicesApi.update(invoiceId!, companyStore?.activeCompany?.id || 0, payload as UpdateInvoiceData);
|
||||||
if (response.error) throw new Error(response.error);
|
if (response.error) throw new Error(response.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -635,8 +647,7 @@
|
|||||||
|
|
||||||
<!-- Footer fijo en la parte inferior -->
|
<!-- Footer fijo en la parte inferior -->
|
||||||
<div
|
<div
|
||||||
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] transition-[left] duration-200 ease-linear"
|
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5]"
|
||||||
style:left={sidebar.isMobile ? '0' : (sidebar.open ? 'var(--sidebar-width)' : '0')}
|
|
||||||
>
|
>
|
||||||
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
|
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
|
||||||
<!-- Tabs Navigation -->
|
<!-- Tabs Navigation -->
|
||||||
|
|||||||
132
frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts
Normal file
132
frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import type { PageLoad } from './$types';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
// Deshabilitar SSR para esta página debido al layout de dashboard que usa stores del cliente
|
||||||
|
export const ssr = false;
|
||||||
|
|
||||||
|
export const load: PageLoad = async ({ params, url, parent }) => {
|
||||||
|
// Obtener datos del layout padre
|
||||||
|
const parentData = await parent();
|
||||||
|
|
||||||
|
const operationTypeParam = url.searchParams.get('operation_type');
|
||||||
|
const invoiceTypeParam = url.searchParams.get('invoice_type');
|
||||||
|
|
||||||
|
let parsedOperationType: number | null = null;
|
||||||
|
if (operationTypeParam) {
|
||||||
|
const parsed = parseInt(operationTypeParam, 10);
|
||||||
|
if (!isNaN(parsed)) {
|
||||||
|
parsedOperationType = parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si el ID es "new", cargar datos de referencia
|
||||||
|
if (params.id === 'new') {
|
||||||
|
try {
|
||||||
|
// Llamar a la ruta de servidor que ya existe
|
||||||
|
const response = await fetch(`/api-sveltekit/invoices/reference-data`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('Error loading reference data:', response.status);
|
||||||
|
// Retornar estructura vacía en caso de error
|
||||||
|
return {
|
||||||
|
invoice: null,
|
||||||
|
invoiceId: null,
|
||||||
|
isCreate: true,
|
||||||
|
invoiceTypes: [],
|
||||||
|
customsBrokers: [],
|
||||||
|
clients: [],
|
||||||
|
providers: [],
|
||||||
|
currencyTypes: [],
|
||||||
|
transportTypes: [],
|
||||||
|
seals: [],
|
||||||
|
incoterms: [],
|
||||||
|
pedimentos: [],
|
||||||
|
filters: {
|
||||||
|
operation_type: parsedOperationType,
|
||||||
|
invoice_type: invoiceTypeParam || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
invoice: null,
|
||||||
|
invoiceId: null,
|
||||||
|
isCreate: true,
|
||||||
|
invoiceTypes: data.invoiceTypes || [],
|
||||||
|
customsBrokers: data.customsBrokers || [],
|
||||||
|
clients: data.clients || [],
|
||||||
|
providers: data.providers || [],
|
||||||
|
currencyTypes: data.currencyTypes || [],
|
||||||
|
transportTypes: data.transportTypes || [],
|
||||||
|
seals: data.seals || [],
|
||||||
|
incoterms: data.incoterms || [],
|
||||||
|
pedimentos: data.pedimentos || [],
|
||||||
|
filters: {
|
||||||
|
operation_type: parsedOperationType,
|
||||||
|
invoice_type: invoiceTypeParam || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading data for new invoice:', err);
|
||||||
|
return {
|
||||||
|
invoice: null,
|
||||||
|
invoiceId: null,
|
||||||
|
isCreate: true,
|
||||||
|
invoiceTypes: [],
|
||||||
|
customsBrokers: [],
|
||||||
|
clients: [],
|
||||||
|
providers: [],
|
||||||
|
currencyTypes: [],
|
||||||
|
transportTypes: [],
|
||||||
|
seals: [],
|
||||||
|
incoterms: [],
|
||||||
|
pedimentos: [],
|
||||||
|
filters: {
|
||||||
|
operation_type: parsedOperationType,
|
||||||
|
invoice_type: invoiceTypeParam || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si es un ID numérico, cargar la factura
|
||||||
|
const invoiceId = parseInt(params.id);
|
||||||
|
if (isNaN(invoiceId)) {
|
||||||
|
throw error(400, 'ID de factura inválido');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api-sveltekit/invoices/${invoiceId}/edit-data`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw error(response.status, 'Error al cargar la factura');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
return {
|
||||||
|
invoice: data.invoice,
|
||||||
|
invoiceId,
|
||||||
|
isCreate: false,
|
||||||
|
invoiceTypes: data.invoiceTypes || [],
|
||||||
|
customsBrokers: data.customsBrokers || [],
|
||||||
|
clients: data.clients || [],
|
||||||
|
providers: data.providers || [],
|
||||||
|
currencyTypes: data.currencyTypes || [],
|
||||||
|
transportTypes: data.transportTypes || [],
|
||||||
|
seals: data.seals || [],
|
||||||
|
incoterms: data.incoterms || [],
|
||||||
|
pedimentos: data.pedimentos || [],
|
||||||
|
filters: {
|
||||||
|
operation_type: parsedOperationType,
|
||||||
|
invoice_type: invoiceTypeParam || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading invoice:', err);
|
||||||
|
throw error(500, 'Error al cargar la factura');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -6,7 +6,11 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
port: 5173, // fija el puerto
|
port: 5173, // fija el puerto
|
||||||
host: true // escucha en 0.0.0.0
|
host: true, // escucha en 0.0.0.0
|
||||||
|
allowedHosts: [
|
||||||
|
'anexo76-dev.aduanasoft.com',
|
||||||
|
// 'otro-host.com' si necesitas más
|
||||||
|
],
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ YELLOW='\033[1;33m'
|
|||||||
NC='\033[0m' # No Color
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
# Variables de configuración
|
# Variables de configuración
|
||||||
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080}"
|
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080/kcauth}"
|
||||||
KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}"
|
KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}"
|
||||||
KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
|
KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
|
||||||
KEYCLOAK_REALM="${KEYCLOAK_REALM:-master}"
|
KEYCLOAK_REALM="${KEYCLOAK_REALM:-master}"
|
||||||
KEYCLOACK_ADMIN_URL="${KEYCLOACK_ADMIN_URL:-http://localhost:9000}"
|
KEYCLOACK_ADMIN_URL="${KEYCLOACK_ADMIN_URL:-http://localhost:9000/kcauth}"
|
||||||
|
|
||||||
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
POSTGRES_HOST="${POSTGRES_HOST:-localhost}"
|
||||||
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
POSTGRES_PORT="${POSTGRES_PORT:-5432}"
|
||||||
|
|||||||
Reference in New Issue
Block a user