Merge pull request 'feature/production' (#33) from feature/production into development

Reviewed-on: ADUANASOFT/anexo76#33
This commit is contained in:
2025-12-29 19:56:26 +00:00
25 changed files with 831 additions and 230 deletions

View File

@@ -132,6 +132,21 @@ python -c "from core.database import init_db; init_db()"
- **Frontend**: http://localhost:5173
- **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
### Backend

View File

@@ -12,7 +12,7 @@ CORE_DB_USER=postgres
CORE_DB_PASSWORD=postgres
# Keycloak
KEYCLOAK_SERVER_URL=http://localhost:8080
KEYCLOAK_SERVER_URL=http://localhost:8080/kcauth
KEYCLOAK_REALM=master
KEYCLOAK_CLIENT_ID=anexo76-backend
KEYCLOAK_CLIENT_SECRET=your-client-secret

View File

@@ -31,7 +31,7 @@ class AuthService:
def __init__(self, db: Session):
self.db = db
self.keycloak_openid = KeycloakOpenID(
server_url=settings.KEYCLOAK_SERVER_URL,
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
client_id=settings.KEYCLOAK_CLIENT_ID,
realm_name=settings.KEYCLOAK_REALM,
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
@@ -64,7 +64,7 @@ class AuthService:
# Crear nueva instancia de KeycloakOpenID con el realm del tenant
keycloak_client = KeycloakOpenID(
server_url=settings.KEYCLOAK_SERVER_URL,
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
client_id=settings.KEYCLOAK_CLIENT_ID,
realm_name=tenant.keycloak_realm,
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,
@@ -78,7 +78,7 @@ class AuthService:
# o buscar el usuario por username
try:
keycloak_admin = KeycloakAdmin(
server_url=settings.KEYCLOAK_SERVER_URL,
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
username=settings.KEYCLOAK_ADMIN_USERNAME,
password=settings.KEYCLOAK_ADMIN_PASSWORD,
realm_name=tenant.keycloak_realm,
@@ -279,7 +279,7 @@ class AuthService:
# Crear instancia de KeycloakAdmin para gestión de usuarios
keycloak_admin = KeycloakAdmin(
server_url=settings.KEYCLOAK_SERVER_URL,
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
username=settings.KEYCLOAK_ADMIN_USERNAME,
password=settings.KEYCLOAK_ADMIN_PASSWORD,
realm_name=tenant.keycloak_realm,

View File

@@ -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()

View File

@@ -24,7 +24,7 @@ class Settings(BaseSettings):
CORE_DB_PASSWORD: str = "postgres"
# Keycloak
KEYCLOAK_SERVER_URL: str = "http://localhost:8080"
KEYCLOAK_SERVER_URL: str = "http://localhost:8080/kcauth"
KEYCLOAK_REALM: str = "master"
KEYCLOAK_CLIENT_ID: str = "anexo76-backend"
KEYCLOAK_CLIENT_SECRET: str = ""
@@ -43,7 +43,7 @@ class Settings(BaseSettings):
LICENSE_CHECK_ENABLED: bool = True
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

View File

@@ -124,7 +124,7 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware):
db = CoreSessionLocal()
try:
# 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_info = license_service.validate_license(tenant_id)

View File

@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
# Configuración de Keycloak
keycloak_openid = KeycloakOpenID(
server_url=settings.KEYCLOAK_SERVER_URL,
server_url=f"{settings.KEYCLOAK_SERVER_URL}/kcauth",
client_id=settings.KEYCLOAK_CLIENT_ID,
realm_name=settings.KEYCLOAK_REALM,
client_secret_key=settings.KEYCLOAK_CLIENT_SECRET,

View File

@@ -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()

View File

@@ -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
View 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

View File

@@ -94,10 +94,12 @@ services:
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
- --http-relative-path=/kcauth
- --db=postgres
- --db-url-host=postgres-keycloak
- --db-url-port=5432
@@ -120,7 +122,7 @@ services:
- backend-net
restart: unless-stopped
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
timeout: 5s
retries: 30
@@ -156,13 +158,13 @@ services:
- 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}
- 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:-http://localhost:5173,http://localhost:3000}
ports:
- "5050:8000"
- "8000:8000"
depends_on:
postgres-a76:
condition: service_healthy
@@ -209,10 +211,10 @@ services:
- NODE_ENV=${NODE_ENV:-development}
- VITE_API_URL=${VITE_API_URL:-http://localhost: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_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_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-zRU5NuvUFtBSOuh7Kdc372AItoWGLgz9}

59
frontend/Dockerfile.prod Normal file
View 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"]

View File

@@ -56,6 +56,7 @@
},
"dependencies": {
"keycloak-js": "^26.2.1",
"lucide-svelte": "^0.553.0"
"lucide-svelte": "^0.553.0",
"svelte-sonner": "^1.0.7"
}
}

View File

@@ -14,6 +14,9 @@ importers:
lucide-svelte:
specifier: ^0.553.0
version: 0.553.0(svelte@5.40.2)
svelte-sonner:
specifier: ^1.0.7
version: 1.0.7(svelte@5.40.2)
devDependencies:
'@eslint/compat':
specifier: ^1.4.0
@@ -1675,6 +1678,11 @@ packages:
run-parallel@1.2.0:
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:
resolution: {integrity: sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==}
peerDependencies:
@@ -1761,6 +1769,11 @@ packages:
svelte:
optional: true
svelte-sonner@1.0.7:
resolution: {integrity: sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A==}
peerDependencies:
svelte: ^5.0.0
svelte-toolbelt@0.10.6:
resolution: {integrity: sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==}
engines: {node: '>=18', pnpm: '>=8.7.0'}
@@ -3379,6 +3392,11 @@ snapshots:
dependencies:
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):
dependencies:
dequal: 2.0.3
@@ -3460,6 +3478,11 @@ snapshots:
optionalDependencies:
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):
dependencies:
clsx: 2.1.1

View File

@@ -4,7 +4,8 @@
import { getToken } from './auth';
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> {
data?: T;

View File

@@ -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({
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({
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'>) => {
const params = new URLSearchParams({
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) => {
const params = new URLSearchParams({
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({
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'>) => {
const params = new URLSearchParams({
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) => {
const params = new URLSearchParams({
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({
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'>) => {
const params = new URLSearchParams({
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) => {
const params = new URLSearchParams({
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()}`);
}
}
};

View File

@@ -134,48 +134,63 @@ export async function authenticatedFetch(
fetch: typeof globalThis.fetch,
redirectUrl?: string
): Promise<Response> {
const baseUrl = getServerApiUrl();
let { accessToken } = getAuthTokens(cookies);
try {
const baseUrl = getServerApiUrl();
let { accessToken } = getAuthTokens(cookies);
// Si no hay token, redirigir o lanzar error
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);
// Si no hay token, redirigir o lanzar error
if (!accessToken) {
if (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' }
});
}
}
/**

View File

@@ -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');
}
};

View File

@@ -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 });
}
};

View File

@@ -31,14 +31,14 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
// Cargar datos de referencia necesarios
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,
fetch
);
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,
fetch
@@ -46,14 +46,14 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
// Cargar clientes y proveedores
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,
fetch
);
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,
fetch
@@ -61,42 +61,35 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
// Cargar tipos de moneda, transporte, etc.
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,
fetch
);
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,
fetch
);
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,
fetch
);
const incotermsPromise = authenticatedFetch(
'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',
'v1/public/refrence_data/incoterms/?page=1&page_size=100',
{},
cookies,
fetch
);
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,
fetch
@@ -104,61 +97,80 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
// Si el ID es "new", es una creación
if (params.id === 'new') {
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
incotermsResponse,
enclosureResponse,
pedimentosResponse
] = await Promise.all([
invoiceTypesPromise,
customsBrokersPromise,
clientsPromise,
providersPromise,
currencyTypesPromise,
transportTypesPromise,
sealsPromise,
incotermsPromise,
enclosurePromise,
pedimentosPromise
]);
try {
const [
invoiceTypesResponse,
customsBrokersResponse,
clientsResponse,
providersResponse,
currencyTypesResponse,
transportTypesResponse,
sealsResponse,
incotermsResponse,
pedimentosResponse
] = await Promise.all([
invoiceTypesPromise,
customsBrokersPromise,
clientsPromise,
providersPromise,
currencyTypesPromise,
transportTypesPromise,
sealsPromise,
incotermsPromise,
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 seals = sealsResponse.ok ? await sealsResponse.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 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 {
invoice: null,
invoiceId: null,
isCreate: true,
invoiceTypes: invoiceTypes.items || [],
customsBrokers: customsBrokers.items || [],
clients: clients.items || [],
providers: providers.items || [],
currencyTypes: currencyTypes.items || [],
transportTypes: transportTypes.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
enclosure: enclosure.items || [],
pedimentos: pedimentos.items || [],
// Filtros desde query parameters para preselección
filters: {
operation_type: parsedOperationType,
invoice_type: invoiceTypeParam || null
}
};
return {
invoice: null,
invoiceId: null,
isCreate: true,
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 || [],
// Filtros desde query parameters para preselección
filters: {
operation_type: parsedOperationType,
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);
@@ -191,7 +203,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
transportTypesResponse,
sealsResponse,
incotermsResponse,
enclosureResponse,
pedimentosResponse
] = await Promise.all([
invoiceTypesPromise,
@@ -202,7 +213,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
transportTypesPromise,
sealsPromise,
incotermsPromise,
enclosurePromise,
pedimentosPromise
]);
@@ -214,7 +224,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
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 enclosure = enclosureResponse.ok ? await enclosureResponse.json() : { items: [] };
const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] };
return {
@@ -229,7 +238,6 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => {
transportTypes: transportTypes.items || [],
seals: seals.items || [],
incoterms: incoterms.items || [],
enclosure: enclosure.items || [],
pedimentos: pedimentos.items || [],
// Filtros desde query parameters para preselección
filters: {

View File

@@ -1,4 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import * as Tabs from '$lib/components/ui/tabs';
import * as Alert from '$lib/components/ui/alert';
@@ -17,8 +19,6 @@
LoaderCircle,
Save
} 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
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 { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
// Get sidebar context
const sidebar = useSidebar();
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
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 {
invoiceId?: number | null;
@@ -471,7 +483,7 @@
if (data.isCreate) {
// 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) {
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
throw new Error(errorMsg);
@@ -484,7 +496,7 @@
return;
} else {
// 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);
}
@@ -635,8 +647,7 @@
<!-- Footer fijo en la parte inferior -->
<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"
style:left={sidebar.isMobile ? '0' : (sidebar.open ? 'var(--sidebar-width)' : '0')}
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]"
>
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
<!-- Tabs Navigation -->

View 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');
}
};

View File

@@ -6,7 +6,11 @@ import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
server: {
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: [
tailwindcss(),

View File

@@ -31,11 +31,11 @@ YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 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_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD:-admin}"
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_PORT="${POSTGRES_PORT:-5432}"