=== ESTRUCTURA DEL PROYECTO ===
./
    .env.example
    .gitignore
    docker-compose.yml
    proyecto_completo.txt
    README.md
    .github/
        copilot-instructions.md
    backend/
        add_columns.py
        fix_password_script.py
        pyproject.toml
        README.md
        requirements.txt
        alembic/
            versions/
                add_clients_table.py
        app/
            main.py
            api/
                deps.py
                v1/
                    router.py
                    endpoints/
                        auth.py
                        categories.py
                        clients.py
                        health.py
                        systems.py
                        tenants.py
                        users.py
            core/
                config.py
                database.py
                logging.py
                security.py
            middleware/
                correlation_id.py
                tenant.py
            models/
                category.py
                client.py
                system.py
                tenant.py
                ticket.py
                user.py
                clientes/
            schemas/
                client.py
        logs/
        tests/
            test_clients.py
        uploads/
    db/
        schema.sql
    docker/
        Dockerfile.backend
        Dockerfile.frontend
        Dockerfile.worker
        nginx/
            default.conf
            nginx.conf
    docs/
        api-contract.md
        database-schema.md
    frontend-client/
        .eslintrc.json
        .prettierignore
        .prettierrc
        package.json
        postcss.config.js
        svelte.config.js
        tailwind.config.js
        vite.config.js
        .svelte-kit/
            ambient.d.ts
            tsconfig.json
            generated/
                root.svelte
                client/
                    app.js
                    matchers.js
                    nodes/
                        0.js
                        1.js
                        2.js
                        3.js
                        4.js
                        5.js
                        6.js
                        7.js
                server/
                    internal.js
            types/
                route_meta_data.json
                src/
                    routes/
                        $types.d.ts
                        login/
                            $types.d.ts
                        profile/
                            $types.d.ts
                        tickets/
                            $types.d.ts
                            new/
                                $types.d.ts
                            [id]/
                                $types.d.ts
        src/
            app.css
            app.html
            lib/
                components/
                    Header.svelte
                    Icon.svelte
                    TicketCard.svelte
                    Toast.svelte
                stores/
                    app.ts
                    auth.ts
                    clientes.ts
                    tickets.ts
                    toast.ts
            routes/
                +layout.svelte
                +page.svelte
                login/
                    +page.svelte
                profile/
                    +page.svelte
                tickets/
                    +page.svelte
                    new/
                        +page.svelte
                    [id]/
                        +page.svelte
        static/
            images/
                Icono AS(1).png
                Logo AS 192px -192px(1).png
                Logo AS 512px - 512px(1).png
                Logo AS blanco(1).png
                SOPORTE.webp
    frontend-internal/
        .eslintrc.json
        .prettierignore
        .prettierrc
        package.json
        postcss.config.js
        svelte.config.js
        tailwind.config.js
        vite.config.js
        .svelte-kit/
            ambient.d.ts
            tsconfig.json
            generated/
                root.svelte
                client/
                    app.js
                    matchers.js
                    nodes/
                        0.js
                        1.js
                        2.js
                        3.js
                        4.js
                        5.js
                        6.js
                        7.js
                server/
                    internal.js
            types/
                route_meta_data.json
                src/
                    routes/
                        $types.d.ts
                        categories/
                            $types.d.ts
                        login/
                            $types.d.ts
                        systems/
                            $types.d.ts
                        tenants/
                            $types.d.ts
                        users/
                            $types.d.ts
        src/
            app.css
            app.html
            lib/
                components/
                    Header.svelte
                    Icon.svelte
                    Modal.svelte
                    Sidebar.svelte
                    Toast.svelte
                stores/
                    api.ts
                    auth.ts
                    toast.ts
                utils/
                    api.ts
            routes/
                +layout.svelte
                +page.svelte
                categories/
                    +page.svelte
                login/
                    +page.svelte
                systems/
                    +page.svelte
                tenants/
                    +page.svelte
                users/
                    +page.svelte
    scripts/
        README.md
        setup-dev.sh
    workers/
        README.md
        requirements.txt
        app/
            celery.py
            core/
                config.py
                logging.py
            tasks/
                email_tasks.py
                maintenance_tasks.py
                notification_tasks.py
                sla_tasks.py
        beat-data/
        celerybeat-schedule/
        logs/
        uploads/
    Zpracticante/
        Documentacion Practicas.docx
        ~$cumentacion Practicas.docx
        Backups/
            respaldo_docker_v1.sql


=== CONTENIDO DE LOS ARCHIVOS ===

==================================================
ARCHIVO: .\.env.example
==================================================
# ServiceManagerWeb - Variables de Entorno
# Copia este archivo como .env y configura los valores apropiados

# ===================================
# CONFIGURACIÓN GENERAL
# ===================================
ENVIRONMENT=development
DEBUG=true
SECRET_KEY=your-super-secret-key-change-in-production-min-32-chars
API_VERSION=v1
CORS_ORIGINS=http://localhost:3000,http://localhost:3001

# ===================================
# BASE DE DATOS
# ===================================
# PostgreSQL
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=servicemanager
POSTGRES_USER=servicemanager
POSTGRES_PASSWORD=servicemanager123
DATABASE_URL=postgresql+asyncpg://servicemanager:servicemanager123@postgres:5432/servicemanager

# ===================================
# REDIS Y CELERY
# ===================================
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
REDIS_URL=redis://redis:6379/0
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0

# ===================================
# AUTENTICACIÓN JWT
# ===================================
JWT_SECRET_KEY=jwt-secret-key-change-in-production-min-32-chars
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=60
REFRESH_TOKEN_EXPIRE_DAYS=7

# ===================================
# EMAIL / NOTIFICACIONES
# ===================================
# SMTP Settings
SMTP_HOST=mailhog
SMTP_PORT=1025
SMTP_USER=
SMTP_PASSWORD=
SMTP_USE_TLS=false
SMTP_USE_SSL=false

# Email por defecto
DEFAULT_FROM_EMAIL=noreply@servicemanager.local
DEFAULT_FROM_NAME=ServiceManager

# ===================================
# ARCHIVOS Y STORAGE
# ===================================
# Configuración de uploads
MAX_UPLOAD_SIZE_MB=10
ALLOWED_FILE_EXTENSIONS=pdf,jpg,jpeg,png,doc,docx,xls,xlsx,txt,zip
UPLOAD_PATH=/app/uploads

# ===================================
# SEGURIDAD
# ===================================
# Rate limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_AUTH=10/minute
RATE_LIMIT_API=100/minute
RATE_LIMIT_UPLOAD=5/minute

# Password hashing
PASSWORD_MIN_LENGTH=8
ARGON2_TIME_COST=3
ARGON2_MEMORY_COST=65536
ARGON2_PARALLELISM=4

# ===================================
# LOGGING
# ===================================
LOG_LEVEL=INFO
LOG_FORMAT=json
LOG_FILE=/app/logs/app.log

# ===================================
# FRONTEND URLS
# ===================================
CLIENT_FRONTEND_URL=http://localhost:3000
INTERNAL_FRONTEND_URL=http://localhost:3001
API_BASE_URL=http://localhost:8000

# ===================================
# HEALTH CHECKS
# ===================================
HEALTH_CHECK_TIMEOUT=30

# ===================================
# DEVELOPMENT ONLY
# ===================================
# Adminer (DB admin interface)
ADMINER_ENABLED=true

# MailHog (email testing)
MAILHOG_ENABLED=true

# Seed data
LOAD_SAMPLE_DATA=true

==================================================
ARCHIVO: .\docker-compose.yml
==================================================
version: '3.8'

services:
  # ===================================
  # POSTGRES DATABASE
  # ===================================
  postgres:
    image: postgres:15-alpine
    container_name: servicemanager-db
    restart: unless-stopped
    environment:
      POSTGRES_DB: ${POSTGRES_DB:-servicemanager}
      POSTGRES_USER: ${POSTGRES_USER:-servicemanager}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-servicemanager123}
      POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-servicemanager}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - servicemanager-network

  # ===================================
  # REDIS CACHE & BROKER
  # ===================================
  redis:
    image: redis:7-alpine
    container_name: servicemanager-redis
    restart: unless-stopped
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 5
    networks:
      - servicemanager-network

  # ===================================
  # FASTAPI BACKEND
  # ===================================
  backend:
    build:
      context: ./backend
      dockerfile: ../docker/Dockerfile.backend
    container_name: servicemanager-backend
    restart: unless-stopped
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    env_file:
      - .env
    environment:
      - ENVIRONMENT=${ENVIRONMENT:-development}
      - DEBUG=${DEBUG:-true}
      - SECRET_KEY=${SECRET_KEY}
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - CELERY_BROKER_URL=${CELERY_BROKER_URL}
      - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
      - JWT_SECRET_KEY=${JWT_SECRET_KEY}
      - CORS_ORIGINS=${CORS_ORIGINS}
      - SMTP_HOST=${SMTP_HOST}
      - SMTP_PORT=${SMTP_PORT}
      - DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL}
    volumes:
      - ./backend:/app
      - uploads_data:/app/uploads
      - logs_data:/app/logs
    ports:
      - "8000:8000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - servicemanager-network

  # ===================================
  # CELERY WORKER
  # ===================================
  worker:
    build:
      context: ./workers
      dockerfile: ../docker/Dockerfile.worker
    container_name: servicemanager-worker
    restart: unless-stopped
    env_file:
      - .env
    environment:
      - ENVIRONMENT=${ENVIRONMENT:-development}
      - DATABASE_URL=${DATABASE_URL}
      - CELERY_BROKER_URL=${CELERY_BROKER_URL}
      - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
      - SMTP_HOST=${SMTP_HOST}
      - SMTP_PORT=${SMTP_PORT}
      - DEFAULT_FROM_EMAIL=${DEFAULT_FROM_EMAIL}
    volumes:
      - ./workers:/app
      - uploads_data:/app/uploads
      - logs_data:/app/logs
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - servicemanager-network

  # ===================================
  # CELERY BEAT (SCHEDULER)
  # ===================================
  beat:
    build:
      context: ./workers
      dockerfile: ../docker/Dockerfile.worker
    container_name: servicemanager-beat
    restart: unless-stopped
    command: celery -A app.celery beat --loglevel=info --schedule=/tmp/celerybeat-schedule
    env_file:
      - .env
    environment:
      - ENVIRONMENT=${ENVIRONMENT:-development}
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - CELERY_BROKER_URL=${CELERY_BROKER_URL}
      - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND}
    volumes:
      - ./workers:/app
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - servicemanager-network

  # ===================================
  # FRONTEND CLIENT (CLIENTES)
  # ===================================
  frontend-client:
    build:
      context: ./frontend-client
      dockerfile: ../docker/Dockerfile.frontend
      args:
        - FRONTEND_TYPE=client
    container_name: servicemanager-client-frontend
    restart: unless-stopped
    environment:
      - NODE_ENV=${ENVIRONMENT:-development}
      - PUBLIC_API_URL=${API_BASE_URL:-http://localhost:8000}
      - PUBLIC_APP_NAME=ServiceManager Cliente
    volumes:
      - ./frontend-client:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    depends_on:
      - backend
    networks:
      - servicemanager-network

  # ===================================
  # FRONTEND INTERNAL (STAFF INTERNO)
  # ===================================
  frontend-internal:
    build:
      context: ./frontend-internal
      dockerfile: ../docker/Dockerfile.frontend
      args:
        - FRONTEND_TYPE=internal
    container_name: servicemanager-internal-frontend
    restart: unless-stopped
    environment:
      - NODE_ENV=${ENVIRONMENT:-development}
      - PUBLIC_API_URL=${API_BASE_URL:-http://localhost:8000}
      - PUBLIC_APP_NAME=ServiceManager Admin
    volumes:
      - ./frontend-internal:/app
      - /app/node_modules
    ports:
      - "3001:3000"
    depends_on:
      - backend
    networks:
      - servicemanager-network

  # ===================================
  # NGINX REVERSE PROXY
  # ===================================
  nginx:
    image: nginx:alpine
    container_name: servicemanager-nginx
    restart: unless-stopped
    volumes:
      - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - uploads_data:/var/www/uploads:ro
    ports:
      - "80:80"
    depends_on:
      - backend
      - frontend-client
      - frontend-internal
    networks:
      - servicemanager-network

  # ===================================
  # DEVELOPMENT TOOLS
  # ===================================

  # Adminer - Database Admin Interface
  adminer:
    image: adminer:4-standalone
    container_name: servicemanager-adminer
    restart: unless-stopped
    environment:
      ADMINER_DEFAULT_SERVER: postgres
      ADMINER_DESIGN: galkaev
    ports:
      - "8080:8080"
    depends_on:
      - postgres
    networks:
      - servicemanager-network
    profiles:
      - dev

  # MailHog - Email Testing
  mailhog:
    image: mailhog/mailhog:latest
    container_name: servicemanager-mailhog
    restart: unless-stopped
    ports:
      - "1025:1025"  # SMTP server
      - "8025:8025"  # Web UI
    networks:
      - servicemanager-network
    profiles:
      - dev

  # Redis Commander - Redis GUI
  redis-commander:
    image: rediscommander/redis-commander:latest
    container_name: servicemanager-redis-commander
    restart: unless-stopped
    environment:
      REDIS_HOSTS: local:redis:6379
    ports:
      - "8081:8081"
    depends_on:
      - redis
    networks:
      - servicemanager-network
    profiles:
      - dev

# ===================================
# VOLUMES
# ===================================
volumes:
  postgres_data:
    name: servicemanager_postgres_data
  redis_data:
    name: servicemanager_redis_data
  uploads_data:
    name: servicemanager_uploads_data
  logs_data:
    name: servicemanager_logs_data
  celery_beat_data:
    name: servicemanager_celery_beat_data

# ===================================
# NETWORKS
# ===================================
networks:
  servicemanager-network:
    name: servicemanager_network
    driver: bridge




==================================================
ARCHIVO: .\README.md
==================================================
# ServiceManagerWeb - Mesa de Ayuda B2B

Sistema multi-tenant de Mesa de Ayuda/Soporte Técnico empresarial para Aduanasoft.

## Arquitectura

- **Frontend**: SvelteKit + TypeScript (portal clientes + panel interno)
- **Backend**: Python FastAPI + Pydantic v2 
- **Workers**: Celery + Redis (notificaciones, SLAs, jobs)
- **BD**: PostgreSQL + Alembic migrations
- **Auth**: JWT + Refresh tokens + 2FA opcional (TOTP)
- **Infra**: Docker Compose local, preparado para producción

## Estructura del Monorepo

```
ServiceManagerWeb/
├── backend/               # FastAPI app
├── frontend-client/       # SvelteKit app para clientes
├── frontend-internal/     # SvelteKit app para staff interno
├── workers/              # Celery tasks
├── db/                   # Migrations y esquemas
├── docker/               # Dockerfiles específicos
├── docs/                 # Documentación adicional
├── scripts/              # Scripts de desarrollo/despliegue
├── docker-compose.yml    # Orquestación completa
└── .env.example          # Variables de entorno
```

## Stack Tecnológico

### Backend (Python)
- FastAPI (async)
- Pydantic v2
- SQLAlchemy 2.0 (async)
- Alembic (migrations)
- Argon2 (hashing passwords)
- PyJWT
- Celery + Redis

### Frontend (JavaScript/TypeScript)
- SvelteKit
- TypeScript
- TailwindCSS
- shadcn/ui o similar
- Zod (validación)

### Infraestructura
- PostgreSQL 15+
- Redis 7+
- Docker & Docker Compose
- Nginx (reverse proxy)

## Dominios del Sistema

1. **Auth**: Usuarios, roles, permisos, 2FA
2. **Tenants**: Multi-tenancy, organizaciones
3. **Tickets**: Gestión de tickets, estados, SLAs
4. **Notifications**: Email, plantillas, logs
5. **Audit**: Bitácora de acciones

## Roles de Usuario

### Internos (Staff)
- `ADMIN`: Control total del sistema
- `SUPPORT_MANAGER`: Gestión de equipos y SLAs
- `AGENT`: Atención de tickets
- `AUDITOR`: Solo lectura para auditoría

### Clientes
- `CLIENT_ADMIN`: Gestión de organización cliente
- `CLIENT_USER`: Creación y seguimiento de tickets

## Quick Start

```bash
# Clonar y configurar
git clone <repo>
cd ServiceManagerWeb
cp .env.example .env

# Levantar servicios
docker-compose up -d

# Verificar estado
docker-compose ps
```

## URLs por Defecto

- Frontend Clientes: http://localhost:3000
- Frontend Interno: http://localhost:3001  
- API Backend: http://localhost:8000
- API Docs: http://localhost:8000/docs
- Adminer (DB): http://localhost:8080

## Scripts de Desarrollo

```bash
# Backend
cd backend
python -m uvicorn app.main:app --reload --port 8000

# Frontend Cliente
cd frontend-client
npm run dev -- --port 3000

# Frontend Interno  
cd frontend-internal
npm run dev -- --port 3001

# Workers
cd workers
celery -A app.worker worker --loglevel=info
celery -A app.worker beat --loglevel=info
```

## Testing

```bash
# Backend tests
cd backend
pytest

# Frontend tests
cd frontend-client
npm test
cd ../frontend-internal
npm test
```

## Contribución

1. Fork del proyecto
2. Crear feature branch (`git checkout -b feature/nueva-funcionalidad`)
3. Commit cambios (`git commit -am 'Agregar nueva funcionalidad'`)
4. Push a branch (`git push origin feature/nueva-funcionalidad`)
5. Crear Pull Request

## Licencia

Propietario - Aduanasoft © 2026

==================================================
ARCHIVO: .\.github\copilot-instructions.md
==================================================
# ServiceManagerWeb - Mesa de Ayuda B2B Copilot Instructions

Este proyecto es un sistema multi-tenant de Mesa de Ayuda/Soporte Técnico empresarial.

## Contexto del Proyecto

- **Empresa**: Aduanasoft (B2B)
- **Sistema**: Mesa de Ayuda multi-tenant
- **Arquitectura**: Modular Monolith con Clean Architecture
- **Target**: MVP enterprise-grade

## Stack Tecnológico

### Backend
- Python FastAPI (async)
- Pydantic v2 para validación
- SQLAlchemy 2.0 (async ORM)
- PostgreSQL como base de datos principal
- Redis para cache y broker Celery
- Celery para tareas asíncronas
- Alembic para migraciones
- Argon2/Bcrypt para hash de passwords
- PyJWT para autenticación

### Frontend
- SvelteKit + TypeScript
- Dos aplicaciones: cliente e interna
- TailwindCSS para estilos
- Zod para validación del lado cliente

### DevOps
- Docker + Docker Compose
- Nginx como reverse proxy
- Variables de entorno para configuración
- Healthchecks para servicios

## Dominios del Sistema

1. **auth**: Autenticación, usuarios, roles, 2FA
2. **tenants**: Multi-tenancy, organizaciones cliente
3. **tickets**: Core del sistema - tickets, estados, SLAs
4. **notifications**: Email, plantillas, comunicaciones
5. **audit**: Bitácora de acciones para compliance

## Roles de Usuario

- **ADMIN**: Control total (usuarios internos)
- **SUPPORT_MANAGER**: Gestión equipos y SLAs
- **AGENT**: Atención de tickets
- **AUDITOR**: Solo lectura para auditoría
- **CLIENT_ADMIN**: Gestión organización cliente
- **CLIENT_USER**: Creación/seguimiento tickets

## Reglas de Desarrollo

### Seguridad
- Siempre validar inputs con Pydantic
- Rate limiting en endpoints críticos
- Sanitizar archivos adjuntos
- Correlation ID en logs
- CORS restrictivo

### Código
- Clean Architecture por dominios
- Async/await en toda la aplicación
- Type hints obligatorios
- Docstrings en funciones públicas
- Tests unitarios + integración

### Base de Datos
- Migrations solo con Alembic
- Constraints a nivel de BD
- Índices para queries frecuentes
- Soft deletes cuando aplique

### API
- OpenAPI bien documentado
- Versionado con prefijo /v1/
- Paginación en listados
- Responses consistentes

## Comandos Útiles

```bash
# Setup inicial
docker-compose up -d
alembic upgrade head

# Desarrollo backend
uvicorn app.main:app --reload

# Testing
pytest --cov=app tests/

# Linting
ruff check . --fix
black .
mypy .
```

Cuando trabajes en este proyecto, siempre considera la naturaleza multi-tenant y empresarial del sistema.

==================================================
ARCHIVO: .\backend\add_columns.py
==================================================
import asyncio
from sqlalchemy import text
from app.core.database import engine

async def add_columns():
    print("Starting schema update...")
    async with engine.begin() as conn:
        try:
            await conn.execute(text("ALTER TABLE tickets ADD COLUMN system_id UUID REFERENCES systems(id)"))
            print("Added system_id column")
        except Exception as e:
            print(f"Error adding system_id (might exist): {e}")
            
        try:
            await conn.execute(text("ALTER TABLE tickets ADD COLUMN category_id UUID REFERENCES categories(id)"))
            print("Added category_id column")
        except Exception as e:
            print(f"Error adding category_id (might exist): {e}")
    print("Schema update finished.")

if __name__ == "__main__":
    asyncio.run(add_columns())


==================================================
ARCHIVO: .\backend\fix_password_script.py
==================================================

import asyncio
import sys
import os

# Add parent directory to path so we can import 'app'
sys.path.append(os.path.dirname(os.path.abspath(__file__)))

from sqlalchemy import select
from app.core.database import AsyncSessionLocal
from app.models.tenant import Tenant  # Import Tenant to register it
from app.models.ticket import Ticket  # Import Ticket to register it
from app.models.user import User
from app.core.security import SecurityUtils

async def fix_password():
    async with AsyncSessionLocal() as session:
        # Find the admin user
        email = "admin@aduanasoft.com"
        result = await session.execute(select(User).where(User.email == email))
        user = result.scalar_one_or_none()
        
        if user:
            print(f"User {email} found.")
            # Reset password to 'admin123'
            new_password = "admin123"
            hashed = SecurityUtils.hash_password(new_password)
            user.password_hash = hashed
            
            try:
                await session.commit()
                print(f"Password for {email} updated successfully!")
                print(f"New password is: {new_password}")
            except Exception as e:
                await session.rollback()
                print(f"Error updating password: {e}")
        else:
            print(f"User {email} not found!")

if __name__ == "__main__":
    asyncio.run(fix_password())


==================================================
ARCHIVO: .\backend\pyproject.toml
==================================================
# ServiceManagerWeb Backend - PyProject Configuration

[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "servicemanager-backend"
version = "0.1.0"
description = "ServiceManagerWeb Backend - Mesa de Ayuda B2B"
authors = [
    {name = "Aduanasoft", email = "dev@aduanasoft.com"}
]
readme = "README.md"
requires-python = ">=3.11"
license = {text = "Proprietary"}

dependencies = [
    "fastapi==0.104.1",
    "uvicorn[standard]==0.24.0",
    "pydantic==2.5.0",
    "pydantic-settings==2.1.0",
    "sqlalchemy==2.0.23",
    "alembic==1.13.0",
    "asyncpg==0.29.0",
    "psycopg2-binary==2.9.9",
    "python-jose[cryptography]==3.3.0",
    "python-multipart==0.0.6",
    "passlib[argon2]==1.7.4",
    "argon2-cffi==23.1.0",
    "pyotp==2.9.0",
    "celery==5.3.4",
    "redis==5.0.1",
    "email-validator==2.1.0",
    "jinja2==3.1.2",
    "python-magic==0.4.27",
    "pillow==10.1.0",
    "httpx==0.25.2",
    "aiofiles==23.2.1",
    "python-dateutil==2.8.2",
    "pytz==2023.3",
    "slugify==0.0.1",
    "structlog==23.2.0",
    "prometheus-client==0.19.0"
]

[project.optional-dependencies]
dev = [
    "pytest==7.4.3",
    "pytest-asyncio==0.21.1",
    "pytest-cov==4.1.0",
    "faker==20.1.0",
    "ruff==0.1.7",
    "black==23.11.0",
    "mypy==1.7.1",
    "pre-commit==3.6.0"
]

production = [
    "gunicorn==21.2.0"
]

[tool.setuptools.packages.find]
where = ["."]
include = ["app*"]

# ===================================
# RUFF CONFIGURATION
# ===================================
[tool.ruff]
line-length = 100
target-version = "py311"
select = [
    "E",  # pycodestyle errors
    "W",  # pycodestyle warnings
    "F",  # pyflakes
    "I",  # isort
    "B",  # flake8-bugbear
    "C4", # flake8-comprehensions
    "UP", # pyupgrade
    "N",  # pep8-naming
]
ignore = [
    "E501",  # line too long, handled by black
    "B008",  # do not perform function calls in argument defaults
    "C901",  # too complex
]

[tool.ruff.per-file-ignores]
"__init__.py" = ["F401"]
"tests/**/*" = ["N802", "N803", "N806"]

[tool.ruff.isort]
known-first-party = ["app"]

# ===================================
# BLACK CONFIGURATION  
# ===================================
[tool.black]
line-length = 100
target-version = ['py311']
include = '\.pyi?$'
extend-exclude = '''
/(
  # directories
  \.eggs
  | \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | build
  | dist
)/
'''

# ===================================
# MYPY CONFIGURATION
# ===================================
[tool.mypy]
python_version = "3.11"
check_untyped_defs = true
disallow_any_generics = true
disallow_incomplete_defs = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
strict_equality = true
show_error_codes = true

[[tool.mypy.overrides]]
module = [
    "passlib.*",
    "pyotp.*",
    "magic.*",
]
ignore_missing_imports = true

# ===================================
# PYTEST CONFIGURATION
# ===================================
[tool.pytest.ini_options]
minversion = "7.0"
addopts = "-ra -q --strict-markers --strict-config"
testpaths = ["tests"]
asyncio_mode = "auto"
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "integration: marks tests as integration tests",
    "unit: marks tests as unit tests",
]

# ===================================
# COVERAGE CONFIGURATION
# ===================================
[tool.coverage.run]
source = ["app"]
omit = [
    "*/tests/*",
    "*/venv/*",
    "*/__pycache__/*",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "if self.debug:",
    "if settings.DEBUG",
    "raise AssertionError",
    "raise NotImplementedError",
    "if 0:",
    "if __name__ == .__main__.:",
    "class .*\\bProtocol\\):",
    "@(abc\\.)?abstractmethod",
]

==================================================
ARCHIVO: .\backend\README.md
==================================================
# ServiceManagerWeb Backend

FastAPI backend para el sistema de Mesa de Ayuda B2B multi-tenant.

## Estructura

```
backend/
├── app/
│   ├── main.py              # FastAPI app principal
│   ├── core/                # Configuración y utilidades core
│   │   ├── config.py        # Configuración con Pydantic Settings
│   │   ├── database.py      # SQLAlchemy async setup
│   │   ├── security.py      # JWT, hashing, 2FA
│   │   └── logging.py       # Structured logging
│   ├── models/              # Modelos SQLAlchemy
│   │   ├── tenant.py        # Modelo de tenant (multi-tenancy)
│   │   ├── user.py          # Modelo de usuario
│   │   └── ...              # Otros modelos
│   ├── api/                 # API routes
│   │   └── v1/              # API version 1
│   │       ├── router.py    # Router principal
│   │       └── endpoints/   # Endpoints por dominio
│   ├── middleware/          # Custom middleware
│   ├── services/            # Business logic
│   ├── repositories/        # Data access layer
│   ├── schemas/             # Pydantic schemas
│   └── utils/               # Utilidades compartidas
├── tests/                   # Tests unitarios e integración
├── migrations/              # Migraciones Alembic
├── requirements.txt         # Dependencias Python
└── pyproject.toml          # Configuración del proyecto
```

## Características Implementadas

### Core
- [x] FastAPI app con configuración async
- [x] Pydantic Settings para configuración
- [x] SQLAlchemy 2.0 async
- [x] Structured logging con structlog
- [x] JWT authentication con refresh tokens
- [x] 2FA con TOTP
- [x] Multi-tenancy middleware

### API
- [x] Health checks (/health, /health/detailed)
- [x] Authentication endpoints básicos
- [x] Middleware de correlation ID y tenant
- [x] Error handling centralizado
- [x] CORS configurado

### Seguridad
- [x] Argon2 password hashing
- [x] JWT con algoritmos seguros
- [x] TOTP 2FA implementation
- [x] Validation con Pydantic v2

## Quick Start

```bash
# Instalar dependencias
pip install -r requirements.txt

# Variables de entorno (copiar desde raíz del proyecto)
cp ../.env.example .env

# Ejecutar en desarrollo
uvicorn app.main:app --reload --port 8000

# O usar Docker
docker-compose up backend
```

## Testing

```bash
# Ejecutar tests
pytest

# Con coverage
pytest --cov=app tests/

# Solo tests unitarios
pytest -m "unit"

# Solo tests de integración
pytest -m "integration"
```

## Code Quality

```bash
# Linting
ruff check .

# Formateo
black .

# Type checking
mypy .

# Fix automático
ruff check . --fix
black .
```

## Desarrollo

### Agregar nuevos endpoints

1. Crear schema en `app/schemas/`
2. Crear endpoint en `app/api/v1/endpoints/`
3. Registrar router en `app/api/v1/router.py`
4. Agregar tests en `tests/`

### Modelos de base de datos

1. Crear modelo en `app/models/`
2. Importar en `app/models/__init__.py`
3. Crear migración: `alembic revision --autogenerate -m "descripción"`
4. Aplicar migración: `alembic upgrade head`

### Variables de entorno

Todas las configuraciones están en `app/core/config.py` usando Pydantic Settings.

Ver `.env.example` para todas las variables disponibles.

## Arquitectura

### Clean Architecture

- **Presentation**: FastAPI endpoints y schemas
- **Application**: Services y casos de uso
- **Domain**: Entidades y reglas de negocio
- **Infrastructure**: Repositorios, DB, external APIs

### Patrones implementados

- Repository pattern para acceso a datos
- Dependency injection con FastAPI Depends
- Unit of Work para transacciones
- Command/Query separation

## Monitoring

- Structured logging con correlation IDs
- Health checks para load balancer
- Métricas con Prometheus (TODO)
- Error tracking (TODO)

## Security Checklist

- [x] Password hashing con Argon2
- [x] JWT con secret keys seguras
- [x] CORS restrictivo
- [x] Input validation con Pydantic
- [x] SQL injection protection (SQLAlchemy)
- [x] Rate limiting (TODO: implementar)
- [x] File upload validation (TODO: implementar)
- [x] XSS protection (headers en nginx)

## Próximos pasos

1. Implementar repositorios y services
2. Completar autenticación con base de datos
3. Agregar endpoints de users y tickets
4. Implementar rate limiting
5. Agregar métricas y monitoring
6. Tests de integración completos

==================================================
ARCHIVO: .\backend\alembic\versions\add_clients_table.py
==================================================
"""
Add clients table
"""

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "add_clients_table"
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    op.create_table(
        "clients",
        sa.Column("id", sa.Integer, primary_key=True, index=True),
        sa.Column("name", sa.String, nullable=False),
        sa.Column("email", sa.String, unique=True, nullable=False),
        sa.Column("phone", sa.String, nullable=False),
    )

def downgrade():
    op.drop_table("clients")

==================================================
ARCHIVO: .\backend\app\main.py
==================================================
"""
ServiceManagerWeb Backend - FastAPI Application

Mesa de Ayuda B2B multi-tenant con Clean Architecture
"""

from fastapi import FastAPI, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import JSONResponse
from contextlib import asynccontextmanager
import structlog
import time
import uuid

from app.core.config import get_settings
from app.core.database import engine, create_tables
# Import models to register them with SQLAlchemy
from app.models.tenant import Tenant
from app.models.system import System
from app.models.category import Category
from app.models.user import User
from app.models.ticket import Ticket

from app.core.logging import setup_logging
from app.api.v1.router import api_router
from app.middleware.tenant import TenantMiddleware
from app.middleware.correlation_id import CorrelationIDMiddleware

settings = get_settings()
setup_logging()
logger = structlog.get_logger()


@asynccontextmanager
async def lifespan(app: FastAPI):
    """Lifecycle manager para la aplicación."""
    # Startup
    logger.info("Iniciando ServiceManagerWeb Backend", version=settings.API_VERSION)
    
    if settings.ENVIRONMENT == "development":
        await create_tables()
        logger.info("Tablas de base de datos verificadas")
    
    yield
    
    # Shutdown
    logger.info("Cerrando ServiceManagerWeb Backend")


# Crear aplicación FastAPI
app = FastAPI(
    title="ServiceManagerWeb API",
    description="Mesa de Ayuda B2B multi-tenant para Aduanasoft",
    version=settings.API_VERSION,
    lifespan=lifespan,
    docs_url=f"/{settings.API_VERSION}/docs" if settings.ENVIRONMENT == "development" else None,
    redoc_url=f"/{settings.API_VERSION}/redoc" if settings.ENVIRONMENT == "development" else None,
    openapi_url=f"/{settings.API_VERSION}/openapi.json"
)

# ===================================
# MIDDLEWARE
# ===================================

# CORS
cors_origins = settings.CORS_ORIGINS.split(",") if isinstance(settings.CORS_ORIGINS, str) else settings.CORS_ORIGINS
app.add_middleware(
    CORSMiddleware,
    allow_origins=cors_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# Compression
app.add_middleware(GZipMiddleware, minimum_size=1000)

# Custom middleware
app.add_middleware(CorrelationIDMiddleware)
app.add_middleware(TenantMiddleware)

# Request logging middleware
@app.middleware("http")
async def request_logging_middleware(request: Request, call_next):
    """Log todas las requests con métricas de performance."""
    start_time = time.time()
    correlation_id = getattr(request.state, "correlation_id", str(uuid.uuid4()))
    
    # Log request
    logger.info(
        "Request iniciada",
        method=request.method,
        url=str(request.url),
        correlation_id=correlation_id,
        user_agent=request.headers.get("user-agent"),
        remote_addr=request.client.host if request.client else None
    )
    
    # Process request
    response = await call_next(request)
    
    # Log response
    duration = time.time() - start_time
    logger.info(
        "Request completada",
        method=request.method,
        url=str(request.url),
        status_code=response.status_code,
        duration=f"{duration:.3f}s",
        correlation_id=correlation_id
    )
    
    # Add correlation ID to response headers
    response.headers["X-Correlation-ID"] = correlation_id
    
    return response


# ===================================
# EXCEPTION HANDLERS
# ===================================

@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
    """Handler global para excepciones no capturadas."""
    correlation_id = getattr(request.state, "correlation_id", str(uuid.uuid4()))
    
    logger.error(
        "Excepción no manejada",
        error=str(exc),
        correlation_id=correlation_id,
        url=str(request.url),
        method=request.method,
        exc_info=True
    )
    
    return JSONResponse(
        status_code=500,
        content={
            "success": False,
            "error": {
                "code": "INTERNAL_ERROR",
                "message": "Error interno del servidor"
            },
            "correlation_id": correlation_id
        }
    )


# ===================================
# ROUTES
# ===================================

# Health check endpoint
@app.get("/health")
async def health_check():
    """Health check para load balancer y monitoring."""
    return {
        "status": "healthy",
        "service": "ServiceManagerWeb API",
        "version": settings.API_VERSION,
        "environment": settings.ENVIRONMENT
    }


# Root endpoint
@app.get("/")
async def root():
    """Endpoint raíz con información básica."""
    return {
        "service": "ServiceManagerWeb API",
        "version": settings.API_VERSION,
        "docs": f"/{settings.API_VERSION}/docs",
        "environment": settings.ENVIRONMENT
    }


# API routes
app.include_router(
    api_router,
    prefix=f"/{settings.API_VERSION}",
    responses={
        400: {"description": "Bad Request"},
        401: {"description": "Unauthorized"},
        403: {"description": "Forbidden"},
        404: {"description": "Not Found"},
        422: {"description": "Validation Error"},
        500: {"description": "Internal Server Error"}
    }
)


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(
        "app.main:app",
        host="0.0.0.0",
        port=8000,
        reload=settings.ENVIRONMENT == "development"
    )

==================================================
ARCHIVO: .\backend\app\api\deps.py
==================================================
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import ValidationError

from app.core.database import get_db
from app.core.security import security
from app.core.config import get_settings
from app.models.user import User, UserRole

settings = get_settings()

# Define OAuth2 scheme here or import from auth if needed. 
# Defining here creates a separate instance which is fine as they share config.
# Ideally auth.py should import from here, but modifying auth.py is risky now.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"/{settings.API_VERSION}/auth/login")

async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
) -> User:
    payload = security.verify_token(token)
    if payload is None:
         raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Could not validate credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
    user_id: str = payload.get("sub")
    if user_id is None:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Could not validate credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
        
    result = await db.execute(select(User).where(User.id == user_id))
    user = result.scalars().first()
    
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
        
    if not user.is_active:
        raise HTTPException(status_code=400, detail="Inactive user")
        
    return user

async def get_current_active_superuser(
    current_user: User = Depends(get_current_user),
) -> User:
    if current_user.role != UserRole.ADMIN:
        raise HTTPException(
            status_code=403, detail="The user doesn't have enough privileges"
        )
    return current_user


==================================================
ARCHIVO: .\backend\app\api\v1\router.py
==================================================
"""
API v1 Router - ServiceManagerWeb

Router principal para la API v1
"""

from fastapi import APIRouter
from app.api.v1.endpoints import auth, health, tenants, users, systems, categories, clients

api_router = APIRouter()

# Health check routes
api_router.include_router(
    health.router,
    tags=["health"]
)

# Authentication routes
api_router.include_router(
    auth.router,
    prefix="/auth",
    tags=["authentication"]
)

api_router.include_router(
    tenants.router,
    prefix="/tenants",
    tags=["tenants"]
)

api_router.include_router(
    users.router,
    prefix="/users",
    tags=["users"]
)

api_router.include_router(
    systems.router,
    prefix="/systems",
    tags=["systems"]
)

api_router.include_router(
    categories.router,
    prefix="/categories",
    tags=["categories"]
)


==================================================
ARCHIVO: .\backend\app\api\v1\endpoints\auth.py
==================================================
"""
Authentication Endpoints - ServiceManagerWeb

Endpoints para autenticación y autorización
"""

from fastapi import APIRouter, HTTPException, status, Depends
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel, EmailStr
from typing import Optional
import structlog

from app.core.database import get_db
from app.core.security import security
from app.core.config import get_settings
from app.models.user import User
from app.models.tenant import Tenant

router = APIRouter()
logger = structlog.get_logger(__name__)
settings = get_settings()

# OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=f"/{settings.API_VERSION}/auth/login")


# ===================================
# PYDANTIC SCHEMAS
# ===================================

class LoginRequest(BaseModel):
    """Schema for login request."""
    email: EmailStr
    password: str
    tenant_slug: str
    totp_code: Optional[str] = None


class LoginResponse(BaseModel):
    """Schema for login response."""
    access_token: str
    refresh_token: str
    token_type: str = "bearer"
    expires_in: int
    user: dict


class RefreshTokenRequest(BaseModel):
    """Schema for refresh token request."""
    refresh_token: str


class TokenResponse(BaseModel):
    """Schema for token response."""
    access_token: str
    token_type: str = "bearer"
    expires_in: int


# ===================================
# ENDPOINTS
# ===================================

@router.post("/login", response_model=LoginResponse)
async def login(
    login_data: LoginRequest,
    db: AsyncSession = Depends(get_db)
):
    """
    Authenticate user and return access/refresh tokens.
    
    Args:
        login_data: Login credentials
        db: Database session
        
    Returns:
        LoginResponse with tokens and user info
        
    Raises:
        HTTPException: If authentication fails
    """
    logger.info(
        "Login attempt",
        email=login_data.email,
        tenant_slug=login_data.tenant_slug
    )
    
    # 1. Buscar usuario en base de datos
    query = select(User).where(User.email == login_data.email)
    result = await db.execute(query)
    user = result.scalar_one_or_none()

    # 2. Verificar usuario y contraseña
    if not user or not security.verify_password(login_data.password, user.password_hash):
        logger.warning(
            "Login failed - invalid credentials",
            email=login_data.email
        )
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Credenciales inválidas"
        )
    
    # 3. Verificar si está activo
    if not user.is_active:
        logger.warning(
            "Login failed - user inactive",
            email=login_data.email
        )
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Usuario inactivo"
        )
    
    # Create tokens
    token_data = {
        "sub": str(user.id),
        "email": user.email,
        "role": user.role.value if hasattr(user.role, "value") else user.role,
        "tenant_id": str(user.tenant_id)
    }
    
    access_token = security.create_access_token(token_data)
    refresh_token = security.create_refresh_token(token_data)
    
    logger.info(
        "Login successful",
        email=login_data.email,
        tenant_slug=login_data.tenant_slug,
        user_id=str(user.id)
    )
    
    return LoginResponse(
        access_token=access_token,
        refresh_token=refresh_token,
        expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
        user={
            "id": str(user.id),
            "email": user.email,
            "first_name": user.first_name,
            "last_name": user.last_name,
            "role": user.role,
            "tenant_id": str(user.tenant_id),
            "is_active": user.is_active,
            "is_two_factor_enabled": user.totp_enabled or False,
            "created_at": user.created_at.isoformat() if user.created_at else None
        }
    )


@router.post("/refresh", response_model=TokenResponse)
async def refresh_token(
    refresh_data: RefreshTokenRequest,
    db: AsyncSession = Depends(get_db)
):
    """
    Refresh access token using refresh token.
    
    Args:
        refresh_data: Refresh token data
        db: Database session
        
    Returns:
        New access token
        
    Raises:
        HTTPException: If refresh token is invalid
    """
    logger.info("Token refresh attempt")
    
    # Verify refresh token
    payload = security.verify_token(refresh_data.refresh_token)
    if not payload or payload.get("type") != "refresh":
        logger.warning("Token refresh failed - invalid token")
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid refresh token"
        )
    
    # TODO: Check if refresh token exists in database and is not revoked
    
    # Create new access token
    token_data = {
        "sub": payload["sub"],
        "email": payload["email"],
        "role": payload["role"],
        "tenant_id": payload["tenant_id"]
    }
    
    access_token = security.create_access_token(token_data)
    
    logger.info("Token refresh successful", user_id=payload["sub"])
    
    return TokenResponse(
        access_token=access_token,
        expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
    )


@router.post("/logout")
async def logout(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
):
    """
    Logout user and revoke refresh token.
    
    Args:
        token: Access token
        db: Database session
        
    Returns:
        Success message
    """
    logger.info("Logout attempt")
    
    # Verify token
    payload = security.verify_token(token)
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid token"
        )
    
    # TODO: Revoke refresh token in database
    
    logger.info("Logout successful", user_id=payload["sub"])
    
    return {"message": "Successfully logged out"}


@router.get("/me")
async def get_current_user(
    token: str = Depends(oauth2_scheme),
    db: AsyncSession = Depends(get_db)
):
    """
    Get current user information.
    
    Args:
        token: Access token
        db: Database session
        
    Returns:
        Current user data
        
    Raises:
        HTTPException: If token is invalid
    """
    # Verify token
    payload = security.verify_token(token)
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid token"
        )
    
    # TODO: Fetch actual user from database
    
    return {
        "id": payload["sub"],
        "email": payload["email"],
        "role": payload["role"],
        "tenant_id": payload["tenant_id"]
    }


# ===================================
# DEPENDENCIES
# ===================================

async def get_current_active_user(token: str = Depends(oauth2_scheme)):
    """
    Dependency to get current active user from token.
    
    Args:
        token: Access token
        
    Returns:
        Current user data
        
    Raises:
        HTTPException: If token is invalid or user is inactive
    """
    payload = security.verify_token(token)
    if not payload:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid token",
            headers={"WWW-Authenticate": "Bearer"},
        )
    
    # TODO: Verify user exists and is active
    
    return payload

==================================================
ARCHIVO: .\backend\app\api\v1\endpoints\categories.py
==================================================
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
import uuid

from app.core.database import get_db
from app.models.category import Category
from app.api import deps 

router = APIRouter()

class CategoryBase(BaseModel):
    name: str
    description: Optional[str] = None
    is_active: bool = True
    tenant_id: Optional[uuid.UUID] = None
    
class CategoryCreate(CategoryBase):
    pass

class CategoryUpdate(CategoryBase):
    name: Optional[str] = None
    description: Optional[str] = None
    is_active: Optional[bool] = None
    tenant_id: Optional[uuid.UUID] = None

class CategoryResponse(CategoryBase):
    id: uuid.UUID
    
    model_config = ConfigDict(from_attributes=True)

@router.get("/", response_model=List[CategoryResponse])
async def read_categories(
    skip: int = 0, 
    limit: int = 100, 
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    query = select(Category).offset(skip).limit(limit)
    result = await db.execute(query)
    return result.scalars().all()

@router.post("/", response_model=CategoryResponse)
async def create_category(
    category: CategoryCreate, 
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    db_category = Category(**category.model_dump())
    db.add(db_category)
    await db.commit()
    await db.refresh(db_category)
    return db_category


==================================================
ARCHIVO: .\backend\app\api\v1\endpoints\clients.py
==================================================
from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import text  # <--- IMPORTANTE: Necesario para consultas SQL
from app.core.database import get_db

# IMPORTANTE: Renombramos para evitar conflictos
from app.models.client import Client as ClientModel
from app.schemas.client import ClientCreate, ClientUpdate, Client as ClientSchema

router = APIRouter()

# GET: Obtener todos los clientes
@router.get("/clients", response_model=list[ClientSchema])
async def get_clients(db: AsyncSession = Depends(get_db)):
    # Corrección: Usamos text() y mappings().all()
    query = text("SELECT * FROM clients")
    result = await db.execute(query)
    return result.mappings().all()

# POST: Crear cliente
@router.post("/clients", response_model=ClientSchema)
async def create_client(client: ClientCreate, db: AsyncSession = Depends(get_db)):
    # Usamos ClientModel para guardar en BD
    new_client = ClientModel(**client.dict())
    db.add(new_client)
    await db.commit()
    await db.refresh(new_client)
    return new_client

# GET ONE: Obtener un cliente por ID
@router.get("/clients/{client_id}", response_model=ClientSchema)
async def read_client(client_id: int, db: AsyncSession = Depends(get_db)):
    db_client = await db.get(ClientModel, client_id)
    if not db_client:
        raise HTTPException(status_code=404, detail="Client not found")
    return db_client

# PUT: Actualizar cliente
@router.put("/clients/{client_id}", response_model=ClientSchema)
async def update_client(client_id: int, client: ClientUpdate, db: AsyncSession = Depends(get_db)):
    db_client = await db.get(ClientModel, client_id)
    if not db_client:
        raise HTTPException(status_code=404, detail="Client not found")
    
    for key, value in client.dict(exclude_unset=True).items():
        setattr(db_client, key, value)
    
    await db.commit()
    await db.refresh(db_client)
    return db_client

# DELETE: Borrar cliente
@router.delete("/clients/{client_id}")
async def delete_client(client_id: int, db: AsyncSession = Depends(get_db)):
    db_client = await db.get(ClientModel, client_id)
    if not db_client:
        raise HTTPException(status_code=404, detail="Client not found")
    
    await db.delete(db_client)
    await db.commit()
    return {"message": "Client deleted successfully"}

==================================================
ARCHIVO: .\backend\app\api\v1\endpoints\health.py
==================================================
"""
Health Check Endpoints - ServiceManagerWeb

Endpoints para health checks y monitoring
"""

from fastapi import APIRouter, Depends, status
from sqlalchemy.ext.asyncio import AsyncSession
import structlog

from app.core.database import get_db, check_database_health
from app.core.config import get_settings

router = APIRouter()
logger = structlog.get_logger(__name__)
settings = get_settings()


@router.get("/health")
async def health_check():
    """
    Basic health check endpoint.
    
    Returns basic service information and status.
    """
    return {
        "status": "healthy",
        "service": "ServiceManagerWeb API",
        "version": settings.API_VERSION,
        "environment": settings.ENVIRONMENT
    }


@router.get("/health/detailed")
async def detailed_health_check(db: AsyncSession = Depends(get_db)):
    """
    Detailed health check with database connectivity.
    
    Checks database connection and returns detailed status.
    """
    # Check database
    db_healthy = await check_database_health()
    
    # TODO: Add Redis health check
    # TODO: Add Celery health check
    
    overall_status = "healthy" if db_healthy else "unhealthy"
    status_code = status.HTTP_200_OK if db_healthy else status.HTTP_503_SERVICE_UNAVAILABLE
    
    health_data = {
        "status": overall_status,
        "service": "ServiceManagerWeb API", 
        "version": settings.API_VERSION,
        "environment": settings.ENVIRONMENT,
        "checks": {
            "database": "healthy" if db_healthy else "unhealthy",
            "redis": "not_implemented",
            "celery": "not_implemented"
        }
    }
    
    if not db_healthy:
        logger.error("Health check failed - database unhealthy")
    
    return health_data


@router.get("/readiness")
async def readiness_check():
    """
    Kubernetes readiness probe endpoint.
    
    Returns 200 if service is ready to accept traffic.
    """
    # For now, just return ready
    # In production, this might check for startup completion,
    # database migrations, etc.
    return {"status": "ready"}


@router.get("/liveness")
async def liveness_check():
    """
    Kubernetes liveness probe endpoint.
    
    Returns 200 if service is alive and should not be restarted.
    """
    return {"status": "alive"}

==================================================
ARCHIVO: .\backend\app\api\v1\endpoints\systems.py
==================================================
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel, ConfigDict
from typing import List, Optional
import uuid

from app.core.database import get_db
from app.models.system import System
from app.api import deps 

router = APIRouter()

class SystemBase(BaseModel):
    name: str
    description: Optional[str] = None
    is_active: bool = True
    
class SystemCreate(SystemBase):
    pass

class SystemUpdate(SystemBase):
    name: Optional[str] = None
    description: Optional[str] = None
    is_active: Optional[bool] = None

class SystemResponse(SystemBase):
    id: uuid.UUID
    
    model_config = ConfigDict(from_attributes=True)

@router.get("/", response_model=List[SystemResponse])
async def read_systems(
    skip: int = 0, 
    limit: int = 100, 
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    query = select(System).offset(skip).limit(limit)
    result = await db.execute(query)
    return result.scalars().all()

@router.post("/", response_model=SystemResponse)
async def create_system(
    system: SystemCreate, 
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    db_system = System(**system.model_dump())
    db.add(db_system)
    await db.commit()
    await db.refresh(db_system)
    return db_system


==================================================
ARCHIVO: .\backend\app\api\v1\endpoints\tenants.py
==================================================
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel, ConfigDict, EmailStr
from typing import List, Optional
import uuid

from app.core.database import get_db
from app.models.tenant import Tenant, TenantStatus
from app.api import deps 

router = APIRouter()

class TenantBase(BaseModel):
    name: str
    slug: str
    domain: Optional[str] = None
    contact_email: Optional[EmailStr] = None
    
class TenantCreate(TenantBase):
    pass

class TenantUpdate(BaseModel):
    name: Optional[str] = None
    slug: Optional[str] = None
    domain: Optional[str] = None
    contact_email: Optional[EmailStr] = None
    status: Optional[TenantStatus] = None

class TenantResponse(TenantBase):
    id: uuid.UUID
    status: TenantStatus
    
    model_config = ConfigDict(from_attributes=True)

@router.get("/", response_model=List[TenantResponse])
async def read_tenants(
    skip: int = 0, 
    limit: int = 100, 
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    query = select(Tenant).offset(skip).limit(limit)
    result = await db.execute(query)
    return result.scalars().all()

@router.post("/", response_model=TenantResponse)
async def create_tenant(
    tenant: TenantCreate, 
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    # Check existing slug
    query = select(Tenant).where(Tenant.slug == tenant.slug)
    result = await db.execute(query)
    if result.scalar_one_or_none():
        raise HTTPException(status_code=400, detail="Tenant slug already exists")
        
    db_tenant = Tenant(**tenant.model_dump())
    db.add(db_tenant)
    await db.commit()
    await db.refresh(db_tenant)
    return db_tenant

@router.get("/{tenant_id}", response_model=TenantResponse)
async def read_tenant(
    tenant_id: uuid.UUID,
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    tenant = await db.get(Tenant, tenant_id)
    if not tenant:
        raise HTTPException(status_code=404, detail="Tenant not found")
    return tenant

@router.put("/{tenant_id}", response_model=TenantResponse)
async def update_tenant(
    tenant_id: uuid.UUID,
    tenant_in: TenantUpdate,
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    tenant = await db.get(Tenant, tenant_id)
    if not tenant:
        raise HTTPException(status_code=404, detail="Tenant not found")
        
    update_data = tenant_in.model_dump(exclude_unset=True)
    for field, value in update_data.items():
        setattr(tenant, field, value)
        
    db.add(tenant)
    await db.commit()
    await db.refresh(tenant)
    return tenant


==================================================
ARCHIVO: .\backend\app\api\v1\endpoints\users.py
==================================================
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from pydantic import BaseModel, ConfigDict, EmailStr
from typing import List, Optional
import uuid

from app.core.database import get_db
from app.core.security import security
from app.models.user import User, UserRole
from app.api import deps 

router = APIRouter()

class UserBase(BaseModel):
    email: EmailStr
    first_name: str
    last_name: str
    role: UserRole
    is_active: bool = True
    tenant_id: Optional[uuid.UUID] = None

class UserCreate(UserBase):
    password: str

class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
    first_name: Optional[str] = None
    last_name: Optional[str] = None
    role: Optional[UserRole] = None
    is_active: Optional[bool] = None
    password: Optional[str] = None # Optional password update

class UserResponse(UserBase):
    id: uuid.UUID
    
    model_config = ConfigDict(from_attributes=True)

@router.get("/", response_model=List[UserResponse])
async def read_users(
    skip: int = 0, 
    limit: int = 100, 
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    query = select(User).offset(skip).limit(limit)
    result = await db.execute(query)
    return result.scalars().all()

@router.post("/", response_model=UserResponse)
async def create_user(
    user: UserCreate, 
    db: AsyncSession = Depends(get_db),
    current_user = Depends(deps.get_current_active_superuser)
):
    query = select(User).where(User.email == user.email)
    result = await db.execute(query)
    if result.scalar_one_or_none():
        raise HTTPException(status_code=400, detail="Email already registered")
        
    user_data = user.model_dump(exclude={"password"})
    password_hash = security.get_password_hash(user.password)
    
    db_user = User(**user_data, password_hash=password_hash)
    db.add(db_user)
    await db.commit()
    await db.refresh(db_user)
    return db_user


==================================================
ARCHIVO: .\backend\app\core\config.py
==================================================
"""
Core Configuration - ServiceManagerWeb

Configuración centralizada usando Pydantic Settings v2
"""

from functools import lru_cache
from typing import List, Optional
from pydantic_settings import BaseSettings
from pydantic import field_validator, Field
import os


class Settings(BaseSettings):
    """Configuración de la aplicación."""
    
    model_config = {
        "env_file": ".env",
        "env_file_encoding": "utf-8",
        "case_sensitive": False
    }
    
    # ===================================
    # GENERAL
    # ===================================
    ENVIRONMENT: str = Field(default="development", env="ENVIRONMENT")
    DEBUG: bool = Field(default=False, env="DEBUG")
    SECRET_KEY: str = Field(..., env="SECRET_KEY")
    API_VERSION: str = Field(default="v1", env="API_VERSION")
    
    # ===================================
    # DATABASE
    # ===================================
    DATABASE_URL: str = Field(..., env="DATABASE_URL")
    
    # ===================================
    # REDIS
    # ===================================
    REDIS_URL: str = Field(..., env="REDIS_URL")
    
    # ===================================
    # JWT AUTHENTICATION
    # ===================================
    JWT_SECRET_KEY: str = Field(..., env="JWT_SECRET_KEY")
    JWT_ALGORITHM: str = Field(default="HS256", env="JWT_ALGORITHM")
    ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=60, env="ACCESS_TOKEN_EXPIRE_MINUTES")
    REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7, env="REFRESH_TOKEN_EXPIRE_DAYS")
    
    # ===================================
    # CORS
    # ===================================
    CORS_ORIGINS: str = Field(
        default="http://localhost:3000,http://localhost:3001",
        env="CORS_ORIGINS"
    )
    
    # ===================================
    # EMAIL
    # ===================================
    SMTP_HOST: str = Field(default="localhost", env="SMTP_HOST")
    SMTP_PORT: int = Field(default=587, env="SMTP_PORT")
    SMTP_USER: Optional[str] = Field(default=None, env="SMTP_USER")
    SMTP_PASSWORD: Optional[str] = Field(default=None, env="SMTP_PASSWORD")
    SMTP_USE_TLS: bool = Field(default=True, env="SMTP_USE_TLS")
    SMTP_USE_SSL: bool = Field(default=False, env="SMTP_USE_SSL")
    
    DEFAULT_FROM_EMAIL: str = Field(default="noreply@servicemanager.local", env="DEFAULT_FROM_EMAIL")
    DEFAULT_FROM_NAME: str = Field(default="ServiceManager", env="DEFAULT_FROM_NAME")
    
    # ===================================
    # FILE UPLOADS
    # ===================================
    MAX_UPLOAD_SIZE_MB: int = Field(default=10, env="MAX_UPLOAD_SIZE_MB")
    ALLOWED_FILE_EXTENSIONS: List[str] = Field(
        default=["pdf", "jpg", "jpeg", "png", "doc", "docx", "xls", "xlsx", "txt"],
        env="ALLOWED_FILE_EXTENSIONS"
    )
    UPLOAD_PATH: str = Field(default="/app/uploads", env="UPLOAD_PATH")
    
    @field_validator("ALLOWED_FILE_EXTENSIONS", mode='before')
    @classmethod
    def validate_file_extensions(cls, v):
        if isinstance(v, str):
            return [ext.strip().lower() for ext in v.split(",")]
        return [ext.lower() for ext in v]
    
    # ===================================
    # SECURITY
    # ===================================
    RATE_LIMIT_ENABLED: bool = Field(default=True, env="RATE_LIMIT_ENABLED")
    PASSWORD_MIN_LENGTH: int = Field(default=8, env="PASSWORD_MIN_LENGTH")
    
    # Argon2 settings
    ARGON2_TIME_COST: int = Field(default=3, env="ARGON2_TIME_COST")
    ARGON2_MEMORY_COST: int = Field(default=65536, env="ARGON2_MEMORY_COST")
    ARGON2_PARALLELISM: int = Field(default=4, env="ARGON2_PARALLELISM")
    
    # ===================================
    # LOGGING
    # ===================================
    LOG_LEVEL: str = Field(default="INFO", env="LOG_LEVEL")
    LOG_FORMAT: str = Field(default="json", env="LOG_FORMAT")
    LOG_FILE: Optional[str] = Field(default=None, env="LOG_FILE")
    
    # ===================================
    # FRONTEND URLS
    # ===================================
    CLIENT_FRONTEND_URL: str = Field(default="http://localhost:3000", env="CLIENT_FRONTEND_URL")
    INTERNAL_FRONTEND_URL: str = Field(default="http://localhost:3001", env="INTERNAL_FRONTEND_URL")
    
    # ===================================
    # HEALTH CHECKS
    # ===================================
    HEALTH_CHECK_TIMEOUT: int = Field(default=30, env="HEALTH_CHECK_TIMEOUT")
    
    # ===================================
    # CELERY
    # ===================================
    CELERY_BROKER_URL: str = Field(..., env="CELERY_BROKER_URL")
    CELERY_RESULT_BACKEND: str = Field(..., env="CELERY_RESULT_BACKEND")
    
    def is_production(self) -> bool:
        """Check if environment is production."""
        return self.ENVIRONMENT.lower() == "production"
    
    def is_development(self) -> bool:
        """Check if environment is development."""
        return self.ENVIRONMENT.lower() == "development"
    
    def is_testing(self) -> bool:
        """Check if environment is testing."""
        return self.ENVIRONMENT.lower() == "testing"


@lru_cache()
def get_settings() -> Settings:
    """
    Get cached settings instance.
    
    Using lru_cache to create a singleton pattern for settings.
    """
    return Settings()

==================================================
ARCHIVO: .\backend\app\core\database.py
==================================================
"""
Database Configuration - ServiceManagerWeb

SQLAlchemy 2.0 async setup con PostgreSQL
"""

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, DateTime, func
from typing import AsyncGenerator
import uuid
from datetime import datetime

from app.core.config import get_settings

settings = get_settings()

# Create async engine
engine = create_async_engine(
    settings.DATABASE_URL,
    echo=settings.DEBUG,
    pool_size=5,
    max_overflow=10,
    pool_pre_ping=True,  # Verify connections before use
    pool_recycle=3600,   # Recycle connections after 1 hour
)

# Create session factory
AsyncSessionLocal = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,
    autoflush=True,
    autocommit=False
)


class Base(DeclarativeBase):
    """Base class para todos los modelos SQLAlchemy."""
    
    # Columnas comunes para auditoría
    id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
    created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
    updated_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True), 
        server_default=func.now(),
        onupdate=func.now()
    )


async def get_db() -> AsyncGenerator[AsyncSession, None]:
    """
    Dependency para obtener sesión de base de datos.
    
    Yields:
        AsyncSession: Sesión de base de datos
    """
    async with AsyncSessionLocal() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()


async def create_tables():
    """Crear todas las tablas en desarrollo."""
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)


async def drop_tables():
    """Eliminar todas las tablas (solo para testing)."""
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)


# Health check function
async def check_database_health() -> bool:
    """
    Verificar conectividad con la base de datos.
    
    Returns:
        bool: True si la conexión es exitosa
    """
    try:
        async with AsyncSessionLocal() as session:
            await session.execute("SELECT 1")
            return True
    except Exception:
        return False

==================================================
ARCHIVO: .\backend\app\core\logging.py
==================================================
"""
Structured Logging Configuration - ServiceManagerWeb

Configuración de logging estructurado con structlog
"""

import logging
import logging.config
import sys
from typing import Any, Dict
import structlog
from app.core.config import get_settings

settings = get_settings()


def add_correlation_id(logger: Any, method_name: str, event_dict: Dict[str, Any]) -> Dict[str, Any]:
    """Agregar correlation ID a los logs si está disponible."""
    # En un contexto de request real, esto vendría del middleware
    # Por ahora es un placeholder
    return event_dict


def configure_structlog():
    """Configurar structlog para logging estructurado."""
    
    processors = [
        # Add the log level and a timestamp to the event_dict
        structlog.stdlib.filter_by_level,
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.UnicodeDecoder(),
        add_correlation_id,
    ]
    
    if settings.LOG_FORMAT == "json":
        processors.append(structlog.processors.JSONRenderer())
    else:
        processors.append(structlog.dev.ConsoleRenderer())
    
    structlog.configure(
        processors=processors,
        wrapper_class=structlog.stdlib.BoundLogger,
        logger_factory=structlog.stdlib.LoggerFactory(),
        context_class=dict,
        cache_logger_on_first_use=True,
    )


def setup_logging():
    """Configurar el sistema de logging completo."""
    
    # Configure structlog
    configure_structlog()
    
    # Configure standard library logging
    logging_config = {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "json": {
                "()": structlog.stdlib.ProcessorFormatter,
                "processor": structlog.processors.JSONRenderer(),
            },
            "console": {
                "()": structlog.stdlib.ProcessorFormatter,
                "processor": structlog.dev.ConsoleRenderer(colors=True),
            },
        },
        "handlers": {
            "console": {
                "level": settings.LOG_LEVEL,
                "class": "logging.StreamHandler",
                "stream": sys.stdout,
                "formatter": "json" if settings.LOG_FORMAT == "json" else "console",
            },
        },
        "loggers": {
            "": {  # root logger
                "handlers": ["console"],
                "level": settings.LOG_LEVEL,
                "propagate": False,
            },
            "uvicorn": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False,
            },
            "uvicorn.error": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False,
            },
            "uvicorn.access": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False,
            },
            "sqlalchemy": {
                "handlers": ["console"],
                "level": "WARNING",
                "propagate": False,
            },
            "celery": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False,
            },
        },
    }
    
    # Add file handler if specified
    if settings.LOG_FILE:
        logging_config["handlers"]["file"] = {
            "level": settings.LOG_LEVEL,
            "class": "logging.handlers.RotatingFileHandler",
            "filename": settings.LOG_FILE,
            "maxBytes": 10 * 1024 * 1024,  # 10MB
            "backupCount": 5,
            "formatter": "json",
        }
        
        # Add file handler to all loggers
        for logger_config in logging_config["loggers"].values():
            logger_config["handlers"].append("file")
    
    logging.config.dictConfig(logging_config)


# Convenience function to get logger
def get_logger(name: str = None) -> structlog.BoundLogger:
    """
    Get a configured structlog logger.
    
    Args:
        name: Logger name (optional)
        
    Returns:
        Configured structlog logger
    """
    return structlog.get_logger(name)

==================================================
ARCHIVO: .\backend\app\core\security.py
==================================================
"""
Security Utilities - ServiceManagerWeb

Funciones de seguridad para autenticación y autorización
"""

from datetime import datetime, timedelta
from typing import Optional, Union, Dict, Any
from passlib.context import CryptContext
from passlib.handlers.argon2 import argon2
from jose import JWTError, jwt
import pyotp
import secrets
import base64
import struct

from app.core.config import get_settings

settings = get_settings()

# Password hashing context
pwd_context = CryptContext(
    schemes=["argon2"],
    deprecated="auto",
    argon2__time_cost=settings.ARGON2_TIME_COST,
    argon2__memory_cost=settings.ARGON2_MEMORY_COST,
    argon2__parallelism=settings.ARGON2_PARALLELISM,
)


class SecurityUtils:
    """Utilidades de seguridad centralizadas."""
    
    @staticmethod
    def hash_password(password: str) -> str:
        """
        Hash a password using Argon2.
        
        Args:
            password: Plain text password
            
        Returns:
            Hashed password
        """
        return pwd_context.hash(password)
    
    @staticmethod
    def verify_password(plain_password: str, hashed_password: str) -> bool:
        """
        Verify a password against its hash.
        
        Args:
            plain_password: Plain text password
            hashed_password: Hashed password
            
        Returns:
            True if password matches
        """
        return pwd_context.verify(plain_password, hashed_password)
    
    @staticmethod
    def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
        """
        Create a JWT access token.
        
        Args:
            data: Token payload
            expires_delta: Token expiration time
            
        Returns:
            JWT token string
        """
        to_encode = data.copy()
        
        if expires_delta:
            expire = datetime.utcnow() + expires_delta
        else:
            expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
        
        to_encode.update({"exp": expire})
        
        encoded_jwt = jwt.encode(
            to_encode, 
            settings.JWT_SECRET_KEY, 
            algorithm=settings.JWT_ALGORITHM
        )
        
        return encoded_jwt
    
    @staticmethod
    def create_refresh_token(data: Dict[str, Any]) -> str:
        """
        Create a JWT refresh token.
        
        Args:
            data: Token payload
            
        Returns:
            JWT refresh token string
        """
        to_encode = data.copy()
        expire = datetime.utcnow() + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS)
        to_encode.update({"exp": expire, "type": "refresh"})
        
        encoded_jwt = jwt.encode(
            to_encode,
            settings.JWT_SECRET_KEY,
            algorithm=settings.JWT_ALGORITHM
        )
        
        return encoded_jwt
    
    @staticmethod
    def verify_token(token: str) -> Optional[Dict[str, Any]]:
        """
        Verify and decode a JWT token.
        
        Args:
            token: JWT token string
            
        Returns:
            Token payload if valid, None otherwise
        """
        try:
            payload = jwt.decode(
                token,
                settings.JWT_SECRET_KEY,
                algorithms=[settings.JWT_ALGORITHM]
            )
            return payload
        except JWTError:
            return None
    
    @staticmethod
    def generate_totp_secret() -> str:
        """
        Generate a base32-encoded secret for TOTP.
        
        Returns:
            Base32 encoded secret
        """
        return pyotp.random_base32()
    
    @staticmethod
    def generate_totp_uri(secret: str, email: str, issuer_name: str = "ServiceManager") -> str:
        """
        Generate TOTP URI for QR code.
        
        Args:
            secret: Base32 encoded secret
            email: User email
            issuer_name: Application name
            
        Returns:
            TOTP URI
        """
        totp = pyotp.TOTP(secret)
        return totp.provisioning_uri(
            name=email,
            issuer_name=issuer_name
        )
    
    @staticmethod
    def verify_totp(secret: str, token: str, window: int = 1) -> bool:
        """
        Verify a TOTP token.
        
        Args:
            secret: Base32 encoded secret
            token: TOTP token
            window: Time window tolerance
            
        Returns:
            True if token is valid
        """
        totp = pyotp.TOTP(secret)
        return totp.verify(token, valid_window=window)
    
    @staticmethod
    def generate_backup_codes(count: int = 8) -> list[str]:
        """
        Generate backup codes for 2FA.
        
        Args:
            count: Number of codes to generate
            
        Returns:
            List of backup codes
        """
        codes = []
        for _ in range(count):
            code = secrets.token_hex(4).upper()
            # Format as XXXX-XXXX
            formatted_code = f"{code[:4]}-{code[4:]}"
            codes.append(formatted_code)
        return codes
    
    @staticmethod
    def hash_token(token: str) -> str:
        """
        Hash a token for secure storage.
        
        Args:
            token: Token to hash
            
        Returns:
            Hashed token
        """
        return pwd_context.hash(token)
    
    @staticmethod
    def verify_hashed_token(token: str, hashed_token: str) -> bool:
        """
        Verify a token against its hash.
        
        Args:
            token: Plain token
            hashed_token: Hashed token
            
        Returns:
            True if token matches
        """
        return pwd_context.verify(token, hashed_token)
    
    @staticmethod
    def generate_secure_token(length: int = 32) -> str:
        """
        Generate a cryptographically secure random token.
        
        Args:
            length: Token length in bytes
            
        Returns:
            URL-safe base64 encoded token
        """
        token = secrets.token_bytes(length)
        return base64.urlsafe_b64encode(token).decode('utf-8').rstrip('=')
    
    @staticmethod
    def is_strong_password(password: str) -> tuple[bool, list[str]]:
        """
        Check if password meets security requirements.
        
        Args:
            password: Password to check
            
        Returns:
            Tuple of (is_valid, list_of_issues)
        """
        issues = []
        
        if len(password) < settings.PASSWORD_MIN_LENGTH:
            issues.append(f"Password must be at least {settings.PASSWORD_MIN_LENGTH} characters long")
        
        if not any(c.islower() for c in password):
            issues.append("Password must contain at least one lowercase letter")
        
        if not any(c.isupper() for c in password):
            issues.append("Password must contain at least one uppercase letter")
        
        if not any(c.isdigit() for c in password):
            issues.append("Password must contain at least one digit")
        
        if not any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password):
            issues.append("Password must contain at least one special character")
        
        return len(issues) == 0, issues


# Create singleton instance
security = SecurityUtils()

==================================================
ARCHIVO: .\backend\app\middleware\correlation_id.py
==================================================
"""
Correlation ID Middleware - ServiceManagerWeb

Middleware para rastrear requests con correlation ID
"""

from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
import uuid
import structlog

logger = structlog.get_logger(__name__)


class CorrelationIDMiddleware(BaseHTTPMiddleware):
    """
    Middleware para manejar correlation IDs.
    
    Extrae el correlation ID del header X-Correlation-ID o genera uno nuevo.
    Lo almacena en el estado de la request para uso en logs y responses.
    """
    
    async def dispatch(self, request: Request, call_next) -> Response:
        """Process request and add correlation ID."""
        
        # Extract or generate correlation ID
        correlation_id = request.headers.get("X-Correlation-ID")
        if not correlation_id:
            correlation_id = str(uuid.uuid4())
        
        # Store in request state
        request.state.correlation_id = correlation_id
        
        # Add to structlog context
        with structlog.contextvars.bound_contextvars(
            correlation_id=correlation_id,
            path=request.url.path,
            method=request.method
        ):
            response = await call_next(request)
        
        # Add correlation ID to response headers
        response.headers["X-Correlation-ID"] = correlation_id
        
        return response

==================================================
ARCHIVO: .\backend\app\middleware\tenant.py
==================================================
"""
Tenant Middleware - ServiceManagerWeb

Middleware para manejo de multi-tenancy
"""

from fastapi import Request, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
import structlog

logger = structlog.get_logger(__name__)


class TenantMiddleware(BaseHTTPMiddleware):
    """
    Middleware para extraer y validar información del tenant.
    
    Extrae el tenant_id del header X-Tenant-ID y lo almacena 
    en el estado de la request para uso posterior.
    """
    
    # Rutas que no requieren tenant
    EXCLUDED_PATHS = {
        "/health",
        "/",
        "/v1/auth/login",
        "/docs",
        "/openapi.json",
        "/redoc"
    }
    
    async def dispatch(self, request: Request, call_next) -> Response:
        """Process request and add tenant information."""
        
        # Skip tenant validation for excluded paths
        if request.url.path in self.EXCLUDED_PATHS or request.url.path.startswith("/docs"):
            return await call_next(request)
        
        # Extract tenant from header
        tenant_id = request.headers.get("X-Tenant-ID")
        tenant_slug = request.headers.get("X-Tenant-Slug")
        
        # For now, we'll be more permissive in development
        # In production, tenant should be strictly required
        if not tenant_id and not tenant_slug:
            logger.warning(
                "Request without tenant information",
                path=request.url.path,
                method=request.method
            )
            # For now, continue without tenant for development
            # raise HTTPException(
            #     status_code=status.HTTP_400_BAD_REQUEST,
            #     detail="Tenant information required (X-Tenant-ID or X-Tenant-Slug header)"
            # )
        
        # Store tenant info in request state
        request.state.tenant_id = tenant_id
        request.state.tenant_slug = tenant_slug
        
        # TODO: Validate tenant exists and is active
        # This would involve a database query which we'll implement later
        
        logger.debug(
            "Tenant middleware processed",
            tenant_id=tenant_id,
            tenant_slug=tenant_slug,
            path=request.url.path
        )
        
        return await call_next(request)

==================================================
ARCHIVO: .\backend\app\models\category.py
==================================================

"""
Category Model - ServiceManagerWeb
"""
from sqlalchemy import String, Text, Boolean, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID
from typing import List, Optional
import uuid

from app.core.database import Base

class Category(Base):
    __tablename__ = "categories"
    
    name: Mapped[str] = mapped_column(String(100), nullable=False)
    description: Mapped[Optional[str]] = mapped_column(Text)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    
    # Optional: Tenant specific categories?
    tenant_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=True)
    
    # Relationships
    tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="category")
    tenant: Mapped["Tenant"] = relationship("Tenant") # Assuming Tenant model is imported

    def __repr__(self) -> str:
        return f"<Category(id={self.id}, name='{self.name}')>"


==================================================
ARCHIVO: .\backend\app\models\client.py
==================================================
from sqlalchemy import Column, Integer, String
from app.core.database import Base

class Client(Base):
    __tablename__ = "clients"

    id = Column(Integer, primary_key=True, index=True)
    clave = Column(String, nullable=False)
    tipo_cliente = Column(String, nullable=False)
    nombre = Column(String, nullable=False)
    pais = Column(String, nullable=False)
    estado = Column(String, nullable=False)
    ciudad = Column(String, nullable=False)
    direccion = Column(String, nullable=False)
    numero_ext = Column(String, nullable=True)
    cp = Column(String, nullable=True)
    colonia = Column(String, nullable=True)
    lada = Column(String, nullable=True)
    telefono1 = Column(String, nullable=True)
    telefono2 = Column(String, nullable=True)
    tel_directo = Column(String, nullable=True)
    ext = Column(String, nullable=True)
    fax = Column(String, nullable=True)
    horario = Column(String, nullable=True)
    pagina = Column(String, nullable=True)
    correo = Column(String, nullable=True)
    medio_publicidad = Column(String, nullable=True)
    nacionalidad = Column(String, nullable=True)
    logo = Column(String, nullable=True)

==================================================
ARCHIVO: .\backend\app\models\system.py
==================================================

"""
System Model - ServiceManagerWeb
"""
from sqlalchemy import String, Text, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from typing import List, Optional
import uuid

from app.core.database import Base

class System(Base):
    __tablename__ = "systems"
    
    name: Mapped[str] = mapped_column(String(100), nullable=False)
    description: Mapped[Optional[str]] = mapped_column(Text)
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    
    # Relationships
    # If we want tickets to link to systems, we will add relationship in Ticket later or now.
    # We will assume Ticket links to System.
    tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="system")

    def __repr__(self) -> str:
        return f"<System(id={self.id}, name='{self.name}')>"


==================================================
ARCHIVO: .\backend\app\models\tenant.py
==================================================
"""
Tenant Model - ServiceManagerWeb

Modelo para organizaciones cliente (multi-tenancy)
"""

from sqlalchemy import String, Integer, Text, Boolean, ARRAY
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, ENUM
from typing import List, Optional
import enum
import uuid

from app.core.database import Base


class TenantStatus(str, enum.Enum):
    """Estados de un tenant."""
    ACTIVE = "active"
    SUSPENDED = "suspended"
    INACTIVE = "inactive"


class Tenant(Base):
    """Modelo de Tenant (Organización cliente)."""
    
    __tablename__ = "tenants"
    
    # Información básica
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    slug: Mapped[str] = mapped_column(String(100), unique=True, nullable=False)
    domain: Mapped[Optional[str]] = mapped_column(String(255))
    logo_url: Mapped[Optional[str]] = mapped_column(String(500))
    
    # Contacto
    contact_email: Mapped[Optional[str]] = mapped_column(String(320))
    contact_phone: Mapped[Optional[str]] = mapped_column(String(20))
    address: Mapped[Optional[str]] = mapped_column(Text)
    
    # Configuración regional
    timezone: Mapped[str] = mapped_column(String(50), default="UTC")
    locale: Mapped[str] = mapped_column(String(10), default="es-ES")
    
    # Límites y configuración
    max_users: Mapped[int] = mapped_column(Integer, default=50)
    max_storage_mb: Mapped[int] = mapped_column(Integer, default=1024)
    allowed_file_types: Mapped[List[str]] = mapped_column(
        ARRAY(String),
        default=["pdf", "jpg", "jpeg", "png", "doc", "docx", "xls", "xlsx", "txt"]
    )
    
    # Estado
    status: Mapped[TenantStatus] = mapped_column(
        String(20), 
        default=TenantStatus.ACTIVE
    )
    
    # Relaciones
    users: Mapped[List["User"]] = relationship("User", back_populates="tenant")
    tickets: Mapped[List["Ticket"]] = relationship("Ticket", back_populates="tenant")
    
    def __repr__(self) -> str:
        return f"<Tenant(id={self.id}, name='{self.name}', slug='{self.slug}')>"
    
    @property
    def is_active(self) -> bool:
        """Check if tenant is active."""
        return self.status == TenantStatus.ACTIVE

==================================================
ARCHIVO: .\backend\app\models\ticket.py
==================================================
"""
Ticket Model - ServiceManagerWeb
"""
from sqlalchemy import String, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, ENUM
from typing import Optional
import enum
import uuid

from app.core.database import Base

class TicketStatus(str, enum.Enum):
    NEW = "NEW"
    TRIAGE = "TRIAGE"
    IN_PROGRESS = "IN_PROGRESS"
    WAITING_FOR_CLIENT = "WAITING_FOR_CLIENT"
    RESOLVED = "RESOLVED"
    CLOSED = "CLOSED"
    REOPENED = "REOPENED"

class TicketPriority(str, enum.Enum):
    LOW = "LOW"
    MEDIUM = "MEDIUM"
    HIGH = "HIGH"
    URGENT = "URGENT"

class Ticket(Base):
    __tablename__ = "tickets"
    
    # Note: id, created_at, updated_at are inherited from Base
    
    tenant_id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
    
    ticket_number: Mapped[str] = mapped_column(String(20), nullable=False)
    subject: Mapped[str] = mapped_column(String(255), nullable=False)
    description: Mapped[str] = mapped_column(Text, nullable=False)
    
    status: Mapped[TicketStatus] = mapped_column(ENUM(TicketStatus, name="ticket_status_enum", create_type=False), default=TicketStatus.NEW)
    priority: Mapped[TicketPriority] = mapped_column(ENUM(TicketPriority, name="ticket_priority_enum", create_type=False), default=TicketPriority.MEDIUM)
    
    # Foreign Keys
    created_by: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
    assigned_to: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True)
    
    system_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("systems.id"), nullable=True)
    category_id: Mapped[Optional[uuid.UUID]] = mapped_column(UUID(as_uuid=True), ForeignKey("categories.id"), nullable=True)

    # Relationships
    tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="tickets")
    
    system: Mapped["System"] = relationship("System", back_populates="tickets")
    category: Mapped["Category"] = relationship("Category", back_populates="tickets")
    
    created_by_user: Mapped["User"] = relationship(
        "User", 
        foreign_keys=[created_by], 
        back_populates="created_tickets"
    )
    
    assigned_to_user: Mapped[Optional["User"]] = relationship(
        "User", 
        foreign_keys=[assigned_to], 
        back_populates="assigned_tickets"
    )


==================================================
ARCHIVO: .\backend\app\models\user.py
==================================================
"""
User Model - ServiceManagerWeb

Modelo para usuarios del sistema (internos y clientes)
"""

from sqlalchemy import String, Boolean, DateTime, ForeignKey, Text, ARRAY
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.dialects.postgresql import UUID, ENUM
from typing import Optional, List
import enum
import uuid
from datetime import datetime

from app.core.database import Base


class UserRole(str, enum.Enum):
    """Roles de usuario en el sistema."""
    # Staff interno
    ADMIN = "ADMIN"                    # Control total
    SUPPORT_MANAGER = "SUPPORT_MANAGER" # Gestión de equipos y SLAs
    AGENT = "AGENT"                    # Atención de tickets
    AUDITOR = "AUDITOR"                # Solo lectura para auditoría
    
    # Clientes
    CLIENT_ADMIN = "CLIENT_ADMIN"      # Admin de organización cliente
    CLIENT_USER = "CLIENT_USER"        # Usuario final cliente


class User(Base):
    """Modelo de Usuario."""
    
    __tablename__ = "users"
    
    # Relación con tenant
    tenant_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        ForeignKey("tenants.id", ondelete="CASCADE"),
        nullable=False
    )
    
    # Información básica
    email: Mapped[str] = mapped_column(String(320), nullable=False)
    first_name: Mapped[str] = mapped_column(String(100), nullable=False)
    last_name: Mapped[str] = mapped_column(String(100), nullable=False)
    avatar_url: Mapped[Optional[str]] = mapped_column(String(500))
    
    # Autenticación
    password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
    role: Mapped[UserRole] = mapped_column(ENUM(UserRole), nullable=False)
    
    # 2FA (opcional para staff interno)
    totp_secret: Mapped[Optional[str]] = mapped_column(String(32))
    totp_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
    backup_codes: Mapped[Optional[List[str]]] = mapped_column(ARRAY(String))
    
    # Estado
    is_active: Mapped[bool] = mapped_column(Boolean, default=True)
    email_verified: Mapped[bool] = mapped_column(Boolean, default=False)
    last_login: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
    last_activity: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
    
    # Preferencias
    language: Mapped[str] = mapped_column(String(10), default="es")
    timezone: Mapped[str] = mapped_column(String(50), default="UTC")
    notifications_email: Mapped[bool] = mapped_column(Boolean, default=True)
    
    # Relaciones
    tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="users")
    created_tickets: Mapped[List["Ticket"]] = relationship(
        "Ticket", 
        back_populates="created_by_user",
        foreign_keys="Ticket.created_by"
    )
    assigned_tickets: Mapped[List["Ticket"]] = relationship(
        "Ticket",
        back_populates="assigned_to_user", 
        foreign_keys="Ticket.assigned_to"
    )
    
    # Unique constraint por tenant
    __table_args__ = (
        {"postgresql_tablespace": "users"},
    )
    
    def __repr__(self) -> str:
        return f"<User(id={self.id}, email='{self.email}', role='{self.role}')>"
    
    @property
    def full_name(self) -> str:
        """Get user's full name."""
        return f"{self.first_name} {self.last_name}"
    
    @property
    def is_staff(self) -> bool:
        """Check if user is internal staff."""
        return self.role in [
            UserRole.ADMIN,
            UserRole.SUPPORT_MANAGER,
            UserRole.AGENT,
            UserRole.AUDITOR
        ]
    
    @property
    def is_client(self) -> bool:
        """Check if user is a client."""
        return self.role in [
            UserRole.CLIENT_ADMIN,
            UserRole.CLIENT_USER
        ]
    
    @property
    def can_manage_users(self) -> bool:
        """Check if user can manage other users."""
        return self.role in [
            UserRole.ADMIN,
            UserRole.SUPPORT_MANAGER,
            UserRole.CLIENT_ADMIN
        ]
    
    @property
    def can_manage_tickets(self) -> bool:
        """Check if user can manage tickets."""
        return self.role in [
            UserRole.ADMIN,
            UserRole.SUPPORT_MANAGER,
            UserRole.AGENT
        ]
    
    @property
    def requires_2fa(self) -> bool:
        """Check if 2FA is required for this user."""
        # 2FA opcional para staff interno, no requerido para clientes
        return self.is_staff

==================================================
ARCHIVO: .\backend\app\schemas\client.py
==================================================
from pydantic import BaseModel

class ClientBase(BaseModel):
    clave: str
    tipo_cliente: str
    nombre: str
    pais: str
    estado: str
    ciudad: str
    direccion: str
    numero_ext: str | None = None
    cp: str | None = None
    colonia: str | None = None
    lada: str | None = None
    telefono1: str | None = None
    telefono2: str | None = None
    tel_directo: str | None = None
    ext: str | None = None
    fax: str | None = None
    horario: str | None = None
    pagina: str | None = None
    correo: str | None = None
    medio_publicidad: str | None = None
    nacionalidad: str | None = None
    logo: str | None = None

class ClientCreate(ClientBase):
    pass

class ClientUpdate(ClientBase):
    pass

class Client(ClientBase):
    id: int

    class Config:
        orm_mode = True

==================================================
ARCHIVO: .\backend\tests\test_clients.py
==================================================
import pytest
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

@pytest.fixture
def sample_client_data():
    return {
        "clave": "12345",
        "tipo_cliente": "Regular",
        "nombre": "Cliente Prueba",
        "pais": "México",
        "estado": "Chihuahua",
        "ciudad": "Cd. Juárez",
        "direccion": "Calle Falsa 123",
        "numero_ext": "12",
        "cp": "32000",
        "colonia": "Centro",
        "lada": "656",
        "telefono1": "1234567890",
        "telefono2": "0987654321",
        "tel_directo": "1231231234",
        "ext": "101",
        "fax": "1231231235",
        "horario": "9:00 - 18:00",
        "pagina": "www.clienteprueba.com",
        "correo": "cliente@prueba.com",
        "medio_publicidad": "Internet",
        "nacionalidad": "Mexicana",
        "logo": "logo.png"
    }

def test_create_client(sample_client_data):
    response = client.post("/api/v1/clients", json=sample_client_data)
    assert response.status_code == 200
    assert response.json()["clave"] == sample_client_data["clave"]

def test_read_client(sample_client_data):
    # Create a client first
    create_response = client.post("/api/v1/clients", json=sample_client_data)
    client_id = create_response.json()["id"]

    # Read the client
    response = client.get(f"/api/v1/clients/{client_id}")
    assert response.status_code == 200
    assert response.json()["id"] == client_id

def test_update_client(sample_client_data):
    # Create a client first
    create_response = client.post("/api/v1/clients", json=sample_client_data)
    client_id = create_response.json()["id"]

    # Update the client
    updated_data = {"nombre": "Cliente Actualizado"}
    response = client.put(f"/api/v1/clients/{client_id}", json=updated_data)
    assert response.status_code == 200
    assert response.json()["nombre"] == "Cliente Actualizado"

def test_delete_client(sample_client_data):
    # Create a client first
    create_response = client.post("/api/v1/clients", json=sample_client_data)
    client_id = create_response.json()["id"]

    # Delete the client
    response = client.delete(f"/api/v1/clients/{client_id}")
    assert response.status_code == 200

    # Verify deletion
    response = client.get(f"/api/v1/clients/{client_id}")
    assert response.status_code == 404

==================================================
ARCHIVO: .\db\schema.sql
==================================================
-- ServiceManagerWeb - Esquema de Base de Datos
-- Sistema multi-tenant de Mesa de Ayuda B2B
-- PostgreSQL 15+

-- ===================================
-- DOMINIO: TENANTS (Multi-tenancy)
-- ===================================

CREATE TABLE tenants (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    domain VARCHAR(255),
    logo_url VARCHAR(500),
    contact_email VARCHAR(320),
    contact_phone VARCHAR(20),
    address TEXT,
    timezone VARCHAR(50) DEFAULT 'UTC',
    locale VARCHAR(10) DEFAULT 'es-ES',
    
    -- Settings
    max_users INTEGER DEFAULT 50,
    max_storage_mb INTEGER DEFAULT 1024,
    allowed_file_types TEXT[] DEFAULT ARRAY['pdf','jpg','jpeg','png','doc','docx','xls','xlsx','txt'],
    
    -- Status
    status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'inactive')),
    
    -- Timestamps
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    CONSTRAINT tenants_slug_format CHECK (slug ~ '^[a-z0-9-]+$')
);

-- Índices para tenants
CREATE INDEX idx_tenants_slug ON tenants(slug);
CREATE INDEX idx_tenants_status ON tenants(status);
CREATE INDEX idx_tenants_domain ON tenants(domain);

-- ===================================
-- DOMINIO: CLIENTS (Cartera de Clientes)
-- ===================================

-- Enum para los radio buttons de estatus 
CREATE TYPE client_status_enum AS ENUM (
    'prospect', 
    'active', 
    'suspended', 
    'cancelled'
);

CREATE TABLE clients (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,

    -- Identificación (Sección General)
    code VARCHAR(50),           -- Clave del cliente
    name VARCHAR(200) NOT NULL, -- Nombre / Razón Social
    tax_id VARCHAR(20),         -- RFC
    client_type VARCHAR(50),    -- Tipo de Cliente
    account_manager VARCHAR(100), -- Encargado Cliente (Texto simple por ahora)

    -- Dirección (Desglosada)
    address_street TEXT,        -- Dirección / Calle
    address_ext_num VARCHAR(20), -- Núm. Ext
    neighborhood VARCHAR(100),  -- Colonia
    zip_code VARCHAR(10),       -- CP
    city VARCHAR(100),          -- Ciudad
    state VARCHAR(100),         -- Estado
    country VARCHAR(100) DEFAULT 'Mexico',

    -- Contacto
    phone_primary VARCHAR(20),  -- Teléfono 1
    phone_secondary VARCHAR(20),-- Teléfono 2
    fax VARCHAR(20),
    email VARCHAR(320),         -- Correo
    website VARCHAR(255),       -- Página Web

    -- Configuración y Flexibilidad
    status client_status_enum DEFAULT 'prospect',
    logo_url VARCHAR(500),      -- Ruta al archivo PDF/Imagen del logo
    
    -- Campo JSONB para lo que sobre (Horario, Nacionalidad, Medio Publicidad, Lada, etc.)
    properties JSONB DEFAULT '{}'::jsonb,

    -- Auditoría estándar
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

    -- Restricciones: No repetir RFC ni Clave dentro del mismo Tenant
    UNIQUE(tenant_id, code),
    UNIQUE(tenant_id, tax_id)
);

-- Índices para búsqueda rápida
CREATE INDEX idx_clients_tenant_id ON clients(tenant_id);
CREATE INDEX idx_clients_tax_id ON clients(tax_id);
CREATE INDEX idx_clients_name ON clients(name);
CREATE INDEX idx_clients_properties ON clients USING gin (properties);

-- Trigger para mantener actualizado el campo updated_at
-- (Usa la función que ya definiste al final de tu script)
CREATE TRIGGER update_clients_updated_at BEFORE UPDATE ON clients 
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();


-- ===================================
-- DOMINIO: AUTH (Autenticación)
-- ===================================

CREATE TYPE user_role_enum AS ENUM (
    'ADMIN',           -- Control total (staff interno)
    'SUPPORT_MANAGER', -- Gestión de equipos y SLAs
    'AGENT',           -- Atención de tickets
    'AUDITOR',         -- Solo lectura para auditoría
    'CLIENT_ADMIN',    -- Admin de organización cliente
    'CLIENT_USER'      -- Usuario final cliente
);

CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    
    -- Básicos
    email VARCHAR(320) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    avatar_url VARCHAR(500),
    
    -- Auth
    password_hash VARCHAR(255) NOT NULL,
    role user_role_enum NOT NULL,
    
    -- 2FA (opcional para staff interno)
    totp_secret VARCHAR(32),
    totp_enabled BOOLEAN DEFAULT FALSE,
    backup_codes TEXT[], -- códigos de recuperación
    
    -- Status
    is_active BOOLEAN DEFAULT TRUE,
    email_verified BOOLEAN DEFAULT FALSE,
    last_login TIMESTAMP WITH TIME ZONE,
    last_activity TIMESTAMP WITH TIME ZONE,
    
    -- Preferences
    language VARCHAR(10) DEFAULT 'es',
    timezone VARCHAR(50) DEFAULT 'UTC',
    notifications_email BOOLEAN DEFAULT TRUE,
    
    -- Timestamps
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    UNIQUE(tenant_id, email)
);

-- Índices para users
CREATE INDEX idx_users_tenant_id ON users(tenant_id);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_role ON users(role);
CREATE INDEX idx_users_active ON users(is_active);
CREATE INDEX idx_users_tenant_email ON users(tenant_id, email);

-- Tabla para refresh tokens
CREATE TABLE refresh_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token_hash VARCHAR(255) NOT NULL,
    device_info VARCHAR(500),
    ip_address INET,
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    revoked BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Índices para refresh_tokens
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_hash ON refresh_tokens(token_hash);
CREATE INDEX idx_refresh_tokens_expires ON refresh_tokens(expires_at);

-- ===================================
-- DOMINIO: TICKETS (Core del negocio)
-- ===================================

CREATE TYPE ticket_status_enum AS ENUM (
    'NEW',
    'TRIAGE',
    'IN_PROGRESS',
    'WAITING_CUSTOMER',
    'RESOLVED',
    'CLOSED',
    'REOPENED'
);

CREATE TYPE ticket_priority_enum AS ENUM (
    'LOW',
    'MEDIUM', 
    'HIGH',
    'URGENT'
);

-- Categorías de tickets por tenant
CREATE TABLE ticket_categories (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    color VARCHAR(7), -- hex color
    sla_response_hours INTEGER DEFAULT 24,
    sla_resolution_hours INTEGER DEFAULT 72,
    auto_assign_to UUID REFERENCES users(id),
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    UNIQUE(tenant_id, name)
);

-- Sistemas afectados por tenant
CREATE TABLE affected_systems (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    UNIQUE(tenant_id, name)
);

-- Tickets principales
CREATE TABLE tickets (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    ticket_number VARCHAR(20) NOT NULL, -- formato: TKT-2024-000001
    
    -- Básicos
    subject VARCHAR(255) NOT NULL,
    description TEXT NOT NULL,
    
    -- Clasificación
    category_id UUID REFERENCES ticket_categories(id),
    priority ticket_priority_enum DEFAULT 'MEDIUM',
    affected_system_id UUID REFERENCES affected_systems(id),
    
    -- Asignación
    created_by UUID NOT NULL REFERENCES users(id),
    assigned_to UUID REFERENCES users(id),
    
    -- Estado
    status ticket_status_enum DEFAULT 'NEW',
    
    -- SLA tracking
    sla_response_due TIMESTAMP WITH TIME ZONE,
    sla_resolution_due TIMESTAMP WITH TIME ZONE,
    first_response_at TIMESTAMP WITH TIME ZONE,
    resolved_at TIMESTAMP WITH TIME ZONE,
    
    -- CSAT (Customer Satisfaction)
    rating INTEGER CHECK (rating >= 1 AND rating <= 5),
    rating_comment TEXT,
    rated_at TIMESTAMP WITH TIME ZONE,
    
    -- Timestamps
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    UNIQUE(tenant_id, ticket_number)
);

-- Índices para tickets
CREATE INDEX idx_tickets_tenant_id ON tickets(tenant_id);
CREATE INDEX idx_tickets_number ON tickets(ticket_number);
CREATE INDEX idx_tickets_created_by ON tickets(created_by);
CREATE INDEX idx_tickets_assigned_to ON tickets(assigned_to);
CREATE INDEX idx_tickets_status ON tickets(status);
CREATE INDEX idx_tickets_priority ON tickets(priority);
CREATE INDEX idx_tickets_category ON tickets(category_id);
CREATE INDEX idx_tickets_sla_response ON tickets(sla_response_due);
CREATE INDEX idx_tickets_sla_resolution ON tickets(sla_resolution_due);
CREATE INDEX idx_tickets_created_at ON tickets(created_at);

-- Comentarios en tickets
CREATE TABLE ticket_comments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
    author_id UUID NOT NULL REFERENCES users(id),
    
    content TEXT NOT NULL,
    is_internal BOOLEAN DEFAULT FALSE, -- solo visible para staff interno
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Índices para comentarios
CREATE INDEX idx_ticket_comments_ticket_id ON ticket_comments(ticket_id);
CREATE INDEX idx_ticket_comments_author_id ON ticket_comments(author_id);
CREATE INDEX idx_ticket_comments_created_at ON ticket_comments(created_at);

-- Adjuntos en tickets/comentarios
CREATE TABLE ticket_attachments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
    comment_id UUID REFERENCES ticket_comments(id) ON DELETE CASCADE,
    uploaded_by UUID NOT NULL REFERENCES users(id),
    
    -- Archivo
    filename VARCHAR(255) NOT NULL,
    original_filename VARCHAR(255) NOT NULL,
    mime_type VARCHAR(100) NOT NULL,
    file_size INTEGER NOT NULL, -- bytes
    file_path VARCHAR(500) NOT NULL, -- path en storage
    
    -- Checksums para integridad
    md5_hash VARCHAR(32),
    sha256_hash VARCHAR(64),
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Índices para adjuntos
CREATE INDEX idx_ticket_attachments_ticket_id ON ticket_attachments(ticket_id);
CREATE INDEX idx_ticket_attachments_comment_id ON ticket_attachments(comment_id);
CREATE INDEX idx_ticket_attachments_uploaded_by ON ticket_attachments(uploaded_by);

-- Historial de cambios de estado
CREATE TABLE ticket_status_history (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    ticket_id UUID NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
    changed_by UUID NOT NULL REFERENCES users(id),
    
    old_status ticket_status_enum,
    new_status ticket_status_enum NOT NULL,
    old_assigned_to UUID REFERENCES users(id),
    new_assigned_to UUID REFERENCES users(id),
    
    comment TEXT,
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Índices para historial de estados
CREATE INDEX idx_ticket_status_history_ticket_id ON ticket_status_history(ticket_id);
CREATE INDEX idx_ticket_status_history_changed_by ON ticket_status_history(changed_by);
CREATE INDEX idx_ticket_status_history_created_at ON ticket_status_history(created_at);

-- ===================================
-- DOMINIO: NOTIFICATIONS (Comunicaciones)
-- ===================================

-- Templates de email
CREATE TABLE email_templates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID REFERENCES tenants(id), -- NULL = template global
    
    name VARCHAR(100) NOT NULL,
    subject_template TEXT NOT NULL,
    body_template TEXT NOT NULL,
    template_type VARCHAR(50) NOT NULL, -- ticket_created, ticket_assigned, etc.
    
    -- Variables disponibles en formato JSON
    available_variables JSONB,
    
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Índices para templates
CREATE INDEX idx_email_templates_tenant_id ON email_templates(tenant_id);
CREATE INDEX idx_email_templates_type ON email_templates(template_type);

-- Log de notificaciones enviadas
CREATE TABLE notification_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    
    recipient_email VARCHAR(320) NOT NULL,
    subject VARCHAR(500) NOT NULL,
    template_type VARCHAR(50),
    
    -- Metadata
    ticket_id UUID REFERENCES tickets(id),
    user_id UUID REFERENCES users(id),
    
    -- Estado del envío
    status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'sent', 'failed', 'bounced')),
    error_message TEXT,
    
    -- Proveedor (ej: sendgrid, ses, smtp)
    provider VARCHAR(50),
    external_id VARCHAR(255), -- ID del proveedor
    
    sent_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Índices para logs de notificaciones
CREATE INDEX idx_notification_logs_tenant_id ON notification_logs(tenant_id);
CREATE INDEX idx_notification_logs_recipient ON notification_logs(recipient_email);
CREATE INDEX idx_notification_logs_ticket_id ON notification_logs(ticket_id);
CREATE INDEX idx_notification_logs_status ON notification_logs(status);
CREATE INDEX idx_notification_logs_created_at ON notification_logs(created_at);

-- ===================================
-- DOMINIO: AUDIT (Bitácora)
-- ===================================

CREATE TABLE audit_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    user_id UUID REFERENCES users(id), -- NULL para acciones del sistema
    
    -- Acción
    action VARCHAR(100) NOT NULL, -- ej: user.login, ticket.create, ticket.assign
    resource_type VARCHAR(50) NOT NULL, -- user, ticket, comment, etc.
    resource_id UUID,
    
    -- Contexto
    ip_address INET,
    user_agent TEXT,
    correlation_id UUID, -- para rastrear requests
    
    -- Cambios (antes/después en JSON)
    old_values JSONB,
    new_values JSONB,
    
    -- Metadata adicional
    metadata JSONB,
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Índices para audit logs
CREATE INDEX idx_audit_logs_tenant_id ON audit_logs(tenant_id);
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
CREATE INDEX idx_audit_logs_correlation_id ON audit_logs(correlation_id);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);

-- ===================================
-- FUNCIONES Y TRIGGERS
-- ===================================

-- Función para actualizar updated_at automáticamente
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
    NEW.updated_at = NOW();
    RETURN NEW;
END;
$$ language 'plpgsql';

-- Triggers para updated_at
CREATE TRIGGER update_tenants_updated_at BEFORE UPDATE ON tenants 
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users 
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_tickets_updated_at BEFORE UPDATE ON tickets 
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_ticket_comments_updated_at BEFORE UPDATE ON ticket_comments 
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_email_templates_updated_at BEFORE UPDATE ON email_templates 
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();

-- ===================================
-- DATOS INICIALES (SEEDS)
-- ===================================

-- Tenant de ejemplo para desarrollo
INSERT INTO tenants (name, slug, contact_email) VALUES 
('Aduanasoft Demo', 'aduanasoft-demo', 'demo@aduanasoft.com');

-- Usuario admin por defecto (password: admin123)
-- Hash generado con Argon2: $argon2id$v=19$m=65536,t=3,p=4$...
INSERT INTO users (tenant_id, email, first_name, last_name, password_hash, role, is_active, email_verified) 
SELECT 
    id,
    'admin@aduanasoft.com',
    'Admin', 
    'Sistema',
    '$argon2id$v=19$m=65536,t=3,p=4$example_hash_here',
    'ADMIN',
    true,
    true
FROM tenants WHERE slug = 'aduanasoft-demo';

-- Categorías por defecto
INSERT INTO ticket_categories (tenant_id, name, description, sla_response_hours, sla_resolution_hours)
SELECT 
    id,
    'Soporte Técnico',
    'Problemas técnicos generales',
    2,
    24
FROM tenants WHERE slug = 'aduanasoft-demo'
UNION ALL
SELECT 
    id,
    'Consulta Comercial',
    'Preguntas sobre productos y servicios',
    4,
    48
FROM tenants WHERE slug = 'aduanasoft-demo'
UNION ALL
SELECT 
    id,
    'Incidente Crítico',
    'Problemas que afectan operaciones',
    1,
    8
FROM tenants WHERE slug = 'aduanasoft-demo';

-- Sistemas afectados por defecto
INSERT INTO affected_systems (tenant_id, name, description)
SELECT 
    id,
    'Plataforma Web',
    'Sistema web principal'
FROM tenants WHERE slug = 'aduanasoft-demo'
UNION ALL
SELECT 
    id,
    'API',
    'Servicios de API'
FROM tenants WHERE slug = 'aduanasoft-demo'
UNION ALL
SELECT 
    id,
    'Base de Datos',
    'Sistemas de almacenamiento'
FROM tenants WHERE slug = 'aduanasoft-demo';

-- Templates de email básicos
INSERT INTO email_templates (name, subject_template, body_template, template_type, available_variables)
VALUES 
(
    'Ticket Creado',
    'Nuevo ticket #{{ticket_number}}: {{subject}}',
    'Hola {{user_name}},\n\nSe ha creado un nuevo ticket:\n\nNúmero: #{{ticket_number}}\nAsunto: {{subject}}\nPrioridad: {{priority}}\n\nPuedes ver los detalles en: {{ticket_url}}\n\nSaludos,\nEquipo de Soporte',
    'ticket_created',
    '{"ticket_number": "string", "subject": "string", "priority": "string", "user_name": "string", "ticket_url": "string"}'
),
(
    'Ticket Asignado',
    'Ticket #{{ticket_number}} asignado a ti',
    'Hola {{agent_name}},\n\nSe te ha asignado el ticket:\n\nNúmero: #{{ticket_number}}\nAsunto: {{subject}}\nCliente: {{customer_name}}\nPrioridad: {{priority}}\n\nPuedes verlo en: {{ticket_url}}\n\nSaludos,\nSistema de Tickets',
    'ticket_assigned',
    '{"ticket_number": "string", "subject": "string", "agent_name": "string", "customer_name": "string", "priority": "string", "ticket_url": "string"}'
);

-- ===================================
-- COMENTARIOS Y DOCUMENTACIÓN
-- ===================================

COMMENT ON TABLE tenants IS 'Organizaciones cliente en el sistema multi-tenant';
COMMENT ON TABLE users IS 'Usuarios del sistema (internos y clientes)';
COMMENT ON TABLE tickets IS 'Tickets de soporte - core del negocio';
COMMENT ON TABLE ticket_comments IS 'Comentarios en tickets';
COMMENT ON TABLE ticket_attachments IS 'Archivos adjuntos en tickets';
COMMENT ON TABLE audit_logs IS 'Bitácora de acciones para auditoría y compliance';

==================================================
ARCHIVO: .\docs\api-contract.md
==================================================
# API Contract - ServiceManagerWeb
# Mesa de Ayuda B2B - Especificación de Endpoints

## Base URL
- Desarrollo: `http://localhost:8000`
- Producción: `https://api.servicemanager.aduanasoft.com`

## Versionado
- Todos los endpoints tienen prefijo `/v1/`
- Versionado en URL path (no headers)

## Autenticación
- JWT Bearer Token en header `Authorization: Bearer <token>`
- Refresh token para renovación automática

## Headers Estándar
```
Authorization: Bearer <jwt_token>
Content-Type: application/json
X-Tenant-ID: <tenant_uuid>  # Requerido para endpoints multi-tenant
X-Correlation-ID: <uuid>    # Opcional para tracking
Accept-Language: es-ES      # Para internacionalización
```

## Responses Estándar

### Éxito (2xx)
```json
{
  "success": true,
  "data": { ... },
  "message": "Operación exitosa",
  "metadata": {
    "page": 1,
    "per_page": 20,
    "total": 150,
    "total_pages": 8
  }
}
```

### Error (4xx/5xx)
```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Datos inválidos",
    "details": [
      {
        "field": "email",
        "message": "Email inválido"
      }
    ]
  },
  "correlation_id": "uuid"
}
```

---

## DOMINIO: AUTH

### POST /v1/auth/login
**Descripción**: Autenticación de usuario  
**Público**: Sí  

**Request Body**:
```json
{
  "email": "user@example.com",
  "password": "password123",
  "tenant_slug": "aduanasoft-demo",
  "totp_code": "123456"  // opcional, solo si 2FA activado
}
```

**Response 200**:
```json
{
  "success": true,
  "data": {
    "access_token": "jwt_token",
    "refresh_token": "refresh_token",
    "expires_in": 3600,
    "user": {
      "id": "uuid",
      "email": "user@example.com",
      "first_name": "Juan",
      "last_name": "Pérez",
      "role": "AGENT",
      "tenant": {
        "id": "uuid",
        "name": "Aduanasoft Demo",
        "slug": "aduanasoft-demo"
      }
    }
  }
}
```

### POST /v1/auth/refresh
**Descripción**: Renovar access token  
**Público**: Sí  

**Request Body**:
```json
{
  "refresh_token": "refresh_token"
}
```

### POST /v1/auth/logout
**Descripción**: Cerrar sesión (revoca refresh token)  
**Autenticado**: Sí  

### GET /v1/auth/me
**Descripción**: Información del usuario actual  
**Autenticado**: Sí  

### PUT /v1/auth/me
**Descripción**: Actualizar perfil propio  
**Autenticado**: Sí  

**Request Body**:
```json
{
  "first_name": "Juan",
  "last_name": "Pérez",
  "language": "es",
  "timezone": "America/Mexico_City",
  "notifications_email": true
}
```

### POST /v1/auth/change-password
**Descripción**: Cambiar contraseña  
**Autenticado**: Sí  

### POST /v1/auth/2fa/setup
**Descripción**: Configurar 2FA (solo roles internos)  
**Autenticado**: Sí, Roles: ADMIN, SUPPORT_MANAGER, AGENT, AUDITOR  

### POST /v1/auth/2fa/verify
**Descripción**: Verificar código 2FA durante setup  
**Autenticado**: Sí  

---

## DOMINIO: TENANTS

### GET /v1/tenants/current
**Descripción**: Información del tenant actual  
**Autenticado**: Sí  

### PUT /v1/tenants/current
**Descripción**: Actualizar tenant (solo CLIENT_ADMIN/ADMIN)  
**Autenticado**: Sí, Roles: CLIENT_ADMIN, ADMIN  

### GET /v1/tenants (solo ADMIN)
**Descripción**: Listar todos los tenants  
**Autenticado**: Sí, Roles: ADMIN  

### POST /v1/tenants (solo ADMIN)
**Descripción**: Crear nuevo tenant  
**Autenticado**: Sí, Roles: ADMIN  

---

## DOMINIO: USERS

### GET /v1/users
**Descripción**: Listar usuarios del tenant  
**Autenticado**: Sí  
**Roles**: Todos (filtros por rol)  

**Query Params**:
```
?page=1&per_page=20&role=AGENT&is_active=true&search=juan
```

**Response 200**:
```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "email": "agent@example.com",
      "first_name": "Juan",
      "last_name": "Agente",
      "role": "AGENT",
      "is_active": true,
      "email_verified": true,
      "last_login": "2024-01-15T10:30:00Z",
      "created_at": "2024-01-01T00:00:00Z"
    }
  ],
  "metadata": {
    "page": 1,
    "per_page": 20,
    "total": 150,
    "total_pages": 8
  }
}
```

### POST /v1/users
**Descripción**: Crear usuario  
**Autenticado**: Sí, Roles: ADMIN, SUPPORT_MANAGER, CLIENT_ADMIN  

**Request Body**:
```json
{
  "email": "nuevo@example.com",
  "first_name": "Nuevo",
  "last_name": "Usuario",
  "role": "AGENT",
  "password": "temporal123",  // opcional, se genera automáticamente
  "send_welcome_email": true
}
```

### GET /v1/users/{user_id}
**Descripción**: Obtener usuario específico  
**Autenticado**: Sí  

### PUT /v1/users/{user_id}
**Descripción**: Actualizar usuario  
**Autenticado**: Sí, Roles: ADMIN, SUPPORT_MANAGER, CLIENT_ADMIN  

### DELETE /v1/users/{user_id}
**Descripción**: Desactivar usuario (soft delete)  
**Autenticado**: Sí, Roles: ADMIN, SUPPORT_MANAGER, CLIENT_ADMIN  

---

## DOMINIO: TICKETS

### GET /v1/tickets
**Descripción**: Listar tickets con filtros  
**Autenticado**: Sí  

**Query Params**:
```
?page=1&per_page=20
&status=NEW,IN_PROGRESS
&priority=HIGH,URGENT
&assigned_to=uuid
&created_by=uuid
&category_id=uuid
&search=problema+conexion
&sort=created_at_desc
&date_from=2024-01-01
&date_to=2024-01-31
```

**Response 200**:
```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "ticket_number": "TKT-2024-000001",
      "subject": "Problema de conexión",
      "status": "IN_PROGRESS",
      "priority": "HIGH",
      "category": {
        "id": "uuid",
        "name": "Soporte Técnico"
      },
      "created_by": {
        "id": "uuid",
        "first_name": "Juan",
        "last_name": "Cliente"
      },
      "assigned_to": {
        "id": "uuid",
        "first_name": "Ana",
        "last_name": "Soporte"
      },
      "sla_response_due": "2024-01-15T12:00:00Z",
      "sla_resolution_due": "2024-01-16T10:00:00Z",
      "created_at": "2024-01-15T10:00:00Z",
      "updated_at": "2024-01-15T11:30:00Z"
    }
  ],
  "metadata": {
    "page": 1,
    "per_page": 20,
    "total": 150,
    "total_pages": 8
  }
}
```

### POST /v1/tickets
**Descripción**: Crear nuevo ticket  
**Autenticado**: Sí  

**Request Body**:
```json
{
  "subject": "Problema de conexión con el sistema",
  "description": "Descripción detallada del problema...",
  "priority": "HIGH",
  "category_id": "uuid",
  "affected_system_id": "uuid",
  "attachments": [
    {
      "filename": "screenshot.png",
      "content_type": "image/png",
      "content_base64": "base64_data"
    }
  ]
}
```

**Response 201**:
```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "ticket_number": "TKT-2024-000001",
    "subject": "Problema de conexión con el sistema",
    "status": "NEW",
    "sla_response_due": "2024-01-15T12:00:00Z",
    "created_at": "2024-01-15T10:00:00Z"
  }
}
```

### GET /v1/tickets/{ticket_id}
**Descripción**: Obtener ticket completo con comentarios  
**Autenticado**: Sí  

**Response 200**:
```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "ticket_number": "TKT-2024-000001",
    "subject": "Problema de conexión",
    "description": "Descripción completa...",
    "status": "IN_PROGRESS",
    "priority": "HIGH",
    "category": {...},
    "affected_system": {...},
    "created_by": {...},
    "assigned_to": {...},
    "attachments": [...],
    "comments": [
      {
        "id": "uuid",
        "content": "Comentario del ticket...",
        "author": {...},
        "is_internal": false,
        "attachments": [...],
        "created_at": "2024-01-15T11:00:00Z"
      }
    ],
    "status_history": [...],
    "sla_metrics": {
      "response_due": "2024-01-15T12:00:00Z",
      "resolution_due": "2024-01-16T10:00:00Z",
      "first_response_at": "2024-01-15T11:15:00Z",
      "response_sla_met": true,
      "resolution_sla_met": null
    },
    "created_at": "2024-01-15T10:00:00Z",
    "updated_at": "2024-01-15T11:30:00Z"
  }
}
```

### PUT /v1/tickets/{ticket_id}
**Descripción**: Actualizar ticket (estado, asignación, etc.)  
**Autenticado**: Sí  

**Request Body**:
```json
{
  "status": "IN_PROGRESS",
  "assigned_to": "uuid",
  "priority": "URGENT",
  "comment": "Escalando por alta prioridad"
}
```

### POST /v1/tickets/{ticket_id}/comments
**Descripción**: Agregar comentario al ticket  
**Autenticado**: Sí  

**Request Body**:
```json
{
  "content": "Comentario con solución propuesta...",
  "is_internal": false,
  "attachments": [
    {
      "filename": "solution.pdf",
      "content_type": "application/pdf",
      "content_base64": "base64_data"
    }
  ]
}
```

### PUT /v1/tickets/{ticket_id}/rating
**Descripción**: Calificar ticket resuelto (solo cliente)  
**Autenticado**: Sí, Roles: CLIENT_ADMIN, CLIENT_USER  

**Request Body**:
```json
{
  "rating": 5,
  "comment": "Excelente atención y resolución rápida"
}
```

---

## DOMINIO: CATEGORIES & SYSTEMS

### GET /v1/categories
**Descripción**: Listar categorías del tenant  
**Autenticado**: Sí  

### POST /v1/categories (solo staff interno)
**Descripción**: Crear categoría  
**Autenticado**: Sí, Roles: ADMIN, SUPPORT_MANAGER  

### GET /v1/affected-systems
**Descripción**: Listar sistemas afectados  
**Autenticado**: Sí  

---

## DOMINIO: NOTIFICATIONS

### GET /v1/notifications/templates (solo staff interno)
**Descripción**: Listar templates de email  
**Autenticado**: Sí, Roles: ADMIN, SUPPORT_MANAGER  

### POST /v1/notifications/test-email (solo ADMIN)
**Descripción**: Enviar email de prueba  
**Autenticado**: Sí, Roles: ADMIN  

---

## DOMINIO: REPORTS & ANALYTICS

### GET /v1/reports/dashboard
**Descripción**: Métricas del dashboard  
**Autenticado**: Sí  

**Response 200**:
```json
{
  "success": true,
  "data": {
    "tickets": {
      "total": 150,
      "new": 12,
      "in_progress": 45,
      "waiting_customer": 8,
      "resolved_today": 15
    },
    "sla": {
      "response_rate": 95.5,
      "resolution_rate": 87.2
    },
    "agents": {
      "active": 8,
      "avg_load": 5.6
    },
    "csat": {
      "average": 4.2,
      "total_responses": 89
    }
  }
}
```

### GET /v1/reports/tickets
**Descripción**: Reporte de tickets con filtros  
**Autenticado**: Sí  

---

## DOMINIO: AUDIT

### GET /v1/audit/logs (solo AUDITOR/ADMIN)
**Descripción**: Consultar logs de auditoría  
**Autenticado**: Sí, Roles: AUDITOR, ADMIN  

**Query Params**:
```
?page=1&per_page=50
&action=ticket.create,ticket.assign
&user_id=uuid
&resource_type=ticket
&date_from=2024-01-01
&date_to=2024-01-31
```

---

## CÓDIGOS DE ERROR ESTÁNDAR

- `400` - Bad Request (datos inválidos)
- `401` - Unauthorized (no autenticado)
- `403` - Forbidden (sin permisos)
- `404` - Not Found (recurso no encontrado)
- `409` - Conflict (recurso duplicado)
- `422` - Unprocessable Entity (validación fallida)
- `429` - Too Many Requests (rate limit)
- `500` - Internal Server Error

## RATE LIMITING

- Auth endpoints: 10 req/min por IP
- API endpoints: 100 req/min por usuario
- File uploads: 5 req/min por usuario

## PAGINACIÓN

- Default: `per_page=20`, `max=100`
- Links de navegación en metadata
- Total count incluido cuando sea eficiente

## ORDENAMIENTO

Formato: `?sort=field_direction`
- `created_at_desc` (default)
- `updated_at_desc`  
- `priority_desc`
- `status_asc`

## BÚSQUEDA

- Full-text search en `subject` y `description`
- Búsqueda por número de ticket exacto
- Filtros combinables con AND lógico

==================================================
ARCHIVO: .\docs\database-schema.md
==================================================
# Modelo de Datos - ServiceManagerWeb

## Resumen del Esquema

El sistema utiliza PostgreSQL con un diseño multi-tenant donde cada cliente (tenant) tiene sus datos aislados pero comparte la misma estructura de base de datos.

## Dominios Principales

### 1. **TENANTS** - Multi-tenancy
- `tenants`: Organizaciones cliente
- Cada tenant tiene configuraciones propias (usuarios max, storage, tipos de archivo)

### 2. **AUTH** - Autenticación y Autorización  
- `users`: Usuarios del sistema (internos y clientes)
- `refresh_tokens`: Tokens de refresco para JWT
- Roles: ADMIN, SUPPORT_MANAGER, AGENT, AUDITOR, CLIENT_ADMIN, CLIENT_USER
- Soporte para 2FA (TOTP) opcional para staff interno

### 3. **TICKETS** - Core del Negocio
- `tickets`: Tickets de soporte principales
- `ticket_categories`: Categorías personalizables por tenant
- `affected_systems`: Sistemas afectados por tenant
- `ticket_comments`: Conversación en tickets
- `ticket_attachments`: Archivos adjuntos
- `ticket_status_history`: Historial de cambios de estado

Estados de ticket: NEW → TRIAGE → IN_PROGRESS → WAITING_CUSTOMER → RESOLVED → CLOSED
Prioridades: LOW, MEDIUM, HIGH, URGENT

### 4. **NOTIFICATIONS** - Comunicaciones
- `email_templates`: Templates personalizables por tenant
- `notification_logs`: Historial de emails enviados
- Soporte para variables dinámicas en templates

### 5. **AUDIT** - Bitácora y Compliance
- `audit_logs`: Registro completo de acciones
- Tracking con correlation_id para requests
- Almacena cambios antes/después en JSON

## Características Técnicas

### Índices Estratégicos
- Optimizados para queries por tenant
- Indices compuestos para búsquedas frecuentes
- Índices en campos de fecha para reportes

### Constraints y Validación
- CHECK constraints para valores enum
- Foreign keys con CASCADE apropiados
- UNIQUE constraints compuestos (tenant_id + campo)

### Triggers Automáticos
- `updated_at` se actualiza automáticamente
- Preparado para audit logging automático

### Multi-tenancy
- Todos los datos principales tienen `tenant_id`
- Aislamiento a nivel de aplicación
- Configuraciones por tenant (SLA, categorías, etc.)

## Numeración de Tickets

Formato: `TKT-YYYY-NNNNNN` (ej: TKT-2026-000001)
- Único por tenant
- Año incluido para fácil organización
- 6 dígitos con ceros a la izquierda

## SLA Tracking

- `sla_response_due`: Tiempo límite para primera respuesta
- `sla_resolution_due`: Tiempo límite para resolución
- `first_response_at`: Timestamp de primera respuesta
- Configurables por categoría

## Almacenamiento de Archivos

- Metadata en BD, archivos en filesystem/S3
- Checksums MD5 y SHA256 para integridad
- Validación de tipos MIME
- Límites de tamaño por tenant

## Datos Iniciales

El schema incluye:
- Tenant demo para desarrollo
- Usuario admin por defecto
- Categorías base (Soporte Técnico, Consulta Comercial, Incidente Crítico)  
- Sistemas base (Plataforma Web, API, Base de Datos)
- Templates de email básicos

## Escalabilidad

- Preparado para sharding por tenant_id
- Partitioning por fecha en audit_logs
- Índices optimizados para paginación
- Soft deletes donde aplique

==================================================
ARCHIVO: .\frontend-client\.eslintrc.json
==================================================
{
  "extends": [
    "eslint:recommended",
    "@typescript-eslint/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "svelte3"],
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module"
  },
  "env": {
    "browser": true,
    "es2017": true,
    "node": true
  },
  "overrides": [
    {
      "files": ["*.svelte"],
      "processor": "svelte3/svelte3"
    }
  ],
  "rules": {
    "@typescript-eslint/no-unused-vars": [
      "error",
      { "argsIgnorePattern": "^_" }
    ],
    "@typescript-eslint/no-explicit-any": "warn",
    "no-console": ["warn", { "allow": ["warn", "error"] }],
    "prefer-const": "error"
  },
  "settings": {
    "svelte3/typescript": true
  },
  "ignorePatterns": [
    ".svelte-kit/**",
    "build/**",
    "dist/**",
    "node_modules/**"
  ]
}

==================================================
ARCHIVO: .\frontend-client\package.json
==================================================
{
  "name": "@servicemanager/client-frontend",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview",
    "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
    "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
    "lint": "prettier --plugin-search-dir . --check . && eslint .",
    "format": "prettier --plugin-search-dir . --write .",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:ui": "vitest --ui"
  },
  "devDependencies": {
    "@sveltejs/adapter-node": "^1.3.1",
    "@sveltejs/kit": "^1.20.4",
    "@types/cookie": "^0.5.1",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "autoprefixer": "^10.4.14",
    "eslint": "^8.28.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-svelte": "^2.30.0",
    "postcss": "^8.4.24",
    "prettier": "^2.8.0",
    "prettier-plugin-svelte": "^2.10.1",
    "svelte": "^4.0.5",
    "svelte-check": "^3.4.3",
    "tailwindcss": "^3.3.0",
    "tslib": "^2.4.1",
    "typescript": "^5.0.0",
    "vite": "^4.4.2",
    "vitest": "^0.34.0"
  },
  "dependencies": {
    "@tailwindcss/forms": "^0.5.4",
    "@tailwindcss/typography": "^0.5.9",
    "@heroicons/react": "^2.0.18",
    "heroicons": "^2.0.18", 
    "zod": "^3.22.2",
    "date-fns": "^2.30.0"
  }
}

==================================================
ARCHIVO: .\frontend-client\postcss.config.js
==================================================
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

==================================================
ARCHIVO: .\frontend-client\svelte.config.js
==================================================
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Consult https://kit.svelte.dev/docs/integrations#preprocessors
	// for more information about preprocessors
	preprocess: vitePreprocess(),

	kit: {
		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
		// If your environment is not supported or you settled on a specific environment, switch out the adapter.
		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
		adapter: adapter(),
		
		alias: {
			'$components': 'src/lib/components',
			'$stores': 'src/lib/stores',
			'$utils': 'src/lib/utils',
			'$types': 'src/lib/types'
		}
	}
};

export default config;

==================================================
ARCHIVO: .\frontend-client\tailwind.config.js
==================================================
/** @type {import('tailwindcss').Config} */
export default {
	content: ['./src/**/*.{html,js,svelte,ts}'],
	theme: {
		extend: {
			colors: {
				// Brand colors for ServiceManager
				primary: {
					50: '#eff6ff',
					100: '#dbeafe',
					200: '#bfdbfe',
					300: '#93c5fd',
					400: '#60a5fa',
					500: '#3b82f6',  // Main brand color
					600: '#2563eb',
					700: '#1d4ed8',
					800: '#1e40af',
					900: '#1e3a8a',
				},
				secondary: {
					50: '#f8fafc',
					100: '#f1f5f9',
					200: '#e2e8f0',
					300: '#cbd5e1',
					400: '#94a3b8',
					500: '#64748b',
					600: '#475569',
					700: '#334155',
					800: '#1e293b',
					900: '#0f172a',
				},
				success: {
					50: '#f0fdf4',
					100: '#dcfce7',
					200: '#bbf7d0',
					300: '#86efac',
					400: '#4ade80',
					500: '#22c55e',
					600: '#16a34a',
					700: '#15803d',
					800: '#166534',
					900: '#14532d',
				},
				warning: {
					50: '#fffbeb',
					100: '#fef3c7',
					200: '#fde68a',
					300: '#fcd34d',
					400: '#fbbf24',
					500: '#f59e0b',
					600: '#d97706',
					700: '#b45309',
					800: '#92400e',
					900: '#78350f',
				},
				error: {
					50: '#fef2f2',
					100: '#fee2e2',
					200: '#fecaca',
					300: '#fca5a5',
					400: '#f87171',
					500: '#ef4444',
					600: '#dc2626',
					700: '#b91c1c',
					800: '#991b1b',
					900: '#7f1d1d',
				}
			},
			fontFamily: {
				sans: ['Inter', 'ui-sans-serif', 'system-ui', 'sans-serif'],
			},
			boxShadow: {
				'sm': '0 1px 2px 0 rgb(0 0 0 / 0.05)',
				'DEFAULT': '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
				'md': '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
				'lg': '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
				'xl': '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
				'2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)',
			},
			animation: {
				'fade-in': 'fadeIn 0.5s ease-in-out',
				'slide-up': 'slideUp 0.3s ease-out',
				'pulse': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
			},
			keyframes: {
				fadeIn: {
					'0%': { opacity: '0' },
					'100%': { opacity: '1' },
				},
				slideUp: {
					'0%': { transform: 'translateY(10px)', opacity: '0' },
					'100%': { transform: 'translateY(0)', opacity: '1' },
				}
			}
		},
	},
	plugins: [
		require('@tailwindcss/forms'),
		require('@tailwindcss/typography'),
	],
};

==================================================
ARCHIVO: .\frontend-client\vite.config.js
==================================================
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [sveltekit()],
	server: {
		port: 3000,
		host: '0.0.0.0',
		proxy: {
			'/api': {
				target: 'http://servicemanager-backend:8000',
				changeOrigin: true,
				rewrite: (path) => path.replace(/^\/api/, '')
			}
		}
	},
	preview: {
		port: 3000,
		host: '0.0.0.0'
	},
	build: {
		target: 'esnext'
	}
});

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\ambient.d.ts
==================================================

// this file is generated — do not edit it


/// <reference types="@sveltejs/kit" />

/**
 * Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), this module cannot be imported into client-side code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://kit.svelte.dev/docs/configuration#env) (if configured).
 * 
 * _Unlike_ [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), the values exported from this module are statically injected into your bundle at build time, enabling optimisations like dead code elimination.
 * 
 * ```ts
 * import { API_KEY } from '$env/static/private';
 * ```
 * 
 * Note that all environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
 * 
 * ```
 * MY_FEATURE_FLAG=""
 * ```
 * 
 * You can override `.env` values from the command line like so:
 * 
 * ```bash
 * MY_FEATURE_FLAG="enabled" npm run dev
 * ```
 */
declare module '$env/static/private' {
	export const npm_config_user_agent: string;
	export const NODE_VERSION: string;
	export const HOSTNAME: string;
	export const YARN_VERSION: string;
	export const npm_node_execpath: string;
	export const SHLVL: string;
	export const npm_config_noproxy: string;
	export const HOME: string;
	export const npm_package_json: string;
	export const npm_config_userconfig: string;
	export const npm_config_local_prefix: string;
	export const COLOR: string;
	export const npm_config_prefix: string;
	export const npm_config_npm_version: string;
	export const npm_config_cache: string;
	export const npm_config_node_gyp: string;
	export const PATH: string;
	export const NODE: string;
	export const npm_package_name: string;
	export const npm_lifecycle_script: string;
	export const npm_package_version: string;
	export const npm_lifecycle_event: string;
	export const npm_config_globalconfig: string;
	export const npm_config_init_module: string;
	export const PWD: string;
	export const npm_execpath: string;
	export const npm_config_global_prefix: string;
	export const npm_command: string;
	export const NODE_ENV: string;
	export const INIT_CWD: string;
	export const EDITOR: string;
}

/**
 * Similar to [`$env/static/private`](https://kit.svelte.dev/docs/modules#$env-static-private), except that it only includes environment variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
 * 
 * Values are replaced statically at build time.
 * 
 * ```ts
 * import { PUBLIC_BASE_URL } from '$env/static/public';
 * ```
 */
declare module '$env/static/public' {
	export const PUBLIC_APP_NAME: string;
	export const PUBLIC_API_URL: string;
}

/**
 * This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/master/packages/adapter-node) (or running [`vite preview`](https://kit.svelte.dev/docs/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://kit.svelte.dev/docs/configuration#env) (if configured).
 * 
 * This module cannot be imported into client-side code.
 * 
 * ```ts
 * import { env } from '$env/dynamic/private';
 * console.log(env.DEPLOYMENT_SPECIFIC_VARIABLE);
 * ```
 * 
 * > In `dev`, `$env/dynamic` always includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
 */
declare module '$env/dynamic/private' {
	export const env: {
		npm_config_user_agent: string;
		NODE_VERSION: string;
		HOSTNAME: string;
		YARN_VERSION: string;
		npm_node_execpath: string;
		SHLVL: string;
		npm_config_noproxy: string;
		HOME: string;
		npm_package_json: string;
		npm_config_userconfig: string;
		npm_config_local_prefix: string;
		COLOR: string;
		npm_config_prefix: string;
		npm_config_npm_version: string;
		npm_config_cache: string;
		npm_config_node_gyp: string;
		PATH: string;
		NODE: string;
		npm_package_name: string;
		npm_lifecycle_script: string;
		npm_package_version: string;
		npm_lifecycle_event: string;
		npm_config_globalconfig: string;
		npm_config_init_module: string;
		PWD: string;
		npm_execpath: string;
		npm_config_global_prefix: string;
		npm_command: string;
		NODE_ENV: string;
		INIT_CWD: string;
		EDITOR: string;
		[key: `PUBLIC_${string}`]: undefined;
		[key: `${string}`]: string | undefined;
	}
}

/**
 * Similar to [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), but only includes variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
 * 
 * Note that public dynamic environment variables must all be sent from the server to the client, causing larger network requests — when possible, use `$env/static/public` instead.
 * 
 * ```ts
 * import { env } from '$env/dynamic/public';
 * console.log(env.PUBLIC_DEPLOYMENT_SPECIFIC_VARIABLE);
 * ```
 */
declare module '$env/dynamic/public' {
	export const env: {
		PUBLIC_APP_NAME: string;
		PUBLIC_API_URL: string;
		[key: `PUBLIC_${string}`]: string | undefined;
	}
}


==================================================
ARCHIVO: .\frontend-client\.svelte-kit\tsconfig.json
==================================================
{
	"compilerOptions": {
		"paths": {
			"$components": [
				"../src/lib/components"
			],
			"$components/*": [
				"../src/lib/components/*"
			],
			"$stores": [
				"../src/lib/stores"
			],
			"$stores/*": [
				"../src/lib/stores/*"
			],
			"$utils": [
				"../src/lib/utils"
			],
			"$utils/*": [
				"../src/lib/utils/*"
			],
			"$types": [
				"../src/lib/types"
			],
			"$types/*": [
				"../src/lib/types/*"
			],
			"$lib": [
				"../src/lib"
			],
			"$lib/*": [
				"../src/lib/*"
			]
		},
		"rootDirs": [
			"..",
			"./types"
		],
		"importsNotUsedAsValues": "error",
		"isolatedModules": true,
		"preserveValueImports": true,
		"lib": [
			"esnext",
			"DOM",
			"DOM.Iterable"
		],
		"moduleResolution": "node",
		"module": "esnext",
		"noEmit": true,
		"target": "esnext",
		"ignoreDeprecations": "5.0"
	},
	"include": [
		"ambient.d.ts",
		"./types/**/$types.d.ts",
		"../vite.config.js",
		"../vite.config.ts",
		"../src/**/*.js",
		"../src/**/*.ts",
		"../src/**/*.svelte",
		"../tests/**/*.js",
		"../tests/**/*.ts",
		"../tests/**/*.svelte"
	],
	"exclude": [
		"../node_modules/**",
		"./[!ambient.d.ts]**",
		"../src/service-worker.js",
		"../src/service-worker.ts",
		"../src/service-worker.d.ts"
	]
}

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\root.svelte
==================================================
<!-- This file is generated by @sveltejs/kit — do not edit it! -->

<script>
	import { setContext, afterUpdate, onMount, tick } from 'svelte';
	import { browser } from '$app/environment';

	// stores
	export let stores;
	export let page;
	
	export let constructors;
	export let components = [];
	export let form;
	export let data_0 = null;
	export let data_1 = null;

	if (!browser) {
		setContext('__svelte__', stores);
	}

	$: stores.page.set(page);
	afterUpdate(stores.page.notify);

	let mounted = false;
	let navigated = false;
	let title = null;

	onMount(() => {
		const unsubscribe = stores.page.subscribe(() => {
			if (mounted) {
				navigated = true;
				tick().then(() => {
					title = document.title || 'untitled page';
				});
			}
		});

		mounted = true;
		return unsubscribe;
	});
</script>

{#if constructors[1]}
	<svelte:component this={constructors[0]} bind:this={components[0]} data={data_0}>
		<svelte:component this={constructors[1]} bind:this={components[1]} data={data_1} {form} />
	</svelte:component>
{:else}
	<svelte:component this={constructors[0]} bind:this={components[0]} data={data_0} {form} />
{/if}

{#if mounted}
	<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
		{#if navigated}
			{title}
		{/if}
	</div>
{/if}

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\app.js
==================================================
export { matchers } from './matchers.js';

export const nodes = [
	() => import('./nodes/0'),
	() => import('./nodes/1'),
	() => import('./nodes/2'),
	() => import('./nodes/3'),
	() => import('./nodes/4'),
	() => import('./nodes/5'),
	() => import('./nodes/6'),
	() => import('./nodes/7')
];

export const server_loads = [];

export const dictionary = {
		"/": [2],
		"/login": [3],
		"/profile": [4],
		"/tickets": [5],
		"/tickets/new": [7],
		"/tickets/[id]": [6]
	};

export const hooks = {
	handleError: (({ error }) => { console.error(error) }),
};

export { default as root } from '../root.svelte';

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\matchers.js
==================================================
export const matchers = {};

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\nodes\0.js
==================================================
export { default as component } from "../../../../src/routes/+layout.svelte";

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\nodes\1.js
==================================================
export { default as component } from "../../../../node_modules/@sveltejs/kit/src/runtime/components/error.svelte";

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\nodes\2.js
==================================================
export { default as component } from "../../../../src/routes/+page.svelte";

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\nodes\3.js
==================================================
export { default as component } from "../../../../src/routes/login/+page.svelte";

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\nodes\4.js
==================================================
export { default as component } from "../../../../src/routes/profile/+page.svelte";

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\nodes\5.js
==================================================
export { default as component } from "../../../../src/routes/tickets/+page.svelte";

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\nodes\6.js
==================================================
export { default as component } from "../../../../src/routes/tickets/[id]/+page.svelte";

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\client\nodes\7.js
==================================================
export { default as component } from "../../../../src/routes/tickets/new/+page.svelte";

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\generated\server\internal.js
==================================================

import root from '../root.svelte';
import { set_building } from '__sveltekit/environment';
import { set_assets } from '__sveltekit/paths';
import { set_private_env, set_public_env } from '../../../node_modules/@sveltejs/kit/src/runtime/shared-server.js';

export const options = {
	app_template_contains_nonce: false,
	csp: {"mode":"auto","directives":{"upgrade-insecure-requests":false,"block-all-mixed-content":false},"reportOnly":{"upgrade-insecure-requests":false,"block-all-mixed-content":false}},
	csrf_check_origin: true,
	track_server_fetches: false,
	embedded: false,
	env_public_prefix: 'PUBLIC_',
	env_private_prefix: '',
	hooks: null, // added lazily, via `get_hooks`
	preload_strategy: "modulepreload",
	root,
	service_worker: false,
	templates: {
		app: ({ head, body, assets, nonce, env }) => "<!DOCTYPE html>\r\n<html lang=\"es\">\r\n\t<head>\r\n\t\t<meta charset=\"utf-8\" />\r\n\t\t<link rel=\"icon\" href=\"" + assets + "/favicon.png\" />\r\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\r\n\t\t<meta name=\"theme-color\" content=\"#3b82f6\" />\r\n\t\t\r\n\t\t<!-- SEO Meta Tags -->\r\n\t\t<meta name=\"description\" content=\"ServiceManager - Portal de Soporte Técnico para Clientes\" />\r\n\t\t<meta name=\"keywords\" content=\"soporte técnico, mesa de ayuda, tickets, aduanasoft\" />\r\n\t\t<meta name=\"author\" content=\"Aduanasoft\" />\r\n\t\t\r\n\t\t<!-- Open Graph Meta Tags -->\r\n\t\t<meta property=\"og:type\" content=\"website\" />\r\n\t\t<meta property=\"og:title\" content=\"ServiceManager - Portal Cliente\" />\r\n\t\t<meta property=\"og:description\" content=\"Gestiona tus tickets de soporte técnico\" />\r\n\t\t<meta property=\"og:site_name\" content=\"ServiceManager\" />\r\n\t\t\r\n\t\t<!-- Fonts -->\r\n\t\t<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\r\n\t\t<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\r\n\t\t<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\r\n\t\t\r\n\t\t" + head + "\r\n\t</head>\r\n\t<body data-sveltekit-preload-data=\"hover\" class=\"min-h-screen bg-gray-50 antialiased\">\r\n\t\t<div style=\"display: contents\">" + body + "</div>\r\n\t</body>\r\n</html>",
		error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
	},
	version_hash: "3bl7rp"
};

export function get_hooks() {
	return {};
}

export { set_assets, set_building, set_private_env, set_public_env };


==================================================
ARCHIVO: .\frontend-client\.svelte-kit\types\route_meta_data.json
==================================================
{
	"/": [],
	"/login": [],
	"/profile": [],
	"/tickets": [],
	"/tickets/new": [],
	"/tickets/[id]": []
}

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\types\src\routes\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<LayoutData>;
type LayoutRouteId = RouteId | "/" | "/login" | "/profile" | "/tickets" | "/tickets/[id]" | "/tickets/new" | null
type LayoutParams = RouteParams & { id?: string }
type LayoutParentData = EnsureDefined<{}>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;
export type LayoutServerData = null;
export type LayoutData = Expand<LayoutParentData>;

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\types\src\routes\login\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/login';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\types\src\routes\profile\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/profile';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\types\src\routes\tickets\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/tickets';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\types\src\routes\tickets\new\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/tickets/new';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-client\.svelte-kit\types\src\routes\tickets\[id]\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = { id: string };
type RouteId = '/tickets/[id]';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../../$types.js').LayoutData>;

export type EntryGenerator = () => Promise<Array<RouteParams>> | Array<RouteParams>;
export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-client\src\app.css
==================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom base styles */
@layer base {
  html {
    font-family: 'Inter', system-ui, sans-serif;
  }
  
  body {
    @apply text-gray-900 bg-gray-50;
  }
  
  /* Focus styles */
  *:focus {
    @apply outline-none ring-2 ring-primary-500 ring-offset-2;
  }
  
  /* Selection styles */
  ::selection {
    @apply bg-primary-100 text-primary-900;
  }
}

/* Custom component styles */
@layer components {
  /* Card component */
  .card {
    @apply bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden;
  }
  
  .card-content {
    @apply p-6;
  }
  
  /* Button variants */
  .btn {
    @apply inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none px-4 py-2;
  }
  
  .btn-primary {
    @apply bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800;
  }
  
  .btn-secondary {
    @apply bg-secondary-100 text-secondary-900 hover:bg-secondary-200 active:bg-secondary-300;
  }
  
  .btn-success {
    @apply bg-success-600 text-white hover:bg-success-700 active:bg-success-800;
  }
  
  .btn-warning {
    @apply bg-warning-600 text-white hover:bg-warning-700 active:bg-warning-800;
  }
  
  .btn-error {
    @apply bg-error-600 text-white hover:bg-error-700 active:bg-error-800;
  }
  
  .btn-ghost {
    @apply bg-transparent hover:bg-secondary-100 active:bg-secondary-200;
  }
  
  /* Card styles */
  .card {
    @apply bg-white rounded-lg shadow-sm border border-gray-200;
  }
  
  .card-header {
    @apply p-6 border-b border-gray-200;
  }
  
  .card-content {
    @apply p-6;
  }
  
  .card-footer {
    @apply p-6 border-t border-gray-200 bg-gray-50 rounded-b-lg;
  }
  
  /* Form styles */
  .form-input {
    @apply block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm;
  }
  
  .form-label {
    @apply block text-sm font-medium text-gray-700 mb-1;
  }
  
  .form-error {
    @apply text-sm text-error-600 mt-1;
  }
  
  /* Status badges */
  .badge {
    @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
  }
  
  .badge-new {
    @apply badge bg-blue-100 text-blue-800;
  }
  
  .badge-in-progress {
    @apply badge bg-yellow-100 text-yellow-800;
  }
  
  .badge-waiting {
    @apply badge bg-orange-100 text-orange-800;
  }
  
  .badge-resolved {
    @apply badge bg-green-100 text-green-800;
  }
  
  .badge-closed {
    @apply badge bg-gray-100 text-gray-800;
  }
  
  .badge-reopened {
    @apply badge bg-red-100 text-red-800;
  }
  
  /* Priority badges */
  .badge-priority-low {
    @apply badge bg-gray-100 text-gray-600;
  }
  
  .badge-priority-medium {
    @apply badge bg-blue-100 text-blue-700;
  }
  
  .badge-priority-high {
    @apply badge bg-orange-100 text-orange-700;
  }
  
  .badge-priority-urgent {
    @apply badge bg-red-100 text-red-700;
  }
}

/* Custom utility classes */
@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
  
  /* Loading spinner */
  .spinner {
    @apply animate-spin rounded-full border-2 border-gray-300 border-t-primary-600;
  }
  
  /* Animations */
  .animate-fade-in {
    animation: fadeIn 0.3s ease-in-out;
  }
  
  .animate-slide-up {
    animation: slideUp 0.3s ease-out;
  }
}

/* Custom keyframes */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

==================================================
ARCHIVO: .\frontend-client\src\app.html
==================================================
<!DOCTYPE html>
<html lang="es">
	<head>
		<meta charset="utf-8" />
		<link rel="icon" href="%sveltekit.assets%/favicon.png" />
		<meta name="viewport" content="width=device-width, initial-scale=1" />
		<meta name="theme-color" content="#3b82f6" />
		
		<!-- SEO Meta Tags -->
		<meta name="description" content="ServiceManager - Portal de Soporte Técnico para Clientes" />
		<meta name="keywords" content="soporte técnico, mesa de ayuda, tickets, aduanasoft" />
		<meta name="author" content="Aduanasoft" />
		
		<!-- Open Graph Meta Tags -->
		<meta property="og:type" content="website" />
		<meta property="og:title" content="ServiceManager - Portal Cliente" />
		<meta property="og:description" content="Gestiona tus tickets de soporte técnico" />
		<meta property="og:site_name" content="ServiceManager" />
		
		<!-- Fonts -->
		<link rel="preconnect" href="https://fonts.googleapis.com">
		<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
		<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
		
		%sveltekit.head%
	</head>
	<body data-sveltekit-preload-data="hover" class="min-h-screen bg-gray-50 antialiased">
		<div style="display: contents">%sveltekit.body%</div>
	</body>
</html>

==================================================
ARCHIVO: .\frontend-client\src\lib\components\Header.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { auth } from '$lib/stores/auth.js';
  import Icon from './Icon.svelte';
  
  export let showLogo = true;
  export let showNavigation = true;
  
  let isMenuOpen = false;
  
  onMount(() => {
    auth.init();
  });
  
  function toggleMenu() {
    isMenuOpen = !isMenuOpen;
  }
  
  function handleLogout() {
    auth.logout();
    isMenuOpen = false;
  }
</script>

<header class="bg-white shadow-sm border-b border-gray-200">
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
    <div class="flex justify-between items-center h-16">
      <!-- Logo -->
      {#if showLogo}
        <div class="flex items-center">
          <a href="/" class="flex items-center space-x-2">
            <div class="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center">
              <Icon name="ticket" size="w-5 h-5" className="text-white" />
            </div>
            <div class="hidden sm:block">
              <h1 class="text-xl font-semibold text-gray-900">ServiceManager</h1>
              <p class="text-xs text-gray-500">Mesa de Ayuda</p>
            </div>
          </a>
        </div>
      {/if}

      <!-- Navigation -->
      {#if showNavigation && $auth.isAuthenticated}
        <nav class="hidden md:flex space-x-8">
          <a href="/tickets" class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium">
            Mis Tickets
          </a>
          <a href="/tickets/new" class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium">
            Crear Ticket
          </a>
        </nav>
      {/if}

      <!-- User menu -->
      <div class="flex items-center space-x-4">
        {#if $auth.isAuthenticated}
          <div class="relative">
            <button
              on:click={toggleMenu}
              class="flex items-center space-x-2 text-gray-700 hover:text-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-md p-2"
            >
              <div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
                <span class="text-primary-600 text-sm font-medium">
                  {$auth.user?.first_name?.[0]}{$auth.user?.last_name?.[0]}
                </span>
              </div>
              <span class="hidden sm:block text-sm">
                {$auth.user?.first_name} {$auth.user?.last_name}
              </span>
              <Icon name="chevronDown" size="w-4 h-4" />
            </button>

            {#if isMenuOpen}
              <div class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-50">
                <div class="py-1">
                  <div class="px-4 py-2 text-xs text-gray-500 border-b border-gray-200">
                    {$auth.user?.email}
                  </div>
                  <a
                    href="/profile"
                    class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                    on:click={() => isMenuOpen = false}
                  >
                    Mi Perfil
                  </a>
                  <button
                    on:click={handleLogout}
                    class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                  >
                    Cerrar Sesión
                  </button>
                </div>
              </div>
            {/if}
          </div>
        {:else}
          <a
            href="/login"
            class="text-gray-700 hover:text-primary-600 text-sm font-medium"
          >
            Iniciar Sesión
          </a>
        {/if}
      </div>
    </div>

    <!-- Mobile navigation -->
    {#if showNavigation && $auth.isAuthenticated}
      <div class="md:hidden border-t border-gray-200 py-2">
        <nav class="flex space-x-4">
          <a href="/tickets" class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium">
            Mis Tickets
          </a>
          <a href="/tickets/new" class="text-gray-700 hover:text-primary-600 px-3 py-2 text-sm font-medium">
            Crear Ticket
          </a>
        </nav>
      </div>
    {/if}
  </div>
</header>

<!-- Backdrop for mobile menu -->
{#if isMenuOpen}
  <div 
    class="fixed inset-0 z-40 md:hidden" 
    on:click={() => isMenuOpen = false}
  ></div>
{/if}

==================================================
ARCHIVO: .\frontend-client\src\lib\components\Icon.svelte
==================================================
<script lang="ts">
  export let name: string;
  export let size: string = 'w-5 h-5';
  export let className: string = '';

  const icons: Record<string, string> = {
    home: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
    ticket: 'M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z',
    plus: 'M12 4v16m8-8H4',
    user: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z',
    menu: 'M4 6h16M4 12h16M4 18h16',
    x: 'M6 18L18 6M6 6l12 12',
    bell: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9',
    search: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
    edit: 'M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z',
    trash: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',
    eye: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z',
    clock: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
    check: 'M5 13l4 4L19 7',
    chevronDown: 'M19 9l-7 7-7-7'
  };

  $: path = icons[name] || icons.home;
</script>

<svg 
  class="{size} {className}" 
  fill="none" 
  stroke="currentColor" 
  viewBox="0 0 24 24" 
  xmlns="http://www.w3.org/2000/svg"
>
  <path 
    stroke-linecap="round" 
    stroke-linejoin="round" 
    stroke-width="2" 
    d={path}
  />
</svg>

==================================================
ARCHIVO: .\frontend-client\src\lib\components\TicketCard.svelte
==================================================
<script lang="ts">
  export let ticket: import('$lib/stores/tickets').Ticket;
  
  // Status mapping
  const statusConfig = {
    NEW: { label: 'Nuevo', class: 'badge-new' },
    IN_PROGRESS: { label: 'En Progreso', class: 'badge-in-progress' },
    WAITING_FOR_CLIENT: { label: 'Esperando Cliente', class: 'badge-waiting' },
    RESOLVED: { label: 'Resuelto', class: 'badge-resolved' },
    CLOSED: { label: 'Cerrado', class: 'badge-closed' },
    REOPENED: { label: 'Reabierto', class: 'badge-reopened' }
  };
  
  // Priority mapping
  const priorityConfig = {
    LOW: { label: 'Baja', class: 'badge-priority-low' },
    MEDIUM: { label: 'Media', class: 'badge-priority-medium' },
    HIGH: { label: 'Alta', class: 'badge-priority-high' },
    URGENT: { label: 'Urgente', class: 'badge-priority-urgent' }
  };
  
  // Format date
  function formatDate(dateString: string): string {
    return new Date(dateString).toLocaleDateString('es-ES', {
      day: '2-digit',
      month: '2-digit',
      year: 'numeric',
      hour: '2-digit',
      minute: '2-digit'
    });
  }
  
  // Format relative time
  function formatRelativeTime(dateString: string): string {
    const date = new Date(dateString);
    const now = new Date();
    const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
    
    if (diffInMinutes < 1) return 'hace un momento';
    if (diffInMinutes < 60) return `hace ${diffInMinutes}m`;
    
    const diffInHours = Math.floor(diffInMinutes / 60);
    if (diffInHours < 24) return `hace ${diffInHours}h`;
    
    const diffInDays = Math.floor(diffInHours / 24);
    if (diffInDays < 7) return `hace ${diffInDays}d`;
    
    return formatDate(dateString);
  }
</script>

<div class="card hover:shadow-md transition-shadow">
  <div class="card-content">
    <div class="flex justify-between items-start mb-3">
      <h3 class="text-lg font-medium text-gray-900 line-clamp-2">
        <a href="/tickets/{ticket.id}" class="hover:text-primary-600">
          {ticket.title}
        </a>
      </h3>
      <div class="flex items-center space-x-2 ml-4">
        <span class={`${statusConfig[ticket.status].class}`}>
          {statusConfig[ticket.status].label}
        </span>
        <span class={`${priorityConfig[ticket.priority].class}`}>
          {priorityConfig[ticket.priority].label}
        </span>
      </div>
    </div>
    
    <p class="text-gray-600 text-sm line-clamp-3 mb-4">
      {ticket.description}
    </p>
    
    <div class="flex justify-between items-center text-xs text-gray-500">
      <div class="flex items-center space-x-4">
        <span>#{ticket.id.substring(0, 8)}</span>
        {#if ticket.category_name}
          <span class="bg-gray-100 text-gray-600 px-2 py-1 rounded">
            {ticket.category_name}
          </span>
        {/if}
        {#if ticket.assigned_to_name}
          <span class="flex items-center space-x-1">
            <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
            </svg>
            <span>{ticket.assigned_to_name}</span>
          </span>
        {/if}
      </div>
      
      <div class="flex items-center space-x-3">
        {#if ticket.due_date}
          <span class="flex items-center space-x-1 {new Date(ticket.due_date) < new Date() ? 'text-red-600' : ''}">
            <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
            <span>Vence {formatRelativeTime(ticket.due_date)}</span>
          </span>
        {/if}
        
        <span title={formatDate(ticket.updated_at)}>
          Actualizado {formatRelativeTime(ticket.updated_at)}
        </span>
      </div>
    </div>
  </div>
</div>

<style>
  .line-clamp-2 {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  
  .line-clamp-3 {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
</style>

==================================================
ARCHIVO: .\frontend-client\src\lib\components\Toast.svelte
==================================================
<script lang="ts">
  export let type: 'success' | 'error' | 'warning' | 'info' = 'info';
  export let message: string;
  export let duration: number = 5000;
  export let dismissible: boolean = true;
  
  let visible = true;
  let timeoutId: NodeJS.Timeout;
  
  // Auto-dismiss after duration
  if (duration > 0) {
    timeoutId = setTimeout(() => {
      visible = false;
    }, duration);
  }
  
  function dismiss() {
    if (timeoutId) clearTimeout(timeoutId);
    visible = false;
  }
  
  // Cleanup timeout on destroy
  import { onDestroy } from 'svelte';
  onDestroy(() => {
    if (timeoutId) clearTimeout(timeoutId);
  });
  
  // Style mapping
  const typeStyles = {
    success: {
      container: 'bg-success-50 border-success-200 text-success-800',
      icon: 'text-success-400'
    },
    error: {
      container: 'bg-error-50 border-error-200 text-error-800',
      icon: 'text-error-400'
    },
    warning: {
      container: 'bg-warning-50 border-warning-200 text-warning-800',
      icon: 'text-warning-400'
    },
    info: {
      container: 'bg-blue-50 border-blue-200 text-blue-800',
      icon: 'text-blue-400'
    }
  };
  
  // Icon mapping
  const typeIcons = {
    success: 'M5 13l4 4L19 7',
    error: 'M6 18L18 6M6 6l12 12',
    warning: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z',
    info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
  };
</script>

{#if visible}
  <div class="fixed top-4 right-4 max-w-sm w-full z-50 animate-slide-up">
    <div class="rounded-lg border p-4 shadow-lg {typeStyles[type].container}">
      <div class="flex items-start">
        <div class="flex-shrink-0">
          <svg 
            class="h-5 w-5 {typeStyles[type].icon}" 
            fill="none" 
            stroke="currentColor" 
            viewBox="0 0 24 24"
          >
            <path 
              stroke-linecap="round" 
              stroke-linejoin="round" 
              stroke-width="2" 
              d={typeIcons[type]}
            />
          </svg>
        </div>
        
        <div class="ml-3 flex-1">
          <p class="text-sm font-medium">
            {message}
          </p>
        </div>
        
        {#if dismissible}
          <div class="ml-4 flex-shrink-0">
            <button
              type="button"
              class="inline-flex rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 {typeStyles[type].icon} hover:opacity-75"
              on:click={dismiss}
            >
              <span class="sr-only">Cerrar</span>
              <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
              </svg>
            </button>
          </div>
        {/if}
      </div>
    </div>
  </div>
{/if}

<style>
  @keyframes slide-up {
    from {
      transform: translateY(-100%);
      opacity: 0;
    }
    to {
      transform: translateY(0);
      opacity: 1;
    }
  }
  
  .animate-slide-up {
    animation: slide-up 0.3s ease-out;
  }
</style>

==================================================
ARCHIVO: .\frontend-client\src\lib\stores\app.ts
==================================================
import { writable } from 'svelte/store';
import { auth } from './auth.js';
import { get } from 'svelte/store';
import type { Writable } from 'svelte/store';

// Types
export interface Category {
  id: string;
  name: string;
  description: string;
  is_active: boolean;
  tenant_id: string;
  created_at: string;
}

export interface AppState {
  categories: Category[];
  isLoading: boolean;
  error: string | null;
}

// Initial state
const initialState: AppState = {
  categories: [],
  isLoading: false,
  error: null
};

// API helper function
async function apiCall(endpoint: string, options: RequestInit = {}) {
  const authState = get(auth);
  
  const response = await fetch(`/api/v1${endpoint}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${authState.token}`,
      ...options.headers
    }
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.detail || 'Request failed');
  }

  return response.json();
}

// Create app store
function createAppStore() {
  const { subscribe, set, update }: Writable<AppState> = writable(initialState);

  return {
    subscribe,

    // Load categories
    loadCategories: async () => {
      update(state => ({ ...state, isLoading: true, error: null }));
      
      try {
        const categories = await apiCall('/categories/');
        update(state => ({ ...state, categories, isLoading: false }));
      } catch (error) {
        update(state => ({ 
          ...state, 
          isLoading: false, 
          error: error instanceof Error ? error.message : 'Failed to load categories' 
        }));
      }
    },

    // Clear error
    clearError: () => {
      update(state => ({ ...state, error: null }));
    }
  };
}

export const app = createAppStore();

==================================================
ARCHIVO: .\frontend-client\src\lib\stores\auth.ts
==================================================
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';

// Types
export interface User {
  id: string;
  email: string;
  first_name: string;
  last_name: string;
  tenant_id: string;
  role: 'CLIENT_ADMIN' | 'CLIENT_USER';
  is_active: boolean;
  is_two_factor_enabled: boolean;
  created_at: string;
}

export interface AuthState {
  user: User | null;
  token: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
}

export interface LoginRequest {
  email: string;
  password: string;
  tenant_slug: string;
  totp_code?: string;
}

export interface LoginResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
  user: User;
}

// Initial state
const initialState: AuthState = {
  user: null,
  token: null,
  isAuthenticated: false,
  isLoading: false
};

// Create auth store
function createAuthStore() {
  const { subscribe, set, update }: Writable<AuthState> = writable(initialState);

  return {
    subscribe,
    
    // Initialize auth from localStorage
    init: () => {
      if (typeof window !== 'undefined') {
        const token = localStorage.getItem('auth_token');
        const user = localStorage.getItem('auth_user');
        
        if (token && user) {
          try {
            const parsedUser = JSON.parse(user);
            set({
              user: parsedUser,
              token,
              isAuthenticated: true,
              isLoading: false
            });
          } catch (error) {
            console.error('Error parsing stored auth data:', error);
            localStorage.removeItem('auth_token');
            localStorage.removeItem('auth_user');
          }
        }
      }
    },

    // Login
    login: async (credentials: LoginRequest): Promise<void> => {
      update(state => ({ ...state, isLoading: true }));
      
      try {
        const response = await fetch('/api/v1/auth/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(credentials)
        });

        if (!response.ok) {
          const error = await response.json();
          throw new Error(error.detail || 'Login failed');
        }

        const data: LoginResponse = await response.json();
        
        // Store auth data
        if (typeof window !== 'undefined') {
          localStorage.setItem('auth_token', data.access_token);
          localStorage.setItem('auth_user', JSON.stringify(data.user));
        }

        set({
          user: data.user,
          token: data.access_token,
          isAuthenticated: true,
          isLoading: false
        });
      } catch (error) {
        update(state => ({ ...state, isLoading: false }));
        throw error;
      }
    },

    // Logout
    logout: () => {
      if (typeof window !== 'undefined') {
        localStorage.removeItem('auth_token');
        localStorage.removeItem('auth_user');
      }
      set(initialState);
    },

    // Update user data
    updateUser: (user: User) => {
      update(state => ({ ...state, user }));
      if (typeof window !== 'undefined') {
        localStorage.setItem('auth_user', JSON.stringify(user));
      }
    },

    // Set loading state
    setLoading: (isLoading: boolean) => {
      update(state => ({ ...state, isLoading }));
    }
  };
}

export const auth = createAuthStore();

==================================================
ARCHIVO: .\frontend-client\src\lib\stores\clientes.ts
==================================================
import { writable } from 'svelte/store';

export const clients = writable([]);

export async function fetchClients() {
  const response = await fetch('/api/v1/clients');
  const data = await response.json();
  clients.set(data);
}

export async function createClient(client) {
  const response = await fetch('/api/v1/clients', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(client),
  });
  if (response.ok) {
    fetchClients();
  }
}

export async function updateClient(clientId, client) {
  const response = await fetch(`/api/v1/clients/${clientId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(client),
  });
  if (response.ok) {
    fetchClients();
  }
}

export async function deleteClient(clientId) {
  const response = await fetch(`/api/v1/clients/${clientId}`, {
    method: 'DELETE',
  });
  if (response.ok) {
    fetchClients();
  }
}

==================================================
ARCHIVO: .\frontend-client\src\lib\stores\tickets.ts
==================================================
import { writable } from 'svelte/store';
import { auth } from './auth.js';
import { get } from 'svelte/store';
import type { Writable } from 'svelte/store';

// Types
export interface Ticket {
  id: string;
  title: string;
  description: string;
  status: 'NEW' | 'IN_PROGRESS' | 'WAITING_FOR_CLIENT' | 'RESOLVED' | 'CLOSED' | 'REOPENED';
  priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
  category_id: string;
  category_name?: string;
  client_id: string;
  assigned_to_id: string | null;
  assigned_to_name?: string;
  created_at: string;
  updated_at: string;
  due_date: string | null;
  resolution: string | null;
}

export interface TicketComment {
  id: string;
  ticket_id: string;
  user_id: string;
  user_name: string;
  user_role: string;
  content: string;
  is_internal: boolean;
  created_at: string;
}

export interface TicketAttachment {
  id: string;
  ticket_id: string;
  filename: string;
  original_filename: string;
  mime_type: string;
  size_bytes: number;
  uploaded_by_id: string;
  uploaded_by_name: string;
  uploaded_at: string;
}

export interface CreateTicketRequest {
  title: string;
  description: string;
  category_id: string;
  priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
}

export interface TicketsState {
  tickets: Ticket[];
  currentTicket: Ticket | null;
  comments: TicketComment[];
  attachments: TicketAttachment[];
  isLoading: boolean;
  error: string | null;
}

// Initial state
const initialState: TicketsState = {
  tickets: [],
  currentTicket: null,
  comments: [],
  attachments: [],
  isLoading: false,
  error: null
};

// API helper function
async function apiCall(endpoint: string, options: RequestInit = {}) {
  const authState = get(auth);
  
  const response = await fetch(`/api/v1${endpoint}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${authState.token}`,
      ...options.headers
    }
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.detail || 'Request failed');
  }

  return response.json();
}

// Create tickets store
function createTicketsStore() {
  const { subscribe, set, update }: Writable<TicketsState> = writable(initialState);

  return {
    subscribe,

    // Load user's tickets
    loadTickets: async () => {
      update(state => ({ ...state, isLoading: true, error: null }));
      
      try {
        const tickets = await apiCall('/tickets/');
        update(state => ({ ...state, tickets, isLoading: false }));
      } catch (error) {
        update(state => ({ 
          ...state, 
          isLoading: false, 
          error: error instanceof Error ? error.message : 'Failed to load tickets' 
        }));
      }
    },

    // Load specific ticket with details
    loadTicket: async (ticketId: string) => {
      update(state => ({ ...state, isLoading: true, error: null }));
      
      try {
        const [ticket, comments, attachments] = await Promise.all([
          apiCall(`/tickets/${ticketId}`),
          apiCall(`/tickets/${ticketId}/comments`),
          apiCall(`/tickets/${ticketId}/attachments`)
        ]);

        update(state => ({ 
          ...state, 
          currentTicket: ticket, 
          comments, 
          attachments, 
          isLoading: false 
        }));
      } catch (error) {
        update(state => ({ 
          ...state, 
          isLoading: false, 
          error: error instanceof Error ? error.message : 'Failed to load ticket' 
        }));
      }
    },

    // Create new ticket
    createTicket: async (ticket: CreateTicketRequest) => {
      update(state => ({ ...state, isLoading: true, error: null }));
      
      try {
        const newTicket = await apiCall('/tickets/', {
          method: 'POST',
          body: JSON.stringify(ticket)
        });

        update(state => ({ 
          ...state, 
          tickets: [newTicket, ...state.tickets], 
          isLoading: false 
        }));

        return newTicket;
      } catch (error) {
        update(state => ({ 
          ...state, 
          isLoading: false, 
          error: error instanceof Error ? error.message : 'Failed to create ticket' 
        }));
        throw error;
      }
    },

    // Add comment to ticket
    addComment: async (ticketId: string, content: string) => {
      try {
        const comment = await apiCall(`/tickets/${ticketId}/comments`, {
          method: 'POST',
          body: JSON.stringify({ content })
        });

        update(state => ({ 
          ...state, 
          comments: [...state.comments, comment] 
        }));

        return comment;
      } catch (error) {
        update(state => ({ 
          ...state, 
          error: error instanceof Error ? error.message : 'Failed to add comment' 
        }));
        throw error;
      }
    },

    // Upload attachment
    uploadAttachment: async (ticketId: string, file: File) => {
      try {
        const formData = new FormData();
        formData.append('file', file);

        const authState = get(auth);
        const response = await fetch(`/api/v1/tickets/${ticketId}/attachments`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${authState.token}`
          },
          body: formData
        });

        if (!response.ok) {
          const error = await response.json();
          throw new Error(error.detail || 'Upload failed');
        }

        const attachment = await response.json();
        
        update(state => ({ 
          ...state, 
          attachments: [...state.attachments, attachment] 
        }));

        return attachment;
      } catch (error) {
        update(state => ({ 
          ...state, 
          error: error instanceof Error ? error.message : 'Failed to upload attachment' 
        }));
        throw error;
      }
    },

    // Close ticket (client can close their own tickets)
    closeTicket: async (ticketId: string, resolution?: string) => {
      try {
        const updatedTicket = await apiCall(`/tickets/${ticketId}/close`, {
          method: 'PATCH',
          body: JSON.stringify({ resolution })
        });

        update(state => ({
          ...state,
          currentTicket: state.currentTicket?.id === ticketId ? updatedTicket : state.currentTicket,
          tickets: state.tickets.map(t => t.id === ticketId ? updatedTicket : t)
        }));

        return updatedTicket;
      } catch (error) {
        update(state => ({ 
          ...state, 
          error: error instanceof Error ? error.message : 'Failed to close ticket' 
        }));
        throw error;
      }
    },

    // Clear error
    clearError: () => {
      update(state => ({ ...state, error: null }));
    },

    // Clear current ticket
    clearCurrentTicket: () => {
      update(state => ({ 
        ...state, 
        currentTicket: null, 
        comments: [], 
        attachments: [] 
      }));
    }
  };
}

export const tickets = createTicketsStore();

==================================================
ARCHIVO: .\frontend-client\src\lib\stores\toast.ts
==================================================
// Toast notification store
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';

export interface ToastMessage {
  id: string;
  type: 'success' | 'error' | 'warning' | 'info';
  message: string;
  duration?: number;
}

interface ToastState {
  toasts: ToastMessage[];
}

const initialState: ToastState = {
  toasts: []
};

function createToastStore() {
  const { subscribe, update }: Writable<ToastState> = writable(initialState);

  return {
    subscribe,
    
    show: (type: ToastMessage['type'], message: string, duration = 5000) => {
      const id = Math.random().toString(36).substring(2, 9);
      const toast: ToastMessage = { id, type, message, duration };
      
      update(state => ({
        toasts: [...state.toasts, toast]
      }));
      
      // Auto-remove after duration
      if (duration > 0) {
        setTimeout(() => {
          update(state => ({
            toasts: state.toasts.filter(t => t.id !== id)
          }));
        }, duration);
      }
      
      return id;
    },
    
    dismiss: (id: string) => {
      update(state => ({
        toasts: state.toasts.filter(t => t.id !== id)
      }));
    },
    
    clear: () => {
      update(() => initialState);
    },
    
    // Convenience methods
    success: (message: string, duration?: number) => {
      return createToastStore().show('success', message, duration);
    },
    
    error: (message: string, duration?: number) => {
      return createToastStore().show('error', message, duration);
    },
    
    warning: (message: string, duration?: number) => {
      return createToastStore().show('warning', message, duration);
    },
    
    info: (message: string, duration?: number) => {
      return createToastStore().show('info', message, duration);
    }
  };
}

export const toast = createToastStore();

==================================================
ARCHIVO: .\frontend-client\src\routes\+layout.svelte
==================================================
<script lang="ts">
  import Header from '$lib/components/Header.svelte';
  import { toast } from '$lib/stores/toast.js';
  import Toast from '$lib/components/Toast.svelte';
  import { onMount } from 'svelte';
  import { auth } from '$lib/stores/auth.js';
  import { page } from '$app/stores';
  import '../app.css';

  onMount(() => {
    auth.init();
  });

  $: showHeader = !$page.url.pathname.startsWith('/login') && !$page.url.pathname.startsWith('/register');
</script>

<div class="min-h-screen bg-gray-50 font-sans">
  {#if showHeader}
    <Header />
  {/if}
  
  <main class="flex-1">
    <slot />
  </main>
  
  <!-- Toast notifications -->
  {#each $toast.toasts as toastMessage (toastMessage.id)}
    <Toast
      type={toastMessage.type}
      message={toastMessage.message}
      duration={toastMessage.duration}
      on:dismiss={() => toast.dismiss(toastMessage.id)}
    />
  {/each}
</div>

==================================================
ARCHIVO: .\frontend-client\src\routes\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { auth } from '$lib/stores/auth.js';
  import { tickets } from '$lib/stores/tickets.js';
  import { goto } from '$app/navigation';
  import Icon from '$lib/components/Icon.svelte';
  
  onMount(() => {
    // Redirect if not authenticated
    if (!$auth.isAuthenticated) {
      goto('/login');
      return;
    }
    
    // Load user's tickets
    tickets.loadTickets();
  });
</script>

<svelte:head>
  <title>ServiceManager - Mesa de Ayuda</title>
</svelte:head>

<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
  <!-- Welcome Section -->
  <div class="bg-gradient-to-r from-primary-500 to-primary-600 rounded-lg p-8 text-white mb-8">
    <div class="max-w-3xl">
      <h1 class="text-3xl font-bold mb-2">
        Bienvenido, {$auth.user?.first_name} {$auth.user?.last_name}
      </h1>
      <p class="text-primary-100 text-lg">
        Gestiona tus tickets de soporte de manera eficiente. Crea nuevos tickets, 
        da seguimiento a los existentes y mantente actualizado con el estado de tus solicitudes.
      </p>
    </div>
  </div>
  
  <!-- Quick Actions -->
  <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
    <a 
      href="/tickets/new" 
      class="card hover:shadow-lg transition-shadow group cursor-pointer"
    >
      <div class="card-content text-center">
        <div class="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mx-auto mb-4 group-hover:bg-primary-200 transition-colors">
          <Icon name="plus" size="w-6 h-6" className="text-primary-600" />
        </div>
        <h3 class="text-lg font-medium text-gray-900 mb-2">Crear Ticket</h3>
        <p class="text-gray-600 text-sm">
          Reporta un problema o solicita soporte técnico
        </p>
      </div>
    </a>
    
    <a 
      href="/tickets" 
      class="card hover:shadow-lg transition-shadow group cursor-pointer"
    >
      <div class="card-content text-center">
        <div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mx-auto mb-4 group-hover:bg-blue-200 transition-colors">
          <Icon name="ticket" size="w-6 h-6" className="text-blue-600" />
        </div>
        <h3 class="text-lg font-medium text-gray-900 mb-2">Mis Tickets</h3>
        <p class="text-gray-600 text-sm">
          Consulta el estado de todos tus tickets
        </p>
      </div>
    </a>
    
    <a 
      href="/profile" 
      class="card hover:shadow-lg transition-shadow group cursor-pointer"
    >
      <div class="card-content text-center">
        <div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mx-auto mb-4 group-hover:bg-green-200 transition-colors">
          <Icon name="user" size="w-6 h-6" className="text-green-600" />
        </div>
        <h3 class="text-lg font-medium text-gray-900 mb-2">Mi Perfil</h3>
        <p class="text-gray-600 text-sm">
          Actualiza tu información personal
        </p>
      </div>
    </a>
  </div>
  
  <!-- Recent Tickets -->
  <div class="card">
    <div class="card-header">
      <h2 class="text-xl font-semibold text-gray-900">Tickets Recientes</h2>
      <p class="text-gray-600 mt-1">Últimos tickets que has creado o actualizado</p>
    </div>
    
    <div class="card-content">
      {#if $tickets.isLoading}
        <div class="text-center py-8">
          <div class="spinner w-8 h-8 mx-auto mb-4"></div>
          <p class="text-gray-600">Cargando tickets...</p>
        </div>
      {:else if $tickets.error}
        <div class="text-center py-8">
          <div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mx-auto mb-4">
            <svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
          </div>
          <p class="text-gray-600 mb-4">Error al cargar los tickets</p>
          <button 
            on:click={() => tickets.loadTickets()}
            class="btn-primary px-4 py-2"
          >
            Reintentar
          </button>
        </div>
      {:else if $tickets.tickets.length === 0}
        <div class="text-center py-8">
          <div class="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mx-auto mb-4">
            <svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
            </svg>
          </div>
          <p class="text-gray-600 mb-4">No tienes tickets creados</p>
          <a href="/tickets/new" class="btn-primary px-4 py-2">
            Crear tu primer ticket
          </a>
        </div>
      {:else}
        <div class="space-y-4">
          {#each $tickets.tickets.slice(0, 5) as ticket (ticket.id)}
            <div class="border border-gray-200 rounded-lg p-4 hover:bg-gray-50 transition-colors">
              <div class="flex justify-between items-start">
                <div class="flex-1">
                  <h3 class="font-medium text-gray-900 mb-1">
                    <a href="/tickets/{ticket.id}" class="hover:text-primary-600">
                      {ticket.title}
                    </a>
                  </h3>
                  <p class="text-gray-600 text-sm line-clamp-2 mb-2">
                    {ticket.description}
                  </p>
                  <div class="flex items-center space-x-4 text-xs text-gray-500">
                    <span>#{ticket.id.substring(0, 8)}</span>
                    <span>{new Date(ticket.created_at).toLocaleDateString('es-ES')}</span>
                  </div>
                </div>
                <div class="ml-4">
                  <span class="badge-{ticket.status.toLowerCase().replace('_', '-')}">
                    {ticket.status === 'NEW' ? 'Nuevo' : 
                     ticket.status === 'IN_PROGRESS' ? 'En Progreso' :
                     ticket.status === 'WAITING_FOR_CLIENT' ? 'Esperando Cliente' :
                     ticket.status === 'RESOLVED' ? 'Resuelto' :
                     ticket.status === 'CLOSED' ? 'Cerrado' : 'Reabierto'}
                  </span>
                </div>
              </div>
            </div>
          {/each}
          
          {#if $tickets.tickets.length > 5}
            <div class="text-center pt-4 border-t border-gray-200">
              <a href="/tickets" class="btn-secondary px-4 py-2">
                Ver todos los tickets ({$tickets.tickets.length})
              </a>
            </div>
          {/if}
        </div>
      {/if}
    </div>
  </div>
</div>

<style>
  .line-clamp-2 {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
</style>

==================================================
ARCHIVO: .\frontend-client\src\routes\login\+page.svelte
==================================================
<script lang="ts">
  import { auth } from '$lib/stores/auth.js';
  import { toast } from '$lib/stores/toast.js';
  import { goto } from '$app/navigation';
  import { onMount } from 'svelte';
  import Icon from '$lib/components/Icon.svelte';
  
  let email = '';
  let password = '';
  let totpCode = '';
  let isLoading = false;
  let showTwoFactor = false;
  let errorMessage = '';
  let showPassword = false;
  
  onMount(() => {
    // Redirect if already authenticated
    if ($auth.isAuthenticated) {
      goto('/');
    }
  });
  
  async function handleLogin() {
    if (!email || !password) {
      errorMessage = 'Por favor completa todos los campos';
      return;
    }
    
    isLoading = true;
    errorMessage = '';
    
    try {
      await auth.login({
        email,
        password,
        tenant_slug: 'aduanasoft', // Default tenant for now
        totp_code: totpCode || undefined
      });
      
      toast.success('¡Bienvenido! Has iniciado sesión correctamente');
      goto('/');
    } catch (error: any) {
      console.error('Login error:', error);
      
      // Check if 2FA is required
      if (error.message.includes('two-factor') || error.message.includes('2FA')) {
        showTwoFactor = true;
        errorMessage = 'Introduce el código de tu aplicación de autenticación';
      } else {
        errorMessage = error.message || 'Error al iniciar sesión';
        toast.error(errorMessage);
      }
    } finally {
      isLoading = false;
    }
  }
  
  function handleKeyDown(event: KeyboardEvent) {
    if (event.key === 'Enter') {
      handleLogin();
    }
  }
</script>


<div class="min-h-screen flex font-sans bg-white overflow-hidden">
  
  <!-- Left Side: Hero Image & Overlay (55% width) -->
  <div class="hidden lg:flex w-[55%] relative bg-gray-900">
    <!-- Background Image -->
    <div 
        class="absolute inset-0 bg-cover bg-center z-0" 
        style="background-image: url('/images/SOPORTE.webp'); opacity: 1;"
    ></div>
    
    <!-- Gradient Overlay -->
    <div class="absolute inset-0 bg-gradient-to-br from-[#1e3a8a]/75 to-[#172554]/75 z-10"></div>
    <div class="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent z-10"></div>

    <!-- Content -->
    <div class="relative z-20 w-full h-full flex flex-col justify-between p-16 text-white">
        <!-- Top Logo (Left) -->
        <div class="flex flex-col">
            <img src="/images/Logo%20AS%20blanco(1).png" alt="AduanaSoft" class="h-32 w-auto object-contain self-start drop-shadow-lg" />
        </div>

        <!-- Main Hero Text -->
        <div class="space-y-4 mb-12">
            <h2 class="text-5xl font-extrabold tracking-tight drop-shadow-xl leading-tight">
                Control Total <br/>
                de Servicios de TI
            </h2>
            <p class="text-lg text-blue-100/90 font-light max-w-lg leading-relaxed drop-shadow-md">
                Portal de atención a clientes. Genere tickets de soporte técnico para nuestros sistemas y reciba asistencia especializada para garantizar la continuidad de su operación.
            </p>
        </div>

        <!-- Bottom Footer -->
        <div class="text-xs font-bold tracking-[0.2em] text-blue-200/60 uppercase">
            ServiceManager Enterprise Platform
        </div>
    </div>
  </div>

  <!-- Right Side: Login Form (45% width) -->
  <div class="w-full lg:w-[45%] flex flex-col justify-center items-center p-8 lg:p-16 bg-white relative">
    
    <div class="w-full max-w-md space-y-8">
        <!-- Logo & Header -->
        <div class="text-center space-y-2">
            <h2 class="text-3xl font-bold text-gray-900">Bienvenido</h2>
            <p class="text-gray-500 text-sm">Ingrese a su cuenta corporativa</p>
        </div>

        <!-- Form -->
        <form on:submit|preventDefault={handleLogin} class="space-y-6 mt-8">
           {#if errorMessage}
             <div class="p-3 rounded-md bg-red-50 border border-red-100 flex items-center gap-3 animate-fade-in text-sm text-red-600">
               <Icon name="alert-circle" class="w-4 h-4 flex-shrink-0" />
               {errorMessage}
             </div>
           {/if}

           {#if !showTwoFactor}
             <div class="space-y-5">
               <!-- Email Input -->
               <div class="space-y-1.5">
                 <label for="email" class="block text-sm font-semibold text-gray-700">Correo Electrónico</label>
                 <div class="relative group">
                   <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                     <Icon name="mail" class="w-5 h-5 text-gray-400 group-focus-within:text-blue-600 transition-colors" />
                   </div>
                   <input
                     id="email"
                     type="email"
                     bind:value={email}
                     on:keydown={handleKeyDown}
                     class="block w-full pl-10 pr-3 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
                     placeholder="admin@aduanasoft.com"
                     required
                     disabled={isLoading}
                   />
                 </div>
               </div>

               <!-- Password Input -->
               <div class="space-y-1.5">
                 <label for="password" class="block text-sm font-semibold text-gray-700">Contraseña</label>
                 <div class="relative group">
                   <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                     <Icon name="lock" class="w-5 h-5 text-gray-400 group-focus-within:text-blue-600 transition-colors" />
                   </div>
                   {#if showPassword}
                     <input
                       id="password"
                       type="text"
                       bind:value={password}
                       on:keydown={handleKeyDown}
                       class="block w-full pl-10 pr-10 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
                       placeholder="••••••••"
                       required
                       disabled={isLoading}
                     />
                   {:else}
                     <input
                       id="password"
                       type="password"
                       bind:value={password}
                       on:keydown={handleKeyDown}
                       class="block w-full pl-10 pr-10 py-3 bg-[#fff9c4]/0 hover:bg-gray-50 focus:bg-white border text-gray-900 border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent transition-all duration-200 sm:text-sm"
                       placeholder="••••••••"
                       required
                       disabled={isLoading}
                     />
                   {/if}
                   <button 
                     type="button"
                     class="absolute inset-y-0 right-0 pr-3 flex items-center cursor-pointer text-gray-400 hover:text-gray-600 focus:outline-none"
                     on:click={() => showPassword = !showPassword}
                   >
                     <Icon name={showPassword ? 'eye-off' : 'eye'} class="w-5 h-5" />
                   </button>
                 </div>
               </div>
               
               <div class="flex items-center justify-between">
                 <div class="flex items-center">
                    <input id="remember-me" name="remember-me" type="checkbox" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded cursor-pointer">
                    <label for="remember-me" class="ml-2 block text-sm text-gray-500 cursor-pointer select-none">Recordar en este equipo</label>
                 </div>
                 <a href="/forgot-password" class="text-sm font-medium text-blue-600 hover:text-blue-500">
                    Olvide mi clave
                 </a>
               </div>
             </div>
           
           {:else}
             <!-- 2FA Input -->
             <div class="space-y-4 animate-slide-up">
               <label for="code" class="block text-sm font-medium text-gray-700 text-center">Código de Verificación (2FA)</label>
               <p class="text-xs text-center text-gray-500 mb-4">Ingrese el código de 6 dígitos</p>
               
               <div class="relative">
                 <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
                   <Icon name="shield-check" class="w-5 h-5 text-blue-500" />
                 </div>
                 <input
                   id="code"
                   type="text"
                   bind:value={totpCode}
                   on:keydown={handleKeyDown}
                   class="block w-full pl-10 py-3 text-center tracking-[0.5em] font-mono text-lg border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-600 focus:border-transparent"
                   placeholder="000000"
                   maxlength="6"
                   required
                   disabled={isLoading}
                   autofocus
                 />
               </div>
             </div>
           {/if}

           <div class="pt-2">
             <button
               type="submit"
               class="w-full flex justify-center py-3.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-bold text-white bg-blue-700 hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
               disabled={isLoading}
             >
               {#if isLoading}
                 <Icon name="loader-2" class="w-5 h-5 animate-spin mr-2" />
                 Procesando...
               {:else}
                 {showTwoFactor ? 'Verificar Acceso' : 'Acceder al Portal'}
               {/if}
             </button>
           </div>
           
           <div class="mt-8 text-center text-xs text-gray-400">
             © 2026 Aduanasoft. Acceso exclusivo autorizado.
           </div>
        </form>
    </div>
  </div>
</div>

==================================================
ARCHIVO: .\frontend-client\src\routes\profile\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { auth } from '$lib/stores/auth.js';
  import { toast } from '$lib/stores/toast.js';
  import { goto } from '$app/navigation';
  
  let currentPassword = '';
  let newPassword = '';
  let confirmPassword = '';
  let firstName = '';
  let lastName = '';
  let isUpdatingProfile = false;
  let isChangingPassword = false;
  let profileErrors: Record<string, string> = {};
  let passwordErrors: Record<string, string> = {};
  
  onMount(() => {
    // Redirect if not authenticated
    if (!$auth.isAuthenticated) {
      goto('/login');
      return;
    }
    
    // Initialize form with user data
    if ($auth.user) {
      firstName = $auth.user.first_name;
      lastName = $auth.user.last_name;
    }
  });
  
  function validateProfileForm() {
    profileErrors = {};
    
    if (!firstName.trim()) {
      profileErrors.firstName = 'El nombre es requerido';
    }
    
    if (!lastName.trim()) {
      profileErrors.lastName = 'El apellido es requerido';
    }
    
    return Object.keys(profileErrors).length === 0;
  }
  
  function validatePasswordForm() {
    passwordErrors = {};
    
    if (!currentPassword) {
      passwordErrors.currentPassword = 'La contraseña actual es requerida';
    }
    
    if (!newPassword) {
      passwordErrors.newPassword = 'La nueva contraseña es requerida';
    } else if (newPassword.length < 8) {
      passwordErrors.newPassword = 'La contraseña debe tener al menos 8 caracteres';
    }
    
    if (!confirmPassword) {
      passwordErrors.confirmPassword = 'Confirma la nueva contraseña';
    } else if (newPassword !== confirmPassword) {
      passwordErrors.confirmPassword = 'Las contraseñas no coinciden';
    }
    
    return Object.keys(passwordErrors).length === 0;
  }
  
  async function handleProfileUpdate() {
    if (!validateProfileForm()) return;
    
    isUpdatingProfile = true;
    
    try {
      const response = await fetch('/api/v1/auth/profile', {
        method: 'PATCH',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${$auth.token}`
        },
        body: JSON.stringify({
          first_name: firstName.trim(),
          last_name: lastName.trim()
        })
      });
      
      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail || 'Error al actualizar perfil');
      }
      
      const updatedUser = await response.json();
      auth.updateUser(updatedUser);
      toast.success('Perfil actualizado exitosamente');
    } catch (error: any) {
      toast.error(error.message || 'Error al actualizar perfil');
    } finally {
      isUpdatingProfile = false;
    }
  }
  
  async function handlePasswordChange() {
    if (!validatePasswordForm()) return;
    
    isChangingPassword = true;
    
    try {
      const response = await fetch('/api/v1/auth/change-password', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${$auth.token}`
        },
        body: JSON.stringify({
          current_password: currentPassword,
          new_password: newPassword
        })
      });
      
      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail || 'Error al cambiar contraseña');
      }
      
      // Clear form
      currentPassword = '';
      newPassword = '';
      confirmPassword = '';
      
      toast.success('Contraseña cambiada exitosamente');
    } catch (error: any) {
      toast.error(error.message || 'Error al cambiar contraseña');
    } finally {
      isChangingPassword = false;
    }
  }
</script>

<svelte:head>
  <title>Mi Perfil - ServiceManager</title>
</svelte:head>

<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
  <!-- Header -->
  <div class="mb-8">
    <h1 class="text-3xl font-bold text-gray-900">Mi Perfil</h1>
    <p class="text-gray-600 mt-2">
      Gestiona tu información personal y configuración de seguridad
    </p>
  </div>
  
  <div class="space-y-8">
    <!-- Profile Information -->
    <div class="card">
      <div class="card-header">
        <h2 class="text-xl font-semibold text-gray-900">Información Personal</h2>
        <p class="text-gray-600 mt-1">Actualiza tu información básica</p>
      </div>
      
      <div class="card-content">
        <form on:submit|preventDefault={handleProfileUpdate} class="space-y-6">
          <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
            <div>
              <label for="first-name" class="form-label">
                Nombre <span class="text-red-500">*</span>
              </label>
              <input
                id="first-name"
                type="text"
                class="form-input {profileErrors.firstName ? 'border-red-300' : ''}"
                bind:value={firstName}
                disabled={isUpdatingProfile}
              />
              {#if profileErrors.firstName}
                <p class="form-error">{profileErrors.firstName}</p>
              {/if}
            </div>
            
            <div>
              <label for="last-name" class="form-label">
                Apellido <span class="text-red-500">*</span>
              </label>
              <input
                id="last-name"
                type="text"
                class="form-input {profileErrors.lastName ? 'border-red-300' : ''}"
                bind:value={lastName}
                disabled={isUpdatingProfile}
              />
              {#if profileErrors.lastName}
                <p class="form-error">{profileErrors.lastName}</p>
              {/if}
            </div>
          </div>
          
          <div>
            <label for="email" class="form-label">Correo Electrónico</label>
            <input
              id="email"
              type="email"
              class="form-input bg-gray-50"
              value={$auth.user?.email || ''}
              disabled
            />
            <p class="text-xs text-gray-500 mt-1">
              El correo electrónico no se puede cambiar. Contacta con soporte si necesitas actualizarlo.
            </p>
          </div>
          
          <div class="flex justify-end">
            <button
              type="submit"
              class="btn-primary px-6 py-2"
              disabled={isUpdatingProfile}
            >
              {#if isUpdatingProfile}
                <div class="flex items-center space-x-2">
                  <div class="spinner w-4 h-4"></div>
                  <span>Guardando...</span>
                </div>
              {:else}
                Guardar Cambios
              {/if}
            </button>
          </div>
        </form>
      </div>
    </div>
    
    <!-- Account Security -->
    <div class="card">
      <div class="card-header">
        <h2 class="text-xl font-semibold text-gray-900">Seguridad de la Cuenta</h2>
        <p class="text-gray-600 mt-1">Gestiona tu contraseña y configuración de seguridad</p>
      </div>
      
      <div class="card-content space-y-6">
        <!-- Two-Factor Authentication Status -->
        <div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
          <div>
            <h3 class="font-medium text-gray-900">Autenticación de dos factores (2FA)</h3>
            <p class="text-sm text-gray-600">
              {$auth.user?.is_two_factor_enabled 
                ? 'La autenticación de dos factores está habilitada' 
                : 'Mejora la seguridad habilitando 2FA'}
            </p>
          </div>
          <div>
            {#if $auth.user?.is_two_factor_enabled}
              <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
                <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
                </svg>
                Habilitado
              </span>
            {:else}
              <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-red-100 text-red-800">
                <svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                </svg>
                Deshabilitado
              </span>
            {/if}
          </div>
        </div>
        
        <!-- Change Password Form -->
        <form on:submit|preventDefault={handlePasswordChange} class="space-y-6">
          <h3 class="text-lg font-medium text-gray-900">Cambiar Contraseña</h3>
          
          <div>
            <label for="current-password" class="form-label">
              Contraseña Actual <span class="text-red-500">*</span>
            </label>
            <input
              id="current-password"
              type="password"
              class="form-input {passwordErrors.currentPassword ? 'border-red-300' : ''}"
              bind:value={currentPassword}
              disabled={isChangingPassword}
            />
            {#if passwordErrors.currentPassword}
              <p class="form-error">{passwordErrors.currentPassword}</p>
            {/if}
          </div>
          
          <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
            <div>
              <label for="new-password" class="form-label">
                Nueva Contraseña <span class="text-red-500">*</span>
              </label>
              <input
                id="new-password"
                type="password"
                class="form-input {passwordErrors.newPassword ? 'border-red-300' : ''}"
                bind:value={newPassword}
                disabled={isChangingPassword}
              />
              {#if passwordErrors.newPassword}
                <p class="form-error">{passwordErrors.newPassword}</p>
              {/if}
              <p class="text-xs text-gray-500 mt-1">
                Mínimo 8 caracteres
              </p>
            </div>
            
            <div>
              <label for="confirm-password" class="form-label">
                Confirmar Nueva Contraseña <span class="text-red-500">*</span>
              </label>
              <input
                id="confirm-password"
                type="password"
                class="form-input {passwordErrors.confirmPassword ? 'border-red-300' : ''}"
                bind:value={confirmPassword}
                disabled={isChangingPassword}
              />
              {#if passwordErrors.confirmPassword}
                <p class="form-error">{passwordErrors.confirmPassword}</p>
              {/if}
            </div>
          </div>
          
          <div class="flex justify-end">
            <button
              type="submit"
              class="btn-primary px-6 py-2"
              disabled={isChangingPassword}
            >
              {#if isChangingPassword}
                <div class="flex items-center space-x-2">
                  <div class="spinner w-4 h-4"></div>
                  <span>Cambiando...</span>
                </div>
              {:else}
                Cambiar Contraseña
              {/if}
            </button>
          </div>
        </form>
      </div>
    </div>
    
    <!-- Account Information -->
    <div class="card">
      <div class="card-header">
        <h2 class="text-xl font-semibold text-gray-900">Información de la Cuenta</h2>
        <p class="text-gray-600 mt-1">Detalles sobre tu cuenta y organización</p>
      </div>
      
      <div class="card-content">
        <dl class="grid grid-cols-1 md:grid-cols-2 gap-6">
          <div>
            <dt class="text-sm font-medium text-gray-500">ID de Usuario</dt>
            <dd class="text-sm text-gray-900 font-mono mt-1">#{$auth.user?.id.substring(0, 8)}</dd>
          </div>
          
          <div>
            <dt class="text-sm font-medium text-gray-500">Rol</dt>
            <dd class="text-sm text-gray-900 mt-1">
              {$auth.user?.role === 'CLIENT_ADMIN' ? 'Administrador de Cliente' : 'Usuario de Cliente'}
            </dd>
          </div>
          
          <div>
            <dt class="text-sm font-medium text-gray-500">Estado de la Cuenta</dt>
            <dd class="text-sm mt-1">
              {#if $auth.user?.is_active}
                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
                  Activa
                </span>
              {:else}
                <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
                  Inactiva
                </span>
              {/if}
            </dd>
          </div>
          
          <div>
            <dt class="text-sm font-medium text-gray-500">Miembro desde</dt>
            <dd class="text-sm text-gray-900 mt-1">
              {$auth.user?.created_at ? new Date($auth.user.created_at).toLocaleDateString('es-ES', {
                day: '2-digit',
                month: 'long',
                year: 'numeric'
              }) : 'N/A'}
            </dd>
          </div>
        </dl>
      </div>
    </div>
  </div>
</div>

==================================================
ARCHIVO: .\frontend-client\src\routes\tickets\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { auth } from '$lib/stores/auth.js';
  import { tickets } from '$lib/stores/tickets.js';
  import { goto } from '$app/navigation';
  import TicketCard from '$lib/components/TicketCard.svelte';
  
  let searchQuery = '';
  let statusFilter = '';
  let priorityFilter = '';
  let filteredTickets: any[] = [];
  
  onMount(() => {
    // Redirect if not authenticated
    if (!$auth.isAuthenticated) {
      goto('/login');
      return;
    }
    
    // Load tickets
    tickets.loadTickets();
  });
  
  // Filter tickets based on search and filters
  $: {
    filteredTickets = $tickets.tickets.filter(ticket => {
      const matchesSearch = !searchQuery || 
        ticket.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
        ticket.description.toLowerCase().includes(searchQuery.toLowerCase()) ||
        ticket.id.toLowerCase().includes(searchQuery.toLowerCase());
      
      const matchesStatus = !statusFilter || ticket.status === statusFilter;
      const matchesPriority = !priorityFilter || ticket.priority === priorityFilter;
      
      return matchesSearch && matchesStatus && matchesPriority;
    });
  }
  
  // Get status counts
  $: statusCounts = $tickets.tickets.reduce((acc, ticket) => {
    acc[ticket.status] = (acc[ticket.status] || 0) + 1;
    return acc;
  }, {} as Record<string, number>);
  
  function clearFilters() {
    searchQuery = '';
    statusFilter = '';
    priorityFilter = '';
  }
</script>

<svelte:head>
  <title>Mis Tickets - ServiceManager</title>
</svelte:head>

<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
  <!-- Header -->
  <div class="flex justify-between items-center mb-8">
    <div>
      <h1 class="text-3xl font-bold text-gray-900">Mis Tickets</h1>
      <p class="text-gray-600 mt-2">
        Gestiona y da seguimiento a todos tus tickets de soporte
      </p>
    </div>
    <a 
      href="/tickets/new" 
      class="btn-primary px-4 py-2 inline-flex items-center space-x-2"
    >
      <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
      </svg>
      <span>Crear Ticket</span>
    </a>
  </div>
  
  <!-- Stats -->
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
    <div class="card">
      <div class="card-content">
        <div class="flex items-center">
          <div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
            <svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
            </svg>
          </div>
          <div class="ml-3">
            <p class="text-sm font-medium text-gray-500">Total</p>
            <p class="text-2xl font-semibold text-gray-900">{$tickets.tickets.length}</p>
          </div>
        </div>
      </div>
    </div>
    
    <div class="card">
      <div class="card-content">
        <div class="flex items-center">
          <div class="w-8 h-8 bg-yellow-100 rounded-lg flex items-center justify-center">
            <svg class="w-4 h-4 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
          </div>
          <div class="ml-3">
            <p class="text-sm font-medium text-gray-500">En Progreso</p>
            <p class="text-2xl font-semibold text-gray-900">
              {statusCounts['IN_PROGRESS'] || 0}
            </p>
          </div>
        </div>
      </div>
    </div>
    
    <div class="card">
      <div class="card-content">
        <div class="flex items-center">
          <div class="w-8 h-8 bg-orange-100 rounded-lg flex items-center justify-center">
            <svg class="w-4 h-4 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
          </div>
          <div class="ml-3">
            <p class="text-sm font-medium text-gray-500">Esperando</p>
            <p class="text-2xl font-semibold text-gray-900">
              {statusCounts['WAITING_FOR_CLIENT'] || 0}
            </p>
          </div>
        </div>
      </div>
    </div>
    
    <div class="card">
      <div class="card-content">
        <div class="flex items-center">
          <div class="w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
            <svg class="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
            </svg>
          </div>
          <div class="ml-3">
            <p class="text-sm font-medium text-gray-500">Resueltos</p>
            <p class="text-2xl font-semibold text-gray-900">
              {(statusCounts['RESOLVED'] || 0) + (statusCounts['CLOSED'] || 0)}
            </p>
          </div>
        </div>
      </div>
    </div>
  </div>
  
  <!-- Filters -->
  <div class="card mb-8">
    <div class="card-content">
      <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
        <div>
          <label for="search" class="form-label">Buscar</label>
          <input
            id="search"
            type="text"
            class="form-input"
            placeholder="Buscar por título, descripción o ID..."
            bind:value={searchQuery}
          />
        </div>
        
        <div>
          <label for="status-filter" class="form-label">Estado</label>
          <select
            id="status-filter"
            class="form-input"
            bind:value={statusFilter}
          >
            <option value="">Todos los estados</option>
            <option value="NEW">Nuevo</option>
            <option value="IN_PROGRESS">En Progreso</option>
            <option value="WAITING_FOR_CLIENT">Esperando Cliente</option>
            <option value="RESOLVED">Resuelto</option>
            <option value="CLOSED">Cerrado</option>
            <option value="REOPENED">Reabierto</option>
          </select>
        </div>
        
        <div>
          <label for="priority-filter" class="form-label">Prioridad</label>
          <select
            id="priority-filter"
            class="form-input"
            bind:value={priorityFilter}
          >
            <option value="">Todas las prioridades</option>
            <option value="LOW">Baja</option>
            <option value="MEDIUM">Media</option>
            <option value="HIGH">Alta</option>
            <option value="URGENT">Urgente</option>
          </select>
        </div>
        
        <div class="flex items-end">
          <button
            on:click={clearFilters}
            class="btn-secondary px-4 py-2 w-full"
          >
            Limpiar Filtros
          </button>
        </div>
      </div>
    </div>
  </div>
  
  <!-- Tickets List -->
  <div class="space-y-6">
    {#if $tickets.isLoading}
      <div class="text-center py-12">
        <div class="spinner w-8 h-8 mx-auto mb-4"></div>
        <p class="text-gray-600">Cargando tickets...</p>
      </div>
    {:else if $tickets.error}
      <div class="text-center py-12">
        <div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mx-auto mb-4">
          <svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
          </svg>
        </div>
        <h3 class="text-lg font-medium text-gray-900 mb-2">Error al cargar tickets</h3>
        <p class="text-gray-600 mb-4">{$tickets.error}</p>
        <button 
          on:click={() => tickets.loadTickets()}
          class="btn-primary px-4 py-2"
        >
          Reintentar
        </button>
      </div>
    {:else if filteredTickets.length === 0}
      <div class="text-center py-12">
        <div class="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mx-auto mb-4">
          <svg class="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
          </svg>
        </div>
        <h3 class="text-lg font-medium text-gray-900 mb-2">
          {$tickets.tickets.length === 0 ? 'No tienes tickets' : 'No se encontraron tickets'}
        </h3>
        <p class="text-gray-600 mb-4">
          {$tickets.tickets.length === 0 
            ? 'Crea tu primer ticket para comenzar'
            : 'Intenta ajustar los filtros de búsqueda'}
        </p>
        {#if $tickets.tickets.length === 0}
          <a href="/tickets/new" class="btn-primary px-4 py-2">
            Crear Ticket
          </a>
        {:else}
          <button on:click={clearFilters} class="btn-secondary px-4 py-2">
            Limpiar Filtros
          </button>
        {/if}
      </div>
    {:else}
      <div class="space-y-4">
        {#each filteredTickets as ticket (ticket.id)}
          <TicketCard {ticket} />
        {/each}
      </div>
      
      {#if filteredTickets.length !== $tickets.tickets.length}
        <div class="text-center py-4 text-sm text-gray-500">
          Mostrando {filteredTickets.length} de {$tickets.tickets.length} tickets
        </div>
      {/if}
    {/if}
  </div>
</div>

==================================================
ARCHIVO: .\frontend-client\src\routes\tickets\new\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { auth } from '$lib/stores/auth.js';
  import { tickets } from '$lib/stores/tickets.js';
  import { app } from '$lib/stores/app.js';
  import { toast } from '$lib/stores/toast.js';
  import { goto } from '$app/navigation';
  
  let title = '';
  let description = '';
  let categoryId = '';
  let priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT' = 'MEDIUM';
  let isSubmitting = false;
  let errors: Record<string, string> = {};
  
  onMount(() => {
    // Redirect if not authenticated
    if (!$auth.isAuthenticated) {
      goto('/login');
      return;
    }
    
    // Load categories for the form
    app.loadCategories();
  });
  
  function validateForm() {
    errors = {};
    
    if (!title.trim()) {
      errors.title = 'El título es requerido';
    } else if (title.trim().length < 10) {
      errors.title = 'El título debe tener al menos 10 caracteres';
    }
    
    if (!description.trim()) {
      errors.description = 'La descripción es requerida';
    } else if (description.trim().length < 20) {
      errors.description = 'La descripción debe tener al menos 20 caracteres';
    }
    
    if (!categoryId) {
      errors.categoryId = 'Debes seleccionar una categoría';
    }
    
    return Object.keys(errors).length === 0;
  }
  
  async function handleSubmit() {
    if (!validateForm()) return;
    
    isSubmitting = true;
    
    try {
      const newTicket = await tickets.createTicket({
        title: title.trim(),
        description: description.trim(),
        category_id: categoryId,
        priority
      });
      
      toast.success('Ticket creado exitosamente');
      goto(`/tickets/${newTicket.id}`);
    } catch (error: any) {
      console.error('Create ticket error:', error);
      toast.error(error.message || 'Error al crear el ticket');
    } finally {
      isSubmitting = false;
    }
  }
</script>

<svelte:head>
  <title>Crear Ticket - ServiceManager</title>
</svelte:head>

<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
  <!-- Header -->
  <div class="mb-8">
    <div class="flex items-center space-x-2 text-sm text-gray-500 mb-4">
      <a href="/tickets" class="hover:text-primary-600">Mis Tickets</a>
      <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
      </svg>
      <span>Crear Ticket</span>
    </div>
    
    <h1 class="text-3xl font-bold text-gray-900">Crear Nuevo Ticket</h1>
    <p class="text-gray-600 mt-2">
      Describe tu problema o solicitud de soporte con el mayor detalle posible
    </p>
  </div>
  
  <!-- Form -->
  <form on:submit|preventDefault={handleSubmit} class="space-y-6">
    <div class="card">
      <div class="card-content space-y-6">
        <!-- Title -->
        <div>
          <label for="title" class="form-label">
            Título del Ticket <span class="text-red-500">*</span>
          </label>
          <input
            id="title"
            type="text"
            class="form-input {errors.title ? 'border-red-300' : ''}"
            placeholder="Describe brevemente el problema..."
            bind:value={title}
            disabled={isSubmitting}
            maxlength="200"
          />
          {#if errors.title}
            <p class="form-error">{errors.title}</p>
          {/if}
          <p class="text-xs text-gray-500 mt-1">
            {title.length}/200 caracteres
          </p>
        </div>
        
        <!-- Category -->
        <div>
          <label for="category" class="form-label">
            Categoría <span class="text-red-500">*</span>
          </label>
          <select
            id="category"
            class="form-input {errors.categoryId ? 'border-red-300' : ''}"
            bind:value={categoryId}
            disabled={isSubmitting || $app.isLoading}
          >
            <option value="">Selecciona una categoría</option>
            {#each $app.categories as category}
              <option value={category.id}>{category.name}</option>
            {/each}
          </select>
          {#if errors.categoryId}
            <p class="form-error">{errors.categoryId}</p>
          {/if}
        </div>
        
        <!-- Priority -->
        <div>
          <label for="priority" class="form-label">
            Prioridad
          </label>
          <select
            id="priority"
            class="form-input"
            bind:value={priority}
            disabled={isSubmitting}
          >
            <option value="LOW">Baja - No es urgente, puede esperar</option>
            <option value="MEDIUM">Media - Problema normal de trabajo</option>
            <option value="HIGH">Alta - Afecta el trabajo significativamente</option>
            <option value="URGENT">Urgente - Bloquea el trabajo completamente</option>
          </select>
        </div>
        
        <!-- Description -->
        <div>
          <label for="description" class="form-label">
            Descripción del Problema <span class="text-red-500">*</span>
          </label>
          <textarea
            id="description"
            rows="8"
            class="form-input {errors.description ? 'border-red-300' : ''}"
            placeholder="Describe el problema con el mayor detalle posible. Incluye:
- Qué estabas haciendo cuando ocurrió el problema
- Qué esperabas que pasara
- Qué pasó en realidad
- Pasos para reproducir el problema
- Cualquier mensaje de error
- Información adicional relevante"
            bind:value={description}
            disabled={isSubmitting}
            maxlength="2000"
          ></textarea>
          {#if errors.description}
            <p class="form-error">{errors.description}</p>
          {/if}
          <p class="text-xs text-gray-500 mt-1">
            {description.length}/2000 caracteres
          </p>
        </div>
      </div>
    </div>
    
    <!-- Help Tips -->
    <div class="card bg-blue-50 border-blue-200">
      <div class="card-content">
        <div class="flex">
          <div class="flex-shrink-0">
            <svg class="h-5 w-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
            </svg>
          </div>
          <div class="ml-3">
            <h3 class="text-sm font-medium text-blue-800">
              Tips para un mejor soporte
            </h3>
            <div class="mt-2 text-sm text-blue-700">
              <ul class="list-disc pl-5 space-y-1">
                <li>Sé específico y detallado en tu descripción</li>
                <li>Incluye capturas de pantalla si es posible (puedes adjuntarlas después)</li>
                <li>Menciona qué navegador/sistema operativo estás usando</li>
                <li>Indica si el problema es recurrente o fue la primera vez</li>
                <li>Si hay mensajes de error, cópialos exactamente</li>
              </ul>
            </div>
          </div>
        </div>
      </div>
    </div>
    
    <!-- Actions -->
    <div class="flex justify-between items-center pt-6">
      <a 
        href="/tickets" 
        class="btn-secondary px-6 py-2"
      >
        Cancelar
      </a>
      
      <button
        type="submit"
        class="btn-primary px-6 py-2 disabled:opacity-50 disabled:cursor-not-allowed"
        disabled={isSubmitting}
      >
        {#if isSubmitting}
          <div class="flex items-center space-x-2">
            <div class="spinner w-4 h-4"></div>
            <span>Creando...</span>
          </div>
        {:else}
          Crear Ticket
        {/if}
      </button>
    </div>
  </form>
</div>

==================================================
ARCHIVO: .\frontend-client\src\routes\tickets\[id]\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { page } from '$app/stores';
  import { auth } from '$lib/stores/auth.js';
  import { tickets } from '$lib/stores/tickets.js';
  import { toast } from '$lib/stores/toast.js';
  import { goto } from '$app/navigation';
  
  let ticketId: string;
  let newComment = '';
  let isSubmittingComment = false;
  let isClosingTicket = false;
  let showCloseDialog = false;
  let closeResolution = '';
  let fileInput: HTMLInputElement;
  let isUploading = false;
  
  onMount(() => {
    // Redirect if not authenticated
    if (!$auth.isAuthenticated) {
      goto('/login');
      return;
    }
    
    ticketId = $page.params.id;
    if (ticketId) {
      tickets.loadTicket(ticketId);
    }
  });
  
  // Format date
  function formatDate(dateString: string): string {
    return new Date(dateString).toLocaleString('es-ES', {
      day: '2-digit',
      month: '2-digit',
      year: 'numeric',
      hour: '2-digit',
      minute: '2-digit'
    });
  }
  
  // Status mapping
  const statusConfig = {
    NEW: { label: 'Nuevo', class: 'badge-new' },
    IN_PROGRESS: { label: 'En Progreso', class: 'badge-in-progress' },
    WAITING_FOR_CLIENT: { label: 'Esperando Cliente', class: 'badge-waiting' },
    RESOLVED: { label: 'Resuelto', class: 'badge-resolved' },
    CLOSED: { label: 'Cerrado', class: 'badge-closed' },
    REOPENED: { label: 'Reabierto', class: 'badge-reopened' }
  };
  
  // Priority mapping
  const priorityConfig = {
    LOW: { label: 'Baja', class: 'badge-priority-low' },
    MEDIUM: { label: 'Media', class: 'badge-priority-medium' },
    HIGH: { label: 'Alta', class: 'badge-priority-high' },
    URGENT: { label: 'Urgente', class: 'badge-priority-urgent' }
  };
  
  async function handleAddComment() {
    if (!newComment.trim()) return;
    
    isSubmittingComment = true;
    try {
      await tickets.addComment(ticketId, newComment.trim());
      newComment = '';
      toast.success('Comentario agregado');
    } catch (error: any) {
      toast.error(error.message || 'Error al agregar comentario');
    } finally {
      isSubmittingComment = false;
    }
  }
  
  async function handleFileUpload(event: Event) {
    const target = event.target as HTMLInputElement;
    const file = target.files?.[0];
    if (!file) return;
    
    // Validate file size (max 10MB)
    if (file.size > 10 * 1024 * 1024) {
      toast.error('El archivo es demasiado grande. Máximo 10MB');
      target.value = '';
      return;
    }
    
    // Validate file type
    const allowedTypes = [
      'image/jpeg', 'image/png', 'image/gif', 'image/webp',
      'application/pdf', 'text/plain', 'application/msword',
      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      'application/vnd.ms-excel',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    ];
    
    if (!allowedTypes.includes(file.type)) {
      toast.error('Tipo de archivo no permitido');
      target.value = '';
      return;
    }
    
    isUploading = true;
    try {
      await tickets.uploadAttachment(ticketId, file);
      toast.success('Archivo adjuntado correctamente');
      target.value = '';
    } catch (error: any) {
      toast.error(error.message || 'Error al subir archivo');
    } finally {
      isUploading = false;
    }
  }
  
  function handleCloseTicket() {
    showCloseDialog = true;
  }
  
  async function confirmCloseTicket() {
    isClosingTicket = true;
    try {
      await tickets.closeTicket(ticketId, closeResolution.trim() || undefined);
      showCloseDialog = false;
      closeResolution = '';
      toast.success('Ticket cerrado exitosamente');
    } catch (error: any) {
      toast.error(error.message || 'Error al cerrar ticket');
    } finally {
      isClosingTicket = false;
    }
  }
  
  function cancelCloseTicket() {
    showCloseDialog = false;
    closeResolution = '';
  }
  
  // Check if user can close ticket
  $: canClose = $tickets.currentTicket && 
    ['RESOLVED', 'WAITING_FOR_CLIENT'].includes($tickets.currentTicket.status);
</script>

<svelte:head>
  <title>
    {$tickets.currentTicket ? `Ticket: ${$tickets.currentTicket.title}` : 'Cargando...'} - ServiceManager
  </title>
</svelte:head>

<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
  {#if $tickets.isLoading}
    <div class="text-center py-12">
      <div class="spinner w-8 h-8 mx-auto mb-4"></div>
      <p class="text-gray-600">Cargando ticket...</p>
    </div>
  {:else if $tickets.error}
    <div class="text-center py-12">
      <div class="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mx-auto mb-4">
        <svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
        </svg>
      </div>
      <h3 class="text-lg font-medium text-gray-900 mb-2">Error al cargar ticket</h3>
      <p class="text-gray-600 mb-4">{$tickets.error}</p>
      <button 
        on:click={() => tickets.loadTicket(ticketId)}
        class="btn-primary px-4 py-2"
      >
        Reintentar
      </button>
    </div>
  {:else if $tickets.currentTicket}
    <!-- Breadcrumb -->
    <div class="flex items-center space-x-2 text-sm text-gray-500 mb-6">
      <a href="/tickets" class="hover:text-primary-600">Mis Tickets</a>
      <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
      </svg>
      <span>#{$tickets.currentTicket.id.substring(0, 8)}</span>
    </div>
    
    <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
      <!-- Main Content -->
      <div class="lg:col-span-2 space-y-6">
        <!-- Ticket Header -->
        <div class="card">
          <div class="card-header">
            <div class="flex justify-between items-start">
              <div class="flex-1">
                <h1 class="text-2xl font-bold text-gray-900 mb-2">
                  {$tickets.currentTicket.title}
                </h1>
                <div class="flex items-center space-x-3">
                  <span class={statusConfig[$tickets.currentTicket.status].class}>
                    {statusConfig[$tickets.currentTicket.status].label}
                  </span>
                  <span class={priorityConfig[$tickets.currentTicket.priority].class}>
                    {priorityConfig[$tickets.currentTicket.priority].label}
                  </span>
                  <span class="text-sm text-gray-500">
                    Creado {formatDate($tickets.currentTicket.created_at)}
                  </span>
                </div>
              </div>
              
              {#if canClose}
                <button
                  on:click={handleCloseTicket}
                  class="btn-success px-4 py-2"
                  disabled={isClosingTicket}
                >
                  Cerrar Ticket
                </button>
              {/if}
            </div>
          </div>
          
          <div class="card-content">
            <div class="prose max-w-none">
              <p class="whitespace-pre-wrap text-gray-700">
                {$tickets.currentTicket.description}
              </p>
            </div>
            
            {#if $tickets.currentTicket.resolution}
              <div class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
                <h4 class="font-medium text-green-900 mb-2">Resolución:</h4>
                <p class="text-green-800 whitespace-pre-wrap">
                  {$tickets.currentTicket.resolution}
                </p>
              </div>
            {/if}
          </div>
        </div>
        
        <!-- Attachments -->
        {#if $tickets.attachments.length > 0}
          <div class="card">
            <div class="card-header">
              <h3 class="text-lg font-semibold text-gray-900">Archivos Adjuntos</h3>
            </div>
            <div class="card-content">
              <div class="space-y-3">
                {#each $tickets.attachments as attachment}
                  <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
                    <div class="flex items-center space-x-3">
                      <div class="w-8 h-8 bg-gray-200 rounded flex items-center justify-center">
                        <svg class="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
                        </svg>
                      </div>
                      <div>
                        <p class="text-sm font-medium text-gray-900">
                          {attachment.original_filename}
                        </p>
                        <p class="text-xs text-gray-500">
                          {Math.round(attachment.size_bytes / 1024)} KB • 
                          Subido por {attachment.uploaded_by_name} • 
                          {formatDate(attachment.uploaded_at)}
                        </p>
                      </div>
                    </div>
                    <a
                      href="/api/v1/tickets/{ticketId}/attachments/{attachment.id}/download"
                      class="btn-ghost p-2"
                      target="_blank"
                    >
                      <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
                      </svg>
                    </a>
                  </div>
                {/each}
              </div>
            </div>
          </div>
        {/if}
        
        <!-- Comments -->
        <div class="card">
          <div class="card-header">
            <h3 class="text-lg font-semibold text-gray-900">Conversación</h3>
          </div>
          <div class="card-content">
            {#if $tickets.comments.length === 0}
              <p class="text-gray-500 text-center py-4">
                No hay comentarios aún. ¡Sé el primero en comentar!
              </p>
            {:else}
              <div class="space-y-4">
                {#each $tickets.comments as comment}
                  <div class="flex space-x-3">
                    <div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center flex-shrink-0">
                      <span class="text-primary-600 text-xs font-medium">
                        {comment.user_name.split(' ').map(n => n[0]).join('')}
                      </span>
                    </div>
                    <div class="flex-1 min-w-0">
                      <div class="flex items-center space-x-2 mb-1">
                        <span class="text-sm font-medium text-gray-900">
                          {comment.user_name}
                        </span>
                        <span class="text-xs text-gray-500">
                          {formatDate(comment.created_at)}
                        </span>
                        {#if comment.is_internal}
                          <span class="bg-red-100 text-red-700 text-xs px-2 py-0.5 rounded">
                            Interno
                          </span>
                        {/if}
                      </div>
                      <p class="text-gray-700 whitespace-pre-wrap">
                        {comment.content}
                      </p>
                    </div>
                  </div>
                {/each}
              </div>
            {/if}
            
            <!-- Add Comment Form -->
            <div class="mt-6 pt-6 border-t border-gray-200">
              <div class="space-y-4">
                <textarea
                  rows="4"
                  class="form-input"
                  placeholder="Escribe tu comentario o respuesta..."
                  bind:value={newComment}
                  disabled={isSubmittingComment}
                ></textarea>
                
                <div class="flex justify-between items-center">
                  <div class="flex items-center space-x-4">
                    <input
                      type="file"
                      bind:this={fileInput}
                      on:change={handleFileUpload}
                      class="hidden"
                      accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.txt,.doc,.docx,.xls,.xlsx"
                      disabled={isUploading}
                    />
                    <button
                      type="button"
                      on:click={() => fileInput.click()}
                      class="btn-ghost p-2 flex items-center space-x-2"
                      disabled={isUploading}
                    >
                      {#if isUploading}
                        <div class="spinner w-4 h-4"></div>
                      {:else}
                        <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
                        </svg>
                      {/if}
                      <span class="text-sm">Adjuntar archivo</span>
                    </button>
                  </div>
                  
                  <button
                    on:click={handleAddComment}
                    class="btn-primary px-4 py-2"
                    disabled={isSubmittingComment || !newComment.trim()}
                  >
                    {#if isSubmittingComment}
                      <div class="flex items-center space-x-2">
                        <div class="spinner w-4 h-4"></div>
                        <span>Enviando...</span>
                      </div>
                    {:else}
                      Enviar Comentario
                    {/if}
                  </button>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
      
      <!-- Sidebar -->
      <div class="space-y-6">
        <!-- Ticket Info -->
        <div class="card">
          <div class="card-header">
            <h3 class="text-lg font-semibold text-gray-900">Información</h3>
          </div>
          <div class="card-content space-y-4">
            <div>
              <dt class="text-sm font-medium text-gray-500">ID del Ticket</dt>
              <dd class="text-sm text-gray-900 font-mono">#{$tickets.currentTicket.id.substring(0, 8)}</dd>
            </div>
            
            <div>
              <dt class="text-sm font-medium text-gray-500">Categoría</dt>
              <dd class="text-sm text-gray-900">{$tickets.currentTicket.category_name || 'Sin categoría'}</dd>
            </div>
            
            {#if $tickets.currentTicket.assigned_to_name}
              <div>
                <dt class="text-sm font-medium text-gray-500">Asignado a</dt>
                <dd class="text-sm text-gray-900">{$tickets.currentTicket.assigned_to_name}</dd>
              </div>
            {/if}
            
            <div>
              <dt class="text-sm font-medium text-gray-500">Creado</dt>
              <dd class="text-sm text-gray-900">{formatDate($tickets.currentTicket.created_at)}</dd>
            </div>
            
            <div>
              <dt class="text-sm font-medium text-gray-500">Última actualización</dt>
              <dd class="text-sm text-gray-900">{formatDate($tickets.currentTicket.updated_at)}</dd>
            </div>
            
            {#if $tickets.currentTicket.due_date}
              <div>
                <dt class="text-sm font-medium text-gray-500">Fecha límite</dt>
                <dd class="text-sm text-gray-900 {new Date($tickets.currentTicket.due_date) < new Date() ? 'text-red-600' : ''}">
                  {formatDate($tickets.currentTicket.due_date)}
                  {#if new Date($tickets.currentTicket.due_date) < new Date()}
                    <span class="block text-xs text-red-500">¡Vencido!</span>
                  {/if}
                </dd>
              </div>
            {/if}
          </div>
        </div>
      </div>
    </div>
  {/if}
</div>

<!-- Close Ticket Dialog -->
{#if showCloseDialog}
  <div class="fixed inset-0 z-50 overflow-y-auto">
    <div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
      <div class="fixed inset-0 transition-opacity" on:click={cancelCloseTicket}>
        <div class="absolute inset-0 bg-gray-500 opacity-75"></div>
      </div>
      
      <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
        <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
          <div class="sm:flex sm:items-start">
            <div class="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10">
              <svg class="h-6 w-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
              </svg>
            </div>
            <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
              <h3 class="text-lg leading-6 font-medium text-gray-900">
                Cerrar Ticket
              </h3>
              <div class="mt-2">
                <p class="text-sm text-gray-500">
                  ¿Estás seguro de que quieres cerrar este ticket? Esta acción indica que el problema ha sido resuelto satisfactoriamente.
                </p>
              </div>
              
              <div class="mt-4">
                <label for="close-resolution" class="form-label">
                  Comentario de cierre (opcional)
                </label>
                <textarea
                  id="close-resolution"
                  rows="3"
                  class="form-input"
                  placeholder="Describe cómo se resolvió el problema o agrega comentarios finales..."
                  bind:value={closeResolution}
                  disabled={isClosingTicket}
                ></textarea>
              </div>
            </div>
          </div>
        </div>
        <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
          <button
            type="button"
            class="w-full inline-flex justify-center btn-success px-4 py-2 sm:ml-3 sm:w-auto disabled:opacity-50"
            disabled={isClosingTicket}
            on:click={confirmCloseTicket}
          >
            {#if isClosingTicket}
              <div class="flex items-center space-x-2">
                <div class="spinner w-4 h-4"></div>
                <span>Cerrando...</span>
              </div>
            {:else}
              Cerrar Ticket
            {/if}
          </button>
          <button
            type="button"
            class="mt-3 w-full inline-flex justify-center btn-secondary px-4 py-2 sm:mt-0 sm:w-auto"
            disabled={isClosingTicket}
            on:click={cancelCloseTicket}
          >
            Cancelar
          </button>
        </div>
      </div>
    </div>
  </div>
{/if}

==================================================
ARCHIVO: .\frontend-internal\.eslintrc.json
==================================================
{
  "extends": [
    "eslint:recommended",
    "@typescript-eslint/recommended",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint", "svelte3"],
  "parserOptions": {
    "ecmaVersion": 2020,
    "sourceType": "module"
  },
  "env": {
    "browser": true,
    "es2017": true,
    "node": true
  },
  "overrides": [
    {
      "files": ["*.svelte"],
      "processor": "svelte3/svelte3"
    }
  ],
  "rules": {
    "@typescript-eslint/no-unused-vars": [
      "error",
      { "argsIgnorePattern": "^_" }
    ],
    "@typescript-eslint/no-explicit-any": "warn",
    "no-console": ["warn", { "allow": ["warn", "error"] }],
    "prefer-const": "error"
  },
  "settings": {
    "svelte3/typescript": true
  },
  "ignorePatterns": [
    ".svelte-kit/**",
    "build/**",
    "dist/**",
    "node_modules/**"
  ]
}

==================================================
ARCHIVO: .\frontend-internal\package.json
==================================================
{
  "name": "@servicemanager/internal-frontend",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite dev --port 3000 --host 0.0.0.0",
    "build": "vite build",
    "preview": "vite preview --port 3000 --host 0.0.0.0",
    "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
    "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
    "lint": "prettier --plugin-search-dir . --check . && eslint .",
    "format": "prettier --plugin-search-dir . --write .",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:ui": "vitest --ui"
  },
  "devDependencies": {
    "@sveltejs/adapter-node": "^1.3.1",
    "@sveltejs/kit": "^1.20.4",
    "@types/cookie": "^0.5.1",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "autoprefixer": "^10.4.14",
    "eslint": "^8.28.0",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-svelte": "^2.30.0",
    "postcss": "^8.4.24",
    "prettier": "^2.8.0",
    "prettier-plugin-svelte": "^2.10.1",
    "svelte": "^4.0.5",
    "svelte-check": "^3.4.3",
    "tailwindcss": "^3.3.0",
    "tslib": "^2.4.1",
    "typescript": "^5.0.0",
    "vite": "^4.4.2",
    "vitest": "^0.34.0"
  },
  "dependencies": {
    "@tailwindcss/forms": "^0.5.4",
    "@tailwindcss/typography": "^0.5.9",
    "@heroicons/react": "^2.0.18",
    "heroicons": "^2.0.18",
    "zod": "^3.22.2",
    "date-fns": "^2.30.0",
    "chart.js": "^4.3.0",
    "chartjs-adapter-date-fns": "^3.0.0"
  }
}

==================================================
ARCHIVO: .\frontend-internal\postcss.config.js
==================================================
export default {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}

==================================================
ARCHIVO: .\frontend-internal\svelte.config.js
==================================================
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';

/** @type {import('@sveltejs/kit').Config} */
const config = {
	// Consult https://kit.svelte.dev/docs/integrations#preprocessors
	// for more information about preprocessors
	preprocess: vitePreprocess(),

	kit: {
		// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
		// If your environment is not supported or you settled on a specific environment, switch out the adapter.
		// See https://kit.svelte.dev/docs/adapters for more information about adapters.
		adapter: adapter(),
		
		alias: {
			'$components': 'src/lib/components',
			'$stores': 'src/lib/stores',
			'$utils': 'src/lib/utils',
			'$types': 'src/lib/types'
		}
	}
};

export default config;


==================================================
ARCHIVO: .\frontend-internal\tailwind.config.js
==================================================
/** @type {import('tailwindcss').Config} */
export default {
	content: ['./src/**/*.{html,js,svelte,ts}'],
	theme: {
		extend: {
			colors: {
				primary: {
					50: '#eff6ff',
					100: '#dbeafe',
					200: '#bfdbfe',
					300: '#93c5fd',
					400: '#60a5fa',
					500: '#3b82f6',
					600: '#2563eb',
					700: '#1d4ed8',
					800: '#1e40af',
					900: '#1e3a8a',
				}
			}
		}
	},
	plugins: [],
}


==================================================
ARCHIVO: .\frontend-internal\vite.config.js
==================================================
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';

export default defineConfig({
	plugins: [sveltekit()],
	server: {
		port: 3000,
		host: '0.0.0.0',
		proxy: {
			'/api/v1': {
				target: 'http://servicemanager-backend:8000',
				changeOrigin: true,
				rewrite: (path) => path.replace(/^\/api/, '')
			}
		}
	},
	preview: {
		port: 3000,
		host: '0.0.0.0'
	},
	build: {
		target: 'esnext'
	}
});


==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\ambient.d.ts
==================================================

// this file is generated — do not edit it


/// <reference types="@sveltejs/kit" />

/**
 * Environment variables [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env`. Like [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), this module cannot be imported into client-side code. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://kit.svelte.dev/docs/configuration#env) (if configured).
 * 
 * _Unlike_ [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), the values exported from this module are statically injected into your bundle at build time, enabling optimisations like dead code elimination.
 * 
 * ```ts
 * import { API_KEY } from '$env/static/private';
 * ```
 * 
 * Note that all environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
 * 
 * ```
 * MY_FEATURE_FLAG=""
 * ```
 * 
 * You can override `.env` values from the command line like so:
 * 
 * ```bash
 * MY_FEATURE_FLAG="enabled" npm run dev
 * ```
 */
declare module '$env/static/private' {
	export const npm_config_user_agent: string;
	export const NODE_VERSION: string;
	export const HOSTNAME: string;
	export const YARN_VERSION: string;
	export const npm_node_execpath: string;
	export const SHLVL: string;
	export const npm_config_noproxy: string;
	export const HOME: string;
	export const npm_package_json: string;
	export const npm_config_userconfig: string;
	export const npm_config_local_prefix: string;
	export const COLOR: string;
	export const npm_config_prefix: string;
	export const npm_config_npm_version: string;
	export const npm_config_cache: string;
	export const npm_config_node_gyp: string;
	export const PATH: string;
	export const NODE: string;
	export const npm_package_name: string;
	export const npm_lifecycle_script: string;
	export const npm_package_version: string;
	export const npm_lifecycle_event: string;
	export const npm_config_globalconfig: string;
	export const npm_config_init_module: string;
	export const PWD: string;
	export const npm_execpath: string;
	export const npm_config_global_prefix: string;
	export const npm_command: string;
	export const NODE_ENV: string;
	export const INIT_CWD: string;
	export const EDITOR: string;
}

/**
 * Similar to [`$env/static/private`](https://kit.svelte.dev/docs/modules#$env-static-private), except that it only includes environment variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
 * 
 * Values are replaced statically at build time.
 * 
 * ```ts
 * import { PUBLIC_BASE_URL } from '$env/static/public';
 * ```
 */
declare module '$env/static/public' {
	export const PUBLIC_APP_NAME: string;
	export const PUBLIC_API_URL: string;
}

/**
 * This module provides access to runtime environment variables, as defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/master/packages/adapter-node) (or running [`vite preview`](https://kit.svelte.dev/docs/cli)), this is equivalent to `process.env`. This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://kit.svelte.dev/docs/configuration#env) (if configured).
 * 
 * This module cannot be imported into client-side code.
 * 
 * ```ts
 * import { env } from '$env/dynamic/private';
 * console.log(env.DEPLOYMENT_SPECIFIC_VARIABLE);
 * ```
 * 
 * > In `dev`, `$env/dynamic` always includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
 */
declare module '$env/dynamic/private' {
	export const env: {
		npm_config_user_agent: string;
		NODE_VERSION: string;
		HOSTNAME: string;
		YARN_VERSION: string;
		npm_node_execpath: string;
		SHLVL: string;
		npm_config_noproxy: string;
		HOME: string;
		npm_package_json: string;
		npm_config_userconfig: string;
		npm_config_local_prefix: string;
		COLOR: string;
		npm_config_prefix: string;
		npm_config_npm_version: string;
		npm_config_cache: string;
		npm_config_node_gyp: string;
		PATH: string;
		NODE: string;
		npm_package_name: string;
		npm_lifecycle_script: string;
		npm_package_version: string;
		npm_lifecycle_event: string;
		npm_config_globalconfig: string;
		npm_config_init_module: string;
		PWD: string;
		npm_execpath: string;
		npm_config_global_prefix: string;
		npm_command: string;
		NODE_ENV: string;
		INIT_CWD: string;
		EDITOR: string;
		[key: `PUBLIC_${string}`]: undefined;
		[key: `${string}`]: string | undefined;
	}
}

/**
 * Similar to [`$env/dynamic/private`](https://kit.svelte.dev/docs/modules#$env-dynamic-private), but only includes variables that begin with [`config.kit.env.publicPrefix`](https://kit.svelte.dev/docs/configuration#env) (which defaults to `PUBLIC_`), and can therefore safely be exposed to client-side code.
 * 
 * Note that public dynamic environment variables must all be sent from the server to the client, causing larger network requests — when possible, use `$env/static/public` instead.
 * 
 * ```ts
 * import { env } from '$env/dynamic/public';
 * console.log(env.PUBLIC_DEPLOYMENT_SPECIFIC_VARIABLE);
 * ```
 */
declare module '$env/dynamic/public' {
	export const env: {
		PUBLIC_APP_NAME: string;
		PUBLIC_API_URL: string;
		[key: `PUBLIC_${string}`]: string | undefined;
	}
}


==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\tsconfig.json
==================================================
{
	"compilerOptions": {
		"paths": {
			"$components": [
				"../src/lib/components"
			],
			"$components/*": [
				"../src/lib/components/*"
			],
			"$stores": [
				"../src/lib/stores"
			],
			"$stores/*": [
				"../src/lib/stores/*"
			],
			"$utils": [
				"../src/lib/utils"
			],
			"$utils/*": [
				"../src/lib/utils/*"
			],
			"$types": [
				"../src/lib/types"
			],
			"$types/*": [
				"../src/lib/types/*"
			],
			"$lib": [
				"../src/lib"
			],
			"$lib/*": [
				"../src/lib/*"
			]
		},
		"rootDirs": [
			"..",
			"./types"
		],
		"importsNotUsedAsValues": "error",
		"isolatedModules": true,
		"preserveValueImports": true,
		"lib": [
			"esnext",
			"DOM",
			"DOM.Iterable"
		],
		"moduleResolution": "node",
		"module": "esnext",
		"noEmit": true,
		"target": "esnext",
		"ignoreDeprecations": "5.0"
	},
	"include": [
		"ambient.d.ts",
		"./types/**/$types.d.ts",
		"../vite.config.js",
		"../vite.config.ts",
		"../src/**/*.js",
		"../src/**/*.ts",
		"../src/**/*.svelte",
		"../tests/**/*.js",
		"../tests/**/*.ts",
		"../tests/**/*.svelte"
	],
	"exclude": [
		"../node_modules/**",
		"./[!ambient.d.ts]**",
		"../src/service-worker.js",
		"../src/service-worker.ts",
		"../src/service-worker.d.ts"
	]
}

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\root.svelte
==================================================
<!-- This file is generated by @sveltejs/kit — do not edit it! -->

<script>
	import { setContext, afterUpdate, onMount, tick } from 'svelte';
	import { browser } from '$app/environment';

	// stores
	export let stores;
	export let page;
	
	export let constructors;
	export let components = [];
	export let form;
	export let data_0 = null;
	export let data_1 = null;

	if (!browser) {
		setContext('__svelte__', stores);
	}

	$: stores.page.set(page);
	afterUpdate(stores.page.notify);

	let mounted = false;
	let navigated = false;
	let title = null;

	onMount(() => {
		const unsubscribe = stores.page.subscribe(() => {
			if (mounted) {
				navigated = true;
				tick().then(() => {
					title = document.title || 'untitled page';
				});
			}
		});

		mounted = true;
		return unsubscribe;
	});
</script>

{#if constructors[1]}
	<svelte:component this={constructors[0]} bind:this={components[0]} data={data_0}>
		<svelte:component this={constructors[1]} bind:this={components[1]} data={data_1} {form} />
	</svelte:component>
{:else}
	<svelte:component this={constructors[0]} bind:this={components[0]} data={data_0} {form} />
{/if}

{#if mounted}
	<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
		{#if navigated}
			{title}
		{/if}
	</div>
{/if}

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\app.js
==================================================
export { matchers } from './matchers.js';

export const nodes = [
	() => import('./nodes/0'),
	() => import('./nodes/1'),
	() => import('./nodes/2'),
	() => import('./nodes/3'),
	() => import('./nodes/4'),
	() => import('./nodes/5'),
	() => import('./nodes/6'),
	() => import('./nodes/7')
];

export const server_loads = [];

export const dictionary = {
		"/": [2],
		"/categories": [3],
		"/login": [4],
		"/systems": [5],
		"/tenants": [6],
		"/users": [7]
	};

export const hooks = {
	handleError: (({ error }) => { console.error(error) }),
};

export { default as root } from '../root.svelte';

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\matchers.js
==================================================
export const matchers = {};

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\nodes\0.js
==================================================
export { default as component } from "../../../../src/routes/+layout.svelte";

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\nodes\1.js
==================================================
export { default as component } from "../../../../node_modules/@sveltejs/kit/src/runtime/components/error.svelte";

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\nodes\2.js
==================================================
export { default as component } from "../../../../src/routes/+page.svelte";

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\nodes\3.js
==================================================
export { default as component } from "../../../../src/routes/categories/+page.svelte";

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\nodes\4.js
==================================================
export { default as component } from "../../../../src/routes/login/+page.svelte";

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\nodes\5.js
==================================================
export { default as component } from "../../../../src/routes/systems/+page.svelte";

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\nodes\6.js
==================================================
export { default as component } from "../../../../src/routes/tenants/+page.svelte";

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\client\nodes\7.js
==================================================
export { default as component } from "../../../../src/routes/users/+page.svelte";

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\generated\server\internal.js
==================================================

import root from '../root.svelte';
import { set_building } from '__sveltekit/environment';
import { set_assets } from '__sveltekit/paths';
import { set_private_env, set_public_env } from '../../../node_modules/@sveltejs/kit/src/runtime/shared-server.js';

export const options = {
	app_template_contains_nonce: false,
	csp: {"mode":"auto","directives":{"upgrade-insecure-requests":false,"block-all-mixed-content":false},"reportOnly":{"upgrade-insecure-requests":false,"block-all-mixed-content":false}},
	csrf_check_origin: true,
	track_server_fetches: false,
	embedded: false,
	env_public_prefix: 'PUBLIC_',
	env_private_prefix: '',
	hooks: null, // added lazily, via `get_hooks`
	preload_strategy: "modulepreload",
	root,
	service_worker: false,
	templates: {
		app: ({ head, body, assets, nonce, env }) => "<!doctype html>\r\n<html lang=\"es\">\r\n  <head>\r\n    <meta charset=\"utf-8\" />\r\n    <meta name=\"description\" content=\"ServiceManager - Mesa de Ayuda Empresarial - Portal Interno\" />\r\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\r\n    <link rel=\"icon\" href=\"" + assets + "/favicon.ico\" />\r\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\r\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\r\n    <link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" rel=\"stylesheet\">\r\n    " + head + "\r\n  </head>\r\n  <body data-sveltekit-preload-data=\"hover\">\r\n    <div style=\"display: contents\">" + body + "</div>\r\n  </body>\r\n</html>",
		error: ({ status, message }) => "<!doctype html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>" + message + "</title>\n\n\t\t<style>\n\t\t\tbody {\n\t\t\t\t--bg: white;\n\t\t\t\t--fg: #222;\n\t\t\t\t--divider: #ccc;\n\t\t\t\tbackground: var(--bg);\n\t\t\t\tcolor: var(--fg);\n\t\t\t\tfont-family:\n\t\t\t\t\tsystem-ui,\n\t\t\t\t\t-apple-system,\n\t\t\t\t\tBlinkMacSystemFont,\n\t\t\t\t\t'Segoe UI',\n\t\t\t\t\tRoboto,\n\t\t\t\t\tOxygen,\n\t\t\t\t\tUbuntu,\n\t\t\t\t\tCantarell,\n\t\t\t\t\t'Open Sans',\n\t\t\t\t\t'Helvetica Neue',\n\t\t\t\t\tsans-serif;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tjustify-content: center;\n\t\t\t\theight: 100vh;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t.error {\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t\tmax-width: 32rem;\n\t\t\t\tmargin: 0 1rem;\n\t\t\t}\n\n\t\t\t.status {\n\t\t\t\tfont-weight: 200;\n\t\t\t\tfont-size: 3rem;\n\t\t\t\tline-height: 1;\n\t\t\t\tposition: relative;\n\t\t\t\ttop: -0.05rem;\n\t\t\t}\n\n\t\t\t.message {\n\t\t\t\tborder-left: 1px solid var(--divider);\n\t\t\t\tpadding: 0 0 0 1rem;\n\t\t\t\tmargin: 0 0 0 1rem;\n\t\t\t\tmin-height: 2.5rem;\n\t\t\t\tdisplay: flex;\n\t\t\t\talign-items: center;\n\t\t\t}\n\n\t\t\t.message h1 {\n\t\t\t\tfont-weight: 400;\n\t\t\t\tfont-size: 1em;\n\t\t\t\tmargin: 0;\n\t\t\t}\n\n\t\t\t@media (prefers-color-scheme: dark) {\n\t\t\t\tbody {\n\t\t\t\t\t--bg: #222;\n\t\t\t\t\t--fg: #ddd;\n\t\t\t\t\t--divider: #666;\n\t\t\t\t}\n\t\t\t}\n\t\t</style>\n\t</head>\n\t<body>\n\t\t<div class=\"error\">\n\t\t\t<span class=\"status\">" + status + "</span>\n\t\t\t<div class=\"message\">\n\t\t\t\t<h1>" + message + "</h1>\n\t\t\t</div>\n\t\t</div>\n\t</body>\n</html>\n"
	},
	version_hash: "eelz0a"
};

export function get_hooks() {
	return {};
}

export { set_assets, set_building, set_private_env, set_public_env };


==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\types\route_meta_data.json
==================================================
{
	"/": [],
	"/categories": [],
	"/login": [],
	"/systems": [],
	"/tenants": [],
	"/users": []
}

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\types\src\routes\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<LayoutData>;
type LayoutRouteId = RouteId | "/" | "/categories" | "/login" | "/systems" | "/tenants" | "/users" | null
type LayoutParams = RouteParams & {  }
type LayoutParentData = EnsureDefined<{}>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;
export type LayoutServerData = null;
export type LayoutData = Expand<LayoutParentData>;

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\types\src\routes\categories\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/categories';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\types\src\routes\login\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/login';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\types\src\routes\systems\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/systems';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\types\src\routes\tenants\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/tenants';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-internal\.svelte-kit\types\src\routes\users\$types.d.ts
==================================================
import type * as Kit from '@sveltejs/kit';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// @ts-ignore
type MatcherParam<M> = M extends (param : string) => param is infer U ? U extends string ? U : string : string;
type RouteParams = {  };
type RouteId = '/users';
type MaybeWithVoid<T> = {} extends T ? T | void : T;
export type RequiredKeys<T> = { [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K; }[keyof T];
type OutputDataShape<T> = MaybeWithVoid<Omit<App.PageData, RequiredKeys<T>> & Partial<Pick<App.PageData, keyof T & keyof App.PageData>> & Record<string, any>>
type EnsureDefined<T> = T extends null | undefined ? {} : T;
type OptionalUnion<U extends Record<string, any>, A extends keyof U = U extends U ? keyof U : never> = U extends unknown ? { [P in Exclude<A, keyof U>]?: never } & U : never;
export type Snapshot<T = any> = Kit.Snapshot<T>;
type PageParentData = EnsureDefined<import('../$types.js').LayoutData>;

export type PageServerData = null;
export type PageData = Expand<PageParentData>;

==================================================
ARCHIVO: .\frontend-internal\src\app.css
==================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

/* Custom base styles */
@layer base {
  html {
    font-family: 'Inter', system-ui, sans-serif;
  }
  
  body {
    @apply text-gray-900 bg-gray-50;
  }
  
  /* Focus styles */
  *:focus {
    @apply outline-none ring-2 ring-primary-500 ring-offset-2;
  }
  
  /* Selection styles */
  ::selection {
    @apply bg-primary-100 text-primary-900;
  }
}

/* Custom component styles */
@layer components {
  /* Card component */
  .card {
    @apply bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden;
  }
  
  .card-content {
    @apply p-6;
  }
  
  /* Button variants */
  .btn {
    @apply inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none px-4 py-2;
  }
  
  .btn-primary {
    @apply bg-primary-600 text-white hover:bg-primary-700 active:bg-primary-800;
  }
  
  .btn-secondary {
    @apply bg-secondary-100 text-secondary-900 hover:bg-secondary-200 active:bg-secondary-300;
  }
  
  .btn-success {
    @apply bg-success-600 text-white hover:bg-success-700 active:bg-success-800;
  }
  
  .btn-warning {
    @apply bg-warning-600 text-white hover:bg-warning-700 active:bg-warning-800;
  }
  
  .btn-error {
    @apply bg-error-600 text-white hover:bg-error-700 active:bg-error-800;
  }
  
  .btn-ghost {
    @apply bg-transparent hover:bg-secondary-100 active:bg-secondary-200;
  }
  
  /* Card styles */
  .card {
    @apply bg-white rounded-lg shadow-sm border border-gray-200;
  }
  
  .card-header {
    @apply p-6 border-b border-gray-200;
  }
  
  .card-content {
    @apply p-6;
  }
  
  .card-footer {
    @apply p-6 border-t border-gray-200 bg-gray-50 rounded-b-lg;
  }
  
  /* Form styles */
  .form-input {
    @apply block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500 sm:text-sm;
  }
  
  .form-label {
    @apply block text-sm font-medium text-gray-700 mb-1;
  }
  
  .form-error {
    @apply text-sm text-error-600 mt-1;
  }
  
  /* Status badges */
  .badge {
    @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
  }

  /* Animations */
  @keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
  }
  
  .animate-fade-in {
    animation: fadeIn 0.3s ease-out forwards;
  }
  
  @keyframes slideUp {
    from { 
      opacity: 0;
      transform: translateY(10px);
    }
    to { 
      opacity: 1;
      transform: translateY(0);
    }
  }
  
  .animate-slide-up {
    animation: slideUp 0.4s ease-out forwards;
  }
  
  .badge-new {
    @apply badge bg-blue-100 text-blue-800;
  }
  
  .badge-in-progress {
    @apply badge bg-yellow-100 text-yellow-800;
  }
  
  .badge-waiting {
    @apply badge bg-orange-100 text-orange-800;
  }
  
  .badge-resolved {
    @apply badge bg-green-100 text-green-800;
  }
  
  .badge-closed {
    @apply badge bg-gray-100 text-gray-800;
  }
  
  .badge-reopened {
    @apply badge bg-red-100 text-red-800;
  }
  
  /* Priority badges */
  .badge-priority-low {
    @apply badge bg-gray-100 text-gray-600;
  }
  
  .badge-priority-medium {
    @apply badge bg-blue-100 text-blue-700;
  }
  
  .badge-priority-high {
    @apply badge bg-orange-100 text-orange-700;
  }
  
  .badge-priority-urgent {
    @apply badge bg-red-100 text-red-700;
  }
  
  /* Role badges */
  .badge-admin {
    @apply badge bg-purple-100 text-purple-800;
  }
  
  .badge-support-manager {
    @apply badge bg-indigo-100 text-indigo-800;
  }
  
  .badge-agent {
    @apply badge bg-blue-100 text-blue-800;
  }
  
  .badge-auditor {
    @apply badge bg-gray-100 text-gray-800;
  }
}

/* Custom utility classes */
@layer utilities {
  .text-balance {
    text-wrap: balance;
  }
  
  /* Loading spinner */
  .spinner {
    @apply animate-spin rounded-full border-2 border-gray-300 border-t-primary-600;
  }
  
  /* Animations */
  .animate-fade-in {
    animation: fadeIn 0.3s ease-in-out;
  }
  
  .animate-slide-up {
    animation: slideUp 0.3s ease-out;
  }
}

/* Custom keyframes */
@keyframes fadeIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes slideUp {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

==================================================
ARCHIVO: .\frontend-internal\src\app.html
==================================================
<!doctype html>
<html lang="es">
  <head>
    <meta charset="utf-8" />
    <meta name="description" content="ServiceManager - Mesa de Ayuda Empresarial - Portal Interno" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" href="%sveltekit.assets%/favicon.ico" />
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
    %sveltekit.head%
  </head>
  <body data-sveltekit-preload-data="hover">
    <div style="display: contents">%sveltekit.body%</div>
  </body>
</html>

==================================================
ARCHIVO: .\frontend-internal\src\lib\components\Header.svelte
==================================================
<script lang="ts">
  import { auth } from '$lib/stores/auth.js';
  
  export let toggleSidebar: () => void;
  
  function handleLogout() {
    auth.logout();
  }
  
  let isMenuOpen = false;
  
  function toggleMenu() {
    isMenuOpen = !isMenuOpen;
  }
</script>

<header class="bg-white shadow-sm border-b border-gray-200">
  <div class="flex justify-between items-center px-4 py-3">
    <!-- Left side -->
    <div class="flex items-center space-x-4">
      <button
        on:click={toggleSidebar}
        class="p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primary-500 lg:hidden"
      >
        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
        </svg>
      </button>
      
      <div class="hidden lg:block">
        <h1 class="text-lg font-semibold text-gray-900">ServiceManager Internal</h1>
      </div>
    </div>
    
    <!-- Right side -->
    <div class="flex items-center space-x-4">
      <!-- User menu -->
      <div class="relative">
        <button
          on:click={toggleMenu}
          class="flex items-center space-x-2 text-gray-700 hover:text-primary-600 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-md p-2"
        >
          <div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
            <span class="text-primary-600 text-sm font-medium">
              {$auth.user?.first_name?.[0]}{$auth.user?.last_name?.[0]}
            </span>
          </div>
          <span class="hidden sm:block text-sm">
            {$auth.user?.first_name} {$auth.user?.last_name}
          </span>
          <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
          </svg>
        </button>

        {#if isMenuOpen}
          <div class="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg border border-gray-200 z-50">
            <div class="py-1">
              <div class="px-4 py-2 text-xs text-gray-500 border-b border-gray-200">
                {$auth.user?.email}
              </div>
              <a
                href="/profile"
                class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
                on:click={() => isMenuOpen = false}
              >
                Mi Perfil
              </a>
              <button
                on:click={handleLogout}
                class="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
              >
                Cerrar Sesión
              </button>
            </div>
          </div>
        {/if}
      </div>
    </div>
  </div>
</header>

<!-- Backdrop for mobile menu -->
{#if isMenuOpen}
  <div 
    class="fixed inset-0 z-40 lg:hidden" 
    on:click={() => isMenuOpen = false}
  ></div>
{/if}

==================================================
ARCHIVO: .\frontend-internal\src\lib\components\Icon.svelte
==================================================
<script lang="ts">
  export let name: string;
  export let size: string = 'w-5 h-5';
  export let className: string = '';

  const icons: Record<string, string> = {
    home: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
    ticket: 'M15 5v2m0 4v2m0 4v2M5 5a2 2 0 00-2 2v3a2 2 0 110 4v3a2 2 0 002 2h14a2 2 0 002-2v-3a2 2 0 110-4V7a2 2 0 00-2-2H5z',
    plus: 'M12 4v16m8-8H4',
    user: 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z',
    menu: 'M4 6h16M4 12h16M4 18h16',
    x: 'M6 18L18 6M6 6l12 12',
    bell: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9',
    search: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
    edit: 'M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z',
    trash: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',
    eye: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z',
    clock: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
    check: 'M5 13l4 4L19 7',
    chevronDown: 'M19 9l-7 7-7-7',
    dashboard: 'M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z',
    users: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z',
    chart: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
    settings: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z'
  };

  $: path = icons[name] || icons.home;
</script>

<svg 
  class="{size} {className}" 
  fill="none" 
  stroke="currentColor" 
  viewBox="0 0 24 24" 
  xmlns="http://www.w3.org/2000/svg"
>
  <path 
    stroke-linecap="round" 
    stroke-linejoin="round" 
    stroke-width="2" 
    d={path}
  />
</svg>

==================================================
ARCHIVO: .\frontend-internal\src\lib\components\Modal.svelte
==================================================
<script>
  import { createEventDispatcher, onMount, onDestroy } from 'svelte';
  
  export let open = false;
  export let title = '';
  
  const dispatch = createEventDispatcher();
  
  function close() {
    dispatch('close');
  }

  function handleKeydown(e) {
    if (e.key === 'Escape' && open) {
      close();
    }
  }
</script>

<svelte:window on:keydown={handleKeydown}/>

{#if open}
  <div class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
    <div class="flex items-end justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
      
      <div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" aria-hidden="true" on:click={close}></div>

      <span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>

      <div class="inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
        <div class="sm:flex sm:items-start">
          <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left w-full">
            <h3 class="text-lg leading-6 font-medium text-gray-900" id="modal-title">
              {title}
            </h3>
            <div class="mt-2 text-sm text-gray-500">
              <slot />
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
{/if}


==================================================
ARCHIVO: .\frontend-internal\src\lib\components\Sidebar.svelte
==================================================
<script lang="ts">
  import { auth } from '$lib/stores/auth.js';
  import { page } from '$app/stores';
  
  export let open = false;
  
  // Navigation items based on user role
  $: navigation = getNavigationForRole($auth.user?.role);
  
  function getNavigationForRole(role: string | undefined) {
    const baseNavigation = [
      {
        name: 'Dashboard',
        href: '/',
        icon: 'M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z'
      },
      {
        name: 'Tickets',
        href: '/tickets',
        icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2'
      }
    ];
    
    if (role === 'ADMIN' || role === 'SUPPORT_MANAGER') {
      baseNavigation.push(
        {
          name: 'Clientes',
          href: '/tenants',
          icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4'
        },
        {
          name: 'Usuarios',
          href: '/users',
          icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z'
        },
        {
          name: 'Categorías',
          href: '/categories',
          icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10'
        },
        {
          name: 'Sistemas',
          href: '/systems',
          icon: 'M5 12a1 1 0 102 0V6.414l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L5 6.414V12zm14 0a1 1 0 10-2 0v5.586l-1.293-1.293a1 1 0 00-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L19 17.586V12z'
        }
      );
    }
    
    if (role === 'ADMIN') {
      baseNavigation.push(
        {
          name: 'SLA Management',
          href: '/sla',
          icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'
        },
        {
          name: 'Reportes',
          href: '/reports',
          icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z'
        },
        {
          name: 'Auditoría',
          href: '/audit',
          icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z'
        }
      );
    }
    
    return baseNavigation;
  }
  
  function isCurrentPage(href: string) {
    return $page.url.pathname === href || 
           ($page.url.pathname.startsWith(href) && href !== '/');
  }
</script>

<!-- Mobile sidebar backdrop -->
{#if open}
  <div class="fixed inset-0 z-40 lg:hidden">
    <div class="fixed inset-0 bg-gray-600 bg-opacity-75" on:click={() => open = false}></div>
  </div>
{/if}

<!-- Sidebar -->
<div class="fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-lg transform {open ? 'translate-x-0' : '-translate-x-full'} transition-transform duration-300 ease-in-out lg:translate-x-0 lg:static lg:inset-0">
  <div class="flex flex-col h-full">
    <!-- Logo -->
    <div class="flex items-center justify-between h-16 px-6 bg-primary-600">
      <div class="flex items-center space-x-2">
        <div class="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
          <svg class="w-5 h-5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192L5.636 18.364M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" />
          </svg>
        </div>
        <div class="text-white">
          <h1 class="text-sm font-semibold">ServiceManager</h1>
          <p class="text-xs text-primary-100">Panel Interno</p>
        </div>
      </div>
      
      <button
        on:click={() => open = false}
        class="p-2 rounded-md text-primary-100 hover:text-white hover:bg-primary-500 lg:hidden"
      >
        <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
        </svg>
      </button>
    </div>
    
    <!-- Navigation -->
    <nav class="flex-1 px-4 py-6 space-y-2">
      {#each navigation as item}
        <a
          href={item.href}
          class="flex items-center space-x-3 px-3 py-2 rounded-md text-sm font-medium transition-colors {
            isCurrentPage(item.href)
              ? 'bg-primary-100 text-primary-700'
              : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900'
          }"
          on:click={() => open = false}
        >
          <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d={item.icon} />
          </svg>
          <span>{item.name}</span>
        </a>
      {/each}
    </nav>
    
    <!-- User info -->
    <div class="px-4 py-4 border-t border-gray-200">
      <div class="flex items-center space-x-3">
        <div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
          <span class="text-primary-600 text-sm font-medium">
            {$auth.user?.first_name?.[0]}{$auth.user?.last_name?.[0]}
          </span>
        </div>
        <div class="flex-1 min-w-0">
          <p class="text-sm font-medium text-gray-900 truncate">
            {$auth.user?.first_name} {$auth.user?.last_name}
          </p>
          <p class="text-xs text-gray-500 truncate">
            {$auth.user?.role === 'ADMIN' ? 'Administrador' :
             $auth.user?.role === 'SUPPORT_MANAGER' ? 'Gerente de Soporte' :
             $auth.user?.role === 'AGENT' ? 'Agente' : 'Auditor'}
          </p>
        </div>
      </div>
    </div>
  </div>
</div>

==================================================
ARCHIVO: .\frontend-internal\src\lib\components\Toast.svelte
==================================================
<script lang="ts">
  export let type: 'success' | 'error' | 'warning' | 'info' = 'info';
  export let message: string;
  export let duration: number = 5000;
  export let dismissible: boolean = true;
  
  let visible = true;
  let timeoutId: NodeJS.Timeout;
  
  // Auto-dismiss after duration
  if (duration > 0) {
    timeoutId = setTimeout(() => {
      visible = false;
    }, duration);
  }
  
  function dismiss() {
    if (timeoutId) clearTimeout(timeoutId);
    visible = false;
  }
  
  // Cleanup timeout on destroy
  import { onDestroy } from 'svelte';
  onDestroy(() => {
    if (timeoutId) clearTimeout(timeoutId);
  });
  
  // Style mapping
  const typeStyles = {
    success: {
      container: 'bg-success-50 border-success-200 text-success-800',
      icon: 'text-success-400'
    },
    error: {
      container: 'bg-error-50 border-error-200 text-error-800',
      icon: 'text-error-400'
    },
    warning: {
      container: 'bg-warning-50 border-warning-200 text-warning-800',
      icon: 'text-warning-400'
    },
    info: {
      container: 'bg-blue-50 border-blue-200 text-blue-800',
      icon: 'text-blue-400'
    }
  };
  
  // Icon mapping
  const typeIcons = {
    success: 'M5 13l4 4L19 7',
    error: 'M6 18L18 6M6 6l12 12',
    warning: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z',
    info: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
  };
</script>

{#if visible}
  <div class="fixed top-4 right-4 max-w-sm w-full z-50 animate-slide-up">
    <div class="rounded-lg border p-4 shadow-lg {typeStyles[type].container}">
      <div class="flex items-start">
        <div class="flex-shrink-0">
          <svg 
            class="h-5 w-5 {typeStyles[type].icon}" 
            fill="none" 
            stroke="currentColor" 
            viewBox="0 0 24 24"
          >
            <path 
              stroke-linecap="round" 
              stroke-linejoin="round" 
              stroke-width="2" 
              d={typeIcons[type]}
            />
          </svg>
        </div>
        
        <div class="ml-3 flex-1">
          <p class="text-sm font-medium">
            {message}
          </p>
        </div>
        
        {#if dismissible}
          <div class="ml-4 flex-shrink-0">
            <button
              type="button"
              class="inline-flex rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 {typeStyles[type].icon} hover:opacity-75"
              on:click={dismiss}
            >
              <span class="sr-only">Cerrar</span>
              <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
              </svg>
            </button>
          </div>
        {/if}
      </div>
    </div>
  </div>
{/if}

<style>
  @keyframes slide-up {
    from {
      transform: translateY(-100%);
      opacity: 0;
    }
    to {
      transform: translateY(0);
      opacity: 1;
    }
  }
  
  .animate-slide-up {
    animation: slide-up 0.3s ease-out;
  }
</style>

==================================================
ARCHIVO: .\frontend-internal\src\lib\stores\api.ts
==================================================
import { writable } from 'svelte/store';

export const api = {
  async getClients() {
    const response = await fetch('/api/v1/clients');
    if (!response.ok) {
      throw new Error('Error fetching clients');
    }
    return await response.json();
  },

  async createClient(client) {
    const response = await fetch('/api/v1/clients', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(client),
    });
    if (!response.ok) {
      throw new Error('Error creating client');
    }
    return await response.json();
  },

  async updateClient(clientId, client) {
    const response = await fetch(`/api/v1/clients/${clientId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(client),
    });
    if (!response.ok) {
      throw new Error('Error updating client');
    }
    return await response.json();
  },

  async deleteClient(clientId) {
    const response = await fetch(`/api/v1/clients/${clientId}`, {
      method: 'DELETE',
    });
    if (!response.ok) {
      throw new Error('Error deleting client');
    }
    return await response.json();
  },
};

==================================================
ARCHIVO: .\frontend-internal\src\lib\stores\auth.ts
==================================================
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';

// Types
export interface InternalUser {
  id: string;
  email: string;
  first_name: string;
  last_name: string;
  role: 'ADMIN' | 'SUPPORT_MANAGER' | 'AGENT' | 'AUDITOR';
  is_active: boolean;
  is_two_factor_enabled: boolean;
  created_at: string;
  tenant_id: string;
}

export interface AuthState {
  user: InternalUser | null;
  token: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
}

export interface LoginRequest {
  email: string;
  password: string;
  tenant_slug: string;
  totp_code?: string;
}

export interface LoginResponse {
  access_token: string;
  refresh_token: string;
  token_type: string;
  expires_in: number;
  user: InternalUser;
}

export interface RefreshTokenRequest {
  refresh_token: string;
}

export interface TokenResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
}

// Initial state
const initialState: AuthState = {
  user: null,
  token: null,
  refreshToken: null,
  isAuthenticated: false,
  isLoading: false
};

// Create auth store
function createAuthStore() {
  const { subscribe, set, update } = writable<AuthState>(initialState);

  return {
    subscribe,
    
    // Initialize auth from localStorage
    init: () => {
      if (typeof window !== 'undefined') {
        const token = localStorage.getItem('internal_auth_token');
        const refreshToken = localStorage.getItem('internal_auth_refresh_token');
        const user = localStorage.getItem('internal_auth_user');
        
        if (token && user) {
          try {
            const parsedUser = JSON.parse(user);
            set({
              user: parsedUser,
              token,
              refreshToken: refreshToken || null,
              isAuthenticated: true,
              isLoading: false
            });
          } catch (error) {
            console.error('Error parsing stored auth data:', error);
            localStorage.removeItem('internal_auth_token');
            localStorage.removeItem('internal_auth_refresh_token');
            localStorage.removeItem('internal_auth_user');
          }
        }
      }
    },

    // Login
    login: async (credentials: LoginRequest): Promise<void> => {
      update(state => ({ ...state, isLoading: true }));
      
      try {
        const response = await fetch('/api/v1/auth/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(credentials)
        });

        if (!response.ok) {
          const error = await response.json();
          throw new Error(error.detail || 'Login failed');
        }

        const data: LoginResponse = await response.json();
        
        // Store auth data
        if (typeof window !== 'undefined') {
          localStorage.setItem('internal_auth_token', data.access_token);
          if (data.refresh_token) {
              localStorage.setItem('internal_auth_refresh_token', data.refresh_token);
          }
          localStorage.setItem('internal_auth_user', JSON.stringify(data.user));
        }

        set({
          user: data.user,
          token: data.access_token,
          refreshToken: data.refresh_token,
          isAuthenticated: true,
          isLoading: false
        });
      } catch (error) {
        update(state => ({ ...state, isLoading: false }));
        throw error;
      }
    },

    // Refresh Session
    refreshSession: async (): Promise<void> => {
        // Need to get current state to access refresh token, logic simplified
        let currentRefreshToken: string | null = null;
        if (typeof window !== 'undefined') {
            currentRefreshToken = localStorage.getItem('internal_auth_refresh_token');
        }

        if (!currentRefreshToken) {
            throw new Error("No refresh token available");
        }

        update (state => ({ ...state, isLoading: true }));

        try {
            const response = await fetch('/api/v1/auth/refresh', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ refresh_token: currentRefreshToken })
            });

            if (!response.ok) {
                 // If refresh fails, logout
                 if (response.status === 401 || response.status === 403) {
                     auth.logout();
                 }
                 const error = await response.json();
                 throw new Error(error.detail || 'Refresh failed');
            }

            const data: TokenResponse = await response.json();

            // Update token in storage and state
            if (typeof window !== 'undefined') {
                localStorage.setItem('internal_auth_token', data.access_token);
            }

            update(state => ({
                ...state,
                token: data.access_token,
                isLoading: false
            }));

        } catch (error) {
             update(state => ({ ...state, isLoading: false }));
             throw error;
        }
    },

    // Logout
    logout: () => {
      if (typeof window !== 'undefined') {
        localStorage.removeItem('internal_auth_token');
        localStorage.removeItem('internal_auth_refresh_token');
        localStorage.removeItem('internal_auth_user');
      }
      set(initialState);
      // Optional: Redirect to login
      if (typeof window !== 'undefined') {
          window.location.href = '/login';
      }
    },

    // Update user data
    updateUser: (user: InternalUser) => {
      update(state => ({ ...state, user }));
      if (typeof window !== 'undefined') {
        localStorage.setItem('internal_auth_user', JSON.stringify(user));
      }
    },

    // Set loading state
    setLoading: (isLoading: boolean) => {
      update(state => ({ ...state, isLoading }));
    }
  };
}

export const auth = createAuthStore();

==================================================
ARCHIVO: .\frontend-internal\src\lib\stores\toast.ts
==================================================
// Toast notification store
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';

export interface ToastMessage {
  id: string;
  type: 'success' | 'error' | 'warning' | 'info';
  message: string;
  duration?: number;
}

interface ToastState {
  toasts: ToastMessage[];
}

const initialState: ToastState = {
  toasts: []
};

function createToastStore() {
  const { subscribe, update }: Writable<ToastState> = writable(initialState);

  return {
    subscribe,
    
    show: (type: ToastMessage['type'], message: string, duration = 5000) => {
      const id = Math.random().toString(36).substring(2, 9);
      const toast: ToastMessage = { id, type, message, duration };
      
      update(state => ({
        toasts: [...state.toasts, toast]
      }));
      
      // Auto-remove after duration
      if (duration > 0) {
        setTimeout(() => {
          update(state => ({
            toasts: state.toasts.filter(t => t.id !== id)
          }));
        }, duration);
      }
      
      return id;
    },
    
    dismiss: (id: string) => {
      update(state => ({
        toasts: state.toasts.filter(t => t.id !== id)
      }));
    },
    
    clear: () => {
      update(() => initialState);
    },
    
    // Convenience methods
    success: (message: string, duration?: number) => {
      return createToastStore().show('success', message, duration);
    },
    
    error: (message: string, duration?: number) => {
      return createToastStore().show('error', message, duration);
    },
    
    warning: (message: string, duration?: number) => {
      return createToastStore().show('warning', message, duration);
    },
    
    info: (message: string, duration?: number) => {
      return createToastStore().show('info', message, duration);
    }
  };
}

export const toast = createToastStore();

==================================================
ARCHIVO: .\frontend-internal\src\lib\utils\api.ts
==================================================
import { auth } from '$lib/stores/auth';
import { get } from 'svelte/store';

const API_BASE = '/api/v1';

interface RequestOptions extends RequestInit {
  params?: Record<string, string>;
}

async function request<T>(endpoint: string, options: RequestOptions = {}): Promise<T> {
  const { params, ...init } = options;
  
  let url = `${API_BASE}${endpoint}`;
  if (params) {
    const searchParams = new URLSearchParams(params);
    url += `?${searchParams.toString()}`;
  }

  const authState = get(auth);
  const token = authState.token || (typeof window !== 'undefined' ? localStorage.getItem('internal_auth_token') : null);

  const headers = new Headers(init.headers);
  if (token) {
    headers.set('Authorization', `Bearer ${token}`);
  }
  if (!headers.has('Content-Type')) {
    headers.set('Content-Type', 'application/json');
  }

  const response = await fetch(url, {
    ...init,
    headers
  });

  if (response.status === 401) {
    // Token expired or invalid
    if (typeof window !== 'undefined') {
        localStorage.removeItem('internal_auth_token');
        localStorage.removeItem('internal_auth_user');
        window.location.href = '/login'; 
    }
    throw new Error('Unauthorized');
  }

  if (!response.ok) {
    const errorData = await response.json().catch(() => ({}));
    throw new Error(errorData.detail || `API error: ${response.statusText}`);
  }

  // Handle empty responses (like 204 No Content)
  if (response.status === 204) {
      return {} as T;
  }

  return response.json();
}

export const api = {
  get: <T>(endpoint: string, params?: Record<string, string>) => 
    request<T>(endpoint, { method: 'GET', params }),
    
  post: <T>(endpoint: string, body: any) => 
    request<T>(endpoint, { method: 'POST', body: JSON.stringify(body) }),
    
  put: <T>(endpoint: string, body: any) => 
    request<T>(endpoint, { method: 'PUT', body: JSON.stringify(body) }),
    
  patch: <T>(endpoint: string, body: any) => 
    request<T>(endpoint, { method: 'PATCH', body: JSON.stringify(body) }),
    
  delete: <T>(endpoint: string) => 
    request<T>(endpoint, { method: 'DELETE' })
};


==================================================
ARCHIVO: .\frontend-internal\src\routes\+layout.svelte
==================================================
<script lang="ts">
  import Header from '$lib/components/Header.svelte';
  import Sidebar from '$lib/components/Sidebar.svelte';
  import Toast from '$lib/components/Toast.svelte';
  import { toast } from '$lib/stores/toast.js';
  import { onMount } from 'svelte';
  import { auth } from '$lib/stores/auth.js';
  import '../app.css';

  let sidebarOpen = false;

  onMount(() => {
    auth.init();
  });

  function toggleSidebar() {
    sidebarOpen = !sidebarOpen;
  }
</script>

<div class="min-h-screen bg-gray-50">
  {#if $auth.isAuthenticated}
    <!-- Internal Layout with Sidebar -->
    <div class="flex h-screen overflow-hidden">
      <!-- Sidebar -->
      <Sidebar bind:open={sidebarOpen} />
      
      <!-- Main content -->
      <div class="flex-1 flex flex-col overflow-hidden">
        <Header {toggleSidebar} />
        
        <main class="flex-1 overflow-auto">
          <slot />
        </main>
      </div>
    </div>
  {:else}
    <!-- Login Layout -->
    <main class="flex-1">
      <slot />
    </main>
  {/if}
  
  <!-- Toast notifications -->
  {#each $toast.toasts as toastMessage (toastMessage.id)}
    <Toast
      type={toastMessage.type}
      message={toastMessage.message}
      duration={toastMessage.duration}
      on:dismiss={() => toast.dismiss(toastMessage.id)}
    />
  {/each}
</div>

==================================================
ARCHIVO: .\frontend-internal\src\routes\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { auth } from '$lib/stores/auth.js';
  import { goto } from '$app/navigation';
  import Icon from '$lib/components/Icon.svelte';
  
  onMount(() => {
    if (!$auth.isAuthenticated) {
      goto('/login');
    }
  });

  const cards = [
    {
      title: 'Clientes',
      description: 'Gestión de organizaciones y tenants',
      icon: 'users',
      href: '/tenants',
      color: 'bg-blue-500'
    },
    {
      title: 'Usuarios',
      description: 'Administración de usuarios y roles',
      icon: 'user-plus',
      href: '/users',
      color: 'bg-green-500'
    },
    {
      title: 'Sistemas',
      description: 'Catálogo de sistemas soportados',
      icon: 'server',
      href: '/systems',
      color: 'bg-purple-500'
    },
    {
      title: 'Categorías',
      description: 'Clasificación de tickets',
      icon: 'tag',
      href: '/categories',
      color: 'bg-orange-500'
    }
  ];
</script>

<svelte:head>
  <title>Dashboard Admin - ServiceManager</title>
</svelte:head>

<div class="px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
  <div class="md:flex md:items-center md:justify-between">
    <div class="flex-1 min-w-0">
      <h2 class="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
        Panel de Administración
      </h2>
      <p class="mt-1 text-sm text-gray-500">
          Bienvenido al sistema de gestión interna.
      </p>
    </div>
  </div>

  <div class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
    {#each cards as card}
      <a href={card.href} class="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow duration-200 cursor-pointer group">
        <div class="p-5">
          <div class="flex items-center">
            <div class="flex-shrink-0">
              <div class="{card.color} rounded-md p-3">
                 <!-- Simple SVG Icon placeholder since Icon component might expect specific names that map to SVGs -->
                 <svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
                 </svg>
              </div>
            </div>
            <div class="ml-5 w-0 flex-1">
              <dl>
                <dt class="text-sm font-medium text-gray-500 truncate">
                  {card.title}
                </dt>
                <dd>
                  <div class="text-xs text-gray-900 font-light mt-1">
                    {card.description}
                  </div>
                </dd>
              </dl>
            </div>
          </div>
        </div>
        <div class="bg-gray-50 px-5 py-3">
          <div class="text-sm">
            <span class="font-medium text-cyan-700 hover:text-cyan-900">
              Ver detalles
            </span>
          </div>
        </div>
      </a>
    {/each}
  </div>
</div>


==================================================
ARCHIVO: .\frontend-internal\src\routes\categories\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { api } from '$lib/utils/api';
  import { toast } from '$lib/stores/toast';
  import Modal from '$lib/components/Modal.svelte';

  let categories = [];
  let tenants = [];
  let isLoading = false;
  let showModal = false;
  let editingCategory = null;

  let formData = {
    name: '',
    description: '',
    tenant_id: '',
    is_active: true
  };

  async function loadData() {
    isLoading = true;
    try {
      const [categoriesData, tenantsData] = await Promise.all([
        api.get('/categories/'),
        api.get('/tenants/')
      ]);
      categories = categoriesData;
      tenants = tenantsData;
    } catch (e) {
      toast.error('Error cargando datos');
    } finally {
      isLoading = false;
    }
  }

  function openCreateModal() {
    editingCategory = null;
    formData = { name: '', description: '', tenant_id: '', is_active: true };
    showModal = true;
  }

  function openEditModal(category) {
    editingCategory = category;
    formData = { 
        name: category.name, 
        description: category.description, 
        tenant_id: category.tenant_id || '', 
        is_active: category.is_active 
    };
    showModal = true;
  }

  async function handleSubmit() {
    try {
        const payload = { ...formData };
        if (!payload.tenant_id) payload.tenant_id = null;

      if (editingCategory) {
        await api.put(`/categories/${editingCategory.id}`, payload);
        toast.success('Categoría actualizada');
      } else {
        await api.post('/categories/', payload);
        toast.success('Categoría creada');
      }
      showModal = false;
      loadData();
    } catch (e) {
      toast.error(e.message || 'Error guardando categoría');
    }
  }
  
  function getTenantName(id) {
    if (!id) return 'Global';
    const t = tenants.find(t => t.id === id);
    return t ? t.name : id;
  }

  onMount(loadData);
</script>

<div class="px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
  <div class="sm:flex sm:items-center">
    <div class="sm:flex-auto">
      <h1 class="text-xl font-semibold text-gray-900">Categorías de Tickets</h1>
      <p class="mt-2 text-sm text-gray-700">Gestión de categorías para clasificación de tickets.</p>
    </div>
    <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
      <button
        type="button"
        on:click={openCreateModal}
        class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 sm:w-auto"
      >
        Nueva Categoría
      </button>
    </div>
  </div>

  <div class="mt-8 flex flex-col">
    <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
      <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
        <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
          <table class="min-w-full divide-y divide-gray-300">
            <thead class="bg-gray-50">
              <tr>
                <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Nombre</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Descripción</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Tipo (Cliente)</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Estado</th>
                <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
                  <span class="sr-only">Acciones</span>
                </th>
              </tr>
            </thead>
            <tbody class="divide-y divide-gray-200 bg-white">
              {#if isLoading}
                 <tr><td colspan="5" class="text-center py-4">Cargando...</td></tr>
              {:else if categories.length === 0}
                 <tr><td colspan="5" class="text-center py-4">No hay categorías registradas</td></tr>
              {:else}
                {#each categories as category}
                  <tr>
                    <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{category.name}</td>
                    <td class="px-3 py-4 text-sm text-gray-500 max-w-xs truncate">{category.description || '-'}</td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
                      <span class:bg-blue-100={!category.tenant_id} class:text-blue-800={!category.tenant_id} class:bg-gray-100={category.tenant_id} class:text-gray-800={category.tenant_id} class="inline-flex rounded-full px-2 text-xs font-semibold leading-5">
                        {getTenantName(category.tenant_id)}
                      </span>
                    </td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
                      <span class:bg-green-100={category.is_active} class:text-green-800={category.is_active} class:bg-red-100={!category.is_active} class:text-red-800={!category.is_active} class="inline-flex rounded-full px-2 text-xs font-semibold leading-5">
                        {category.is_active ? 'Activo' : 'Inactivo'}
                      </span>
                    </td>
                    <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
                      <button on:click={() => openEditModal(category)} class="text-indigo-600 hover:text-indigo-900">Editar</button>
                    </td>
                  </tr>
                {/each}
              {/if}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</div>

<Modal open={showModal} title={editingCategory ? 'Editar Categoría' : 'Nueva Categoría'} on:close={() => showModal = false}>
  <form on:submit|preventDefault={handleSubmit} class="space-y-4">
    <div>
      <label for="name" class="block text-sm font-medium text-gray-700">Nombre</label>
      <input type="text" id="name" bind:value={formData.name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
    </div>

    <div>
      <label for="description" class="block text-sm font-medium text-gray-700">Descripción</label>
      <textarea id="description" bind:value={formData.description} rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"></textarea>
    </div>

    <div>
      <label for="tenant" class="block text-sm font-medium text-gray-700">Cliente (Opcional - Específico para un cliente)</label>
      <select id="tenant" bind:value={formData.tenant_id} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
        <option value="">-- Global (Para todos) --</option>
        {#each tenants as tenant}
          <option value={tenant.id}>{tenant.name}</option>
        {/each}
      </select>
    </div>

    <div class="flex items-center">
      <input type="checkbox" id="is_active" bind:checked={formData.is_active} class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
      <label for="is_active" class="ml-2 block text-sm text-gray-900">Activo</label>
    </div>

    <div class="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
      <button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:col-start-2 sm:text-sm">
        Guardar
      </button>
      <button type="button" on:click={() => showModal = false} class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:col-start-1 sm:text-sm">
        Cancelar
      </button>
    </div>
  </form>
</Modal>


==================================================
ARCHIVO: .\frontend-internal\src\routes\login\+page.svelte
==================================================
<script lang="ts">
  import { auth } from '$lib/stores/auth.js';
  import { toast } from '$lib/stores/toast.js';
  import { goto } from '$app/navigation';
  import { onMount } from 'svelte';
  import Icon from '$lib/components/Icon.svelte';
  
  let email = '';
  let password = '';
  let totpCode = '';
  let isLoading = false;
  let showTwoFactor = false;
  let errorMessage = '';
  
  onMount(() => {
    // Redirect if already authenticated
    if ($auth.isAuthenticated) {
      goto('/');
    }
  });
  
  async function handleLogin() {
    if (!email || !password) {
      errorMessage = 'Por favor completa todos los campos';
      return;
    }
    
    isLoading = true;
    errorMessage = '';
    
    try {
      await auth.login({
        email,
        password,
        tenant_slug: 'system-admin',
        totp_code: totpCode || undefined
      });
      
      toast.success('¡Bienvenido! Has iniciado sesión correctamente');
      goto('/');
    } catch (error: any) {
      console.error('Login error:', error);
      
      // Check if 2FA is required
      if (error.message.includes('two-factor') || error.message.includes('2FA')) {
        showTwoFactor = true;
        errorMessage = 'Introduce el código de tu aplicación de autenticación';
      } else {
        errorMessage = error.message || 'Error al iniciar sesión';
        toast.error(errorMessage);
      }
    } finally {
      isLoading = false;
    }
  }
  
  function handleKeyDown(event: KeyboardEvent) {
    if (event.key === 'Enter') {
      handleLogin();
    }
  }
</script>


<svelte:head>
  <title>Acceso Admin - ServiceManager</title>
</svelte:head>

<div class="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-950 p-4 font-sans">
  <div class="w-full max-w-5xl grid grid-cols-1 md:grid-cols-2 bg-white dark:bg-gray-900 rounded-lg shadow-xl overflow-hidden border border-gray-200 dark:border-gray-800">
    
    <!-- Left Side: Internal Branding -->
    <div class="hidden md:flex flex-col justify-between p-12 bg-gray-900 text-white relative overflow-hidden">
      <!-- Grid pattern overlay -->
      <div class="absolute inset-0 opacity-10" style="background-image: radial-gradient(white 1px, transparent 1px); background-size: 30px 30px;"></div>
      
      <div class="relative z-10">
        <div class="flex items-center space-x-3 mb-6">
           <div class="p-2 bg-blue-500/20 rounded border border-blue-500/30">
             <Icon name="server" class="w-6 h-6 text-blue-400" />
           </div>
           <span class="text-sm font-mono tracking-wider text-blue-400">INTERNAL_ACCESS_V2</span>
        </div>
        
        <h1 class="text-3xl font-bold tracking-tight mb-4">
          Panel de Administración
        </h1>
        <p class="text-gray-400 text-sm leading-relaxed max-w-sm">
          Plataforma de gestión de servicios, monitoreo de tickets y administración de usuarios. Acceso restringido únicamente a personal autorizado.
        </p>
      </div>

      <div class="relative z-10 mt-12">
        <div class="space-y-3">
          <div class="flex items-center space-x-3 text-xs text-gray-400 font-mono">
             <Icon name="check-circle" class="w-4 h-4 text-green-500" />
             <span>System Status: Operational</span>
          </div>
          <div class="flex items-center space-x-3 text-xs text-gray-400 font-mono">
             <Icon name="shield" class="w-4 h-4 text-blue-500" />
             <span>256-bit Encryption Enabled</span>
          </div>
        </div>
      </div>
    </div>

    <!-- Right Side: Login Form -->
    <div class="p-8 md:p-12 flex flex-col justify-center">
       
       <div class="max-w-sm mx-auto w-full">
         <div class="mb-8">
           <h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-1">Identifíquese</h2>
           <p class="text-sm text-gray-500 dark:text-gray-400">Acceso al sistema central</p>
         </div>

         <form on:submit|preventDefault={handleLogin} class="space-y-5">
           {#if errorMessage}
             <div class="p-3 rounded-md bg-red-50 dark:bg-red-900/10 border border-red-200 dark:border-red-900 flex items-start gap-3">
               <Icon name="alert-triangle" class="w-5 h-5 text-red-600 dark:text-red-500 flex-shrink-0 mt-0.5" />
               <p class="text-sm text-red-600 dark:text-red-500">{errorMessage}</p>
             </div>
           {/if}

           {#if !showTwoFactor}
             <div class="space-y-4">
               <div>
                 <label for="email" class="block text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">Usuario / Correo</label>
                 <div class="relative group">
                   <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400 group-focus-within:text-blue-500 transition-colors">
                     <Icon name="user" class="w-5 h-5" />
                   </div>
                   <input
                     id="email"
                     type="email"
                     bind:value={email}
                     on:keydown={handleKeyDown}
                     class="form-input w-full pl-10 py-2.5 bg-gray-50 dark:bg-gray-800 border-gray-300 dark:border-gray-700 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all font-mono text-sm"
                     placeholder="admin@aduanasoft.com"
                     required
                     disabled={isLoading}
                   />
                 </div>
               </div>

               <div>
                 <label for="password" class="block text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">Clave de Acceso</label>
                 <div class="relative group">
                   <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400 group-focus-within:text-blue-500 transition-colors">
                     <Icon name="lock" class="w-5 h-5" />
                   </div>
                   <input
                     id="password"
                     type="password"
                     bind:value={password}
                     on:keydown={handleKeyDown}
                     class="form-input w-full pl-10 py-2.5 bg-gray-50 dark:bg-gray-800 border-gray-300 dark:border-gray-700 rounded focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all font-mono text-sm"
                     placeholder="••••••••••••"
                     required
                     disabled={isLoading}
                   />
                 </div>
               </div>
             </div>
           
           {:else}
             <!-- 2FA Input -->
             <div class="bg-blue-50 dark:bg-blue-900/10 p-4 rounded-lg border border-blue-100 dark:border-blue-800/30">
               <label for="code" class="block text-xs font-semibold uppercase tracking-wider text-blue-800 dark:text-blue-300 mb-2 text-center">Verificación de Seguridad</label>
               <div class="relative">
                 <input
                   id="code"
                   type="text"
                   bind:value={totpCode}
                   on:keydown={handleKeyDown}
                   class="form-input w-full py-3 rounded border-blue-300 dark:border-blue-700 focus:ring-blue-500 focus:border-blue-500 text-center tracking-[0.5em] font-mono text-lg bg-white dark:bg-gray-800"
                   placeholder="000000"
                   maxlength="6"
                   required
                   disabled={isLoading}
                   autofocus
                 />
               </div>
               <p class="text-xs text-blue-600 dark:text-blue-400 mt-2 text-center">
                 Consulte su dispositivo autenticador
               </p>
             </div>
           {/if}

           <div class="pt-4">
             <button
               type="submit"
               class="w-full flex justify-center py-2.5 px-4 rounded bg-gray-900 dark:bg-gray-700 text-white font-medium hover:bg-gray-800 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-900 transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
               disabled={isLoading}
             >
               {#if isLoading}
                 <Icon name="loader" class="w-4 h-4 animate-spin mr-2" />
                 Autenticando...
               {:else}
                 {showTwoFactor ? 'Verificar Token' : 'Entrar al Panel'}
               {/if}
             </button>
           </div>
         </form>
       </div>
       
       <div class="mt-8 pt-6 border-t border-gray-100 dark:border-gray-800">
          <p class="text-[10px] text-gray-400 text-center uppercase tracking-widest">Aduanasoft Internal Systems © 2024</p>
       </div>
    </div>
  </div>
</div>

==================================================
ARCHIVO: .\frontend-internal\src\routes\systems\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { api } from '$lib/utils/api';
  import { toast } from '$lib/stores/toast';
  import Modal from '$lib/components/Modal.svelte';

  let systems = [];
  let isLoading = false;
  let showModal = false;
  let editingSystem = null;

  let formData = {
    name: '',
    description: '',
    is_active: true
  };

  async function loadSystems() {
    isLoading = true;
    try {
      systems = await api.get('/systems/');
    } catch (e) {
      toast.error('Error cargando sistemas');
    } finally {
      isLoading = false;
    }
  }

  function openCreateModal() {
    editingSystem = null;
    formData = { name: '', description: '', is_active: true };
    showModal = true;
  }

  function openEditModal(system) {
    editingSystem = system;
    formData = { ...system };
    showModal = true;
  }

  async function handleSubmit() {
    try {
      if (editingSystem) {
        await api.put(`/systems/${editingSystem.id}`, formData);
        toast.success('Sistema actualizado');
      } else {
        await api.post('/systems/', formData);
        toast.success('Sistema creado');
      }
      showModal = false;
      loadSystems();
    } catch (e) {
      toast.error(e.message || 'Error guardando sistema');
    }
  }

  onMount(loadSystems);
</script>

<div class="px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
  <div class="sm:flex sm:items-center">
    <div class="sm:flex-auto">
      <h1 class="text-xl font-semibold text-gray-900">Sistemas</h1>
      <p class="mt-2 text-sm text-gray-700">Catálogo de sistemas informáticos gestionados.</p>
    </div>
    <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
      <button
        type="button"
        on:click={openCreateModal}
        class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 sm:w-auto"
      >
        Nuevo Sistema
      </button>
    </div>
  </div>

  <div class="mt-8 flex flex-col">
    <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
      <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
        <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
          <table class="min-w-full divide-y divide-gray-300">
            <thead class="bg-gray-50">
              <tr>
                <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Nombre</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Descripción</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Estado</th>
                <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
                  <span class="sr-only">Acciones</span>
                </th>
              </tr>
            </thead>
            <tbody class="divide-y divide-gray-200 bg-white">
              {#if isLoading}
                 <tr><td colspan="4" class="text-center py-4">Cargando...</td></tr>
              {:else if systems.length === 0}
                 <tr><td colspan="4" class="text-center py-4">No hay sistemas registrados</td></tr>
              {:else}
                {#each systems as system}
                  <tr>
                    <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{system.name}</td>
                    <td class="px-3 py-4 text-sm text-gray-500 max-w-xs truncate">{system.description || '-'}</td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
                      <span class:bg-green-100={system.is_active} class:text-green-800={system.is_active} class:bg-red-100={!system.is_active} class:text-red-800={!system.is_active} class="inline-flex rounded-full px-2 text-xs font-semibold leading-5">
                        {system.is_active ? 'Activo' : 'Inactivo'}
                      </span>
                    </td>
                    <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
                      <button on:click={() => openEditModal(system)} class="text-indigo-600 hover:text-indigo-900">Editar</button>
                    </td>
                  </tr>
                {/each}
              {/if}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</div>

<Modal open={showModal} title={editingSystem ? 'Editar Sistema' : 'Nuevo Sistema'} on:close={() => showModal = false}>
  <form on:submit|preventDefault={handleSubmit} class="space-y-4">
    <div>
      <label for="name" class="block text-sm font-medium text-gray-700">Nombre</label>
      <input type="text" id="name" bind:value={formData.name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
    </div>

    <div>
      <label for="description" class="block text-sm font-medium text-gray-700">Descripción</label>
      <textarea id="description" bind:value={formData.description} rows="3" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2"></textarea>
    </div>

    <div class="flex items-center">
      <input type="checkbox" id="is_active" bind:checked={formData.is_active} class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
      <label for="is_active" class="ml-2 block text-sm text-gray-900">Activo</label>
    </div>

    <div class="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
      <button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:col-start-2 sm:text-sm">
        Guardar
      </button>
      <button type="button" on:click={() => showModal = false} class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:col-start-1 sm:text-sm">
        Cancelar
      </button>
    </div>
  </form>
</Modal>


==================================================
ARCHIVO: .\frontend-internal\src\routes\tenants\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { api } from '$lib/utils/api';
  import { toast } from '$lib/stores/toast';
  import Modal from '$lib/components/Modal.svelte';
  import Icon from '$lib/components/Icon.svelte';

  let tenants = [];
  let isLoading = false;
  let showModal = false;
  let editingTenant = null;

  let formData = {
    name: '',
    slug: '',
    domain: '',
    is_active: true
  };

  async function loadTenants() {
    isLoading = true;
    try {
      tenants = await api.get('/tenants/');
    } catch (e) {
      toast.error('Error cargando clientes');
    } finally {
      isLoading = false;
    }
  }

  function openCreateModal() {
    editingTenant = null;
    formData = { name: '', slug: '', domain: '', is_active: true };
    showModal = true;
  }

  function openEditModal(tenant) {
    editingTenant = tenant;
    formData = { ...tenant };
    showModal = true;
  }

  async function handleSubmit() {
    try {
      if (editingTenant) {
        await api.put(`/tenants/${editingTenant.id}`, formData);
        toast.success('Cliente actualizado');
      } else {
        await api.post('/tenants/', formData);
        toast.success('Cliente creado');
      }
      showModal = false;
      loadTenants();
    } catch (e) {
      toast.error(e.message || 'Error guardando cliente');
    }
  }

  onMount(loadTenants);
</script>

<div class="px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
  <div class="sm:flex sm:items-center">
    <div class="sm:flex-auto">
      <h1 class="text-xl font-semibold text-gray-900">Clientes</h1>
      <p class="mt-2 text-sm text-gray-700">Lista de todas las organizaciones/clientes registrados en el sistema.</p>
    </div>
    <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
      <button
        type="button"
        on:click={openCreateModal}
        class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 sm:w-auto"
      >
        Nuevo Cliente
      </button>
    </div>
  </div>

  <div class="mt-8 flex flex-col">
    <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
      <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
        <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
          <table class="min-w-full divide-y divide-gray-300">
            <thead class="bg-gray-50">
              <tr>
                <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Nombre</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Slug</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Dominio</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Estado</th>
                <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
                  <span class="sr-only">Acciones</span>
                </th>
              </tr>
            </thead>
            <tbody class="divide-y divide-gray-200 bg-white">
              {#if isLoading}
                 <tr><td colspan="5" class="text-center py-4">Cargando...</td></tr>
              {:else if tenants.length === 0}
                 <tr><td colspan="5" class="text-center py-4">No hay clientes registrados</td></tr>
              {:else}
                {#each tenants as tenant}
                  <tr>
                    <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">{tenant.name}</td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{tenant.slug}</td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{tenant.domain || '-'}</td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
                      <span class:bg-green-100={tenant.is_active} class:text-green-800={tenant.is_active} class:bg-red-100={!tenant.is_active} class:text-red-800={!tenant.is_active} class="inline-flex rounded-full px-2 text-xs font-semibold leading-5">
                        {tenant.is_active ? 'Activo' : 'Inactivo'}
                      </span>
                    </td>
                    <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
                      <button on:click={() => openEditModal(tenant)} class="text-indigo-600 hover:text-indigo-900">Editar</button>
                    </td>
                  </tr>
                {/each}
              {/if}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</div>

<Modal open={showModal} title={editingTenant ? 'Editar Cliente' : 'Nuevo Cliente'} on:close={() => showModal = false}>
  <form on:submit|preventDefault={handleSubmit} class="space-y-4">
    <div>
      <label for="name" class="block text-sm font-medium text-gray-700">Nombre</label>
      <input type="text" id="name" bind:value={formData.name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
    </div>
    
    <div>
      <label for="slug" class="block text-sm font-medium text-gray-700">Slug (Identificador)</label>
      <input type="text" id="slug" bind:value={formData.slug} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
      <p class="text-xs text-gray-500 mt-1">Usado en URLs y subdominios.</p>
    </div>

    <div>
      <label for="domain" class="block text-sm font-medium text-gray-700">Dominio Personalizado</label>
      <input type="text" id="domain" bind:value={formData.domain} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
    </div>

    <div class="flex items-center">
      <input type="checkbox" id="is_active" bind:checked={formData.is_active} class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
      <label for="is_active" class="ml-2 block text-sm text-gray-900">Activo</label>
    </div>

    <div class="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
      <button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:col-start-2 sm:text-sm">
        Guardar
      </button>
      <button type="button" on:click={() => showModal = false} class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:col-start-1 sm:text-sm">
        Cancelar
      </button>
    </div>
  </form>
</Modal>


==================================================
ARCHIVO: .\frontend-internal\src\routes\users\+page.svelte
==================================================
<script lang="ts">
  import { onMount } from 'svelte';
  import { api } from '$lib/utils/api';
  import { toast } from '$lib/stores/toast';
  import Modal from '$lib/components/Modal.svelte';

  let users = [];
  let tenants = [];
  let isLoading = false;
  let showModal = false;
  let editingUser = null;

  let formData = {
    email: '',
    password: '',
    first_name: '',
    last_name: '',
    role: 'AGENT',
    tenant_id: '',
    is_active: true
  };

  const ROLES = [
    { value: 'ADMIN', label: 'Administrador (Global)' },
    { value: 'SUPPORT_MANAGER', label: 'Gerente de Soporte' },
    { value: 'AGENT', label: 'Agente de Soporte' },
    { value: 'AUDITOR', label: 'Auditor' },
    { value: 'CLIENT_ADMIN', label: 'Admin Cliente' },
    { value: 'CLIENT_USER', label: 'Usuario Cliente' }
  ];

  async function loadData() {
    isLoading = true;
    try {
      const [usersData, tenantsData] = await Promise.all([
        api.get('/users/'),
        api.get('/tenants/')
      ]);
      users = usersData;
      tenants = tenantsData;
    } catch (e) {
      toast.error('Error cargando datos');
    } finally {
      isLoading = false;
    }
  }

  function openCreateModal() {
    editingUser = null;
    formData = {
      email: '',
      password: '',
      first_name: '',
      last_name: '',
      role: 'AGENT',
      tenant_id: '',
      is_active: true
    };
    showModal = true;
  }

  function openEditModal(user) {
    editingUser = user;
    formData = {
      email: user.email,
      password: '', // Don't show password
      first_name: user.first_name,
      last_name: user.last_name,
      role: user.role,
      tenant_id: user.tenant_id || '',
      is_active: user.is_active
    };
    showModal = true;
  }

  async function handleSubmit() {
    try {
      const payload = { ...formData };
      if (!payload.password) delete payload.password; // Don't send empty password on edit
      if (!payload.tenant_id) payload.tenant_id = null; // Send null if empty string

      if (editingUser) {
        await api.put(`/users/${editingUser.id}`, payload);
        toast.success('Usuario actualizado');
      } else {
        if (!payload.password) {
            toast.error('La contraseña es requerida para nuevos usuarios');
            return;
        }
        await api.post('/users/', payload);
        toast.success('Usuario creado');
      }
      showModal = false;
      loadData();
    } catch (e) {
      toast.error(e.message || 'Error guardando usuario');
    }
  }

  function getTenantName(id) {
    if (!id) return '-';
    const t = tenants.find(t => t.id === id);
    return t ? t.name : id;
  }

  onMount(loadData);
</script>

<div class="px-4 py-8 mx-auto max-w-7xl sm:px-6 lg:px-8">
  <div class="sm:flex sm:items-center">
    <div class="sm:flex-auto">
      <h1 class="text-xl font-semibold text-gray-900">Usuarios</h1>
      <p class="mt-2 text-sm text-gray-700">Gestión de usuarios internos y de clientes.</p>
    </div>
    <div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
      <button
        type="button"
        on:click={openCreateModal}
        class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 sm:w-auto"
      >
        Nuevo Usuario
      </button>
    </div>
  </div>

  <div class="mt-8 flex flex-col">
    <div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
      <div class="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
        <div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
          <table class="min-w-full divide-y divide-gray-300">
            <thead class="bg-gray-50">
              <tr>
                <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6">Usuario</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Rol</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Cliente (Tenant)</th>
                <th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Estado</th>
                <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
                  <span class="sr-only">Acciones</span>
                </th>
              </tr>
            </thead>
            <tbody class="divide-y divide-gray-200 bg-white">
              {#if isLoading}
                 <tr><td colspan="5" class="text-center py-4">Cargando...</td></tr>
              {:else if users.length === 0}
                 <tr><td colspan="5" class="text-center py-4">No hay usuarios registrados</td></tr>
              {:else}
                {#each users as user}
                  <tr>
                    <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm sm:pl-6">
                      <div class="font-medium text-gray-900">{user.first_name} {user.last_name}</div>
                      <div class="text-gray-500">{user.email}</div>
                    </td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{user.role}</td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">{getTenantName(user.tenant_id)}</td>
                    <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
                      <span class:bg-green-100={user.is_active} class:text-green-800={user.is_active} class:bg-red-100={!user.is_active} class:text-red-800={!user.is_active} class="inline-flex rounded-full px-2 text-xs font-semibold leading-5">
                        {user.is_active ? 'Activo' : 'Inactivo'}
                      </span>
                    </td>
                    <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
                      <button on:click={() => openEditModal(user)} class="text-indigo-600 hover:text-indigo-900">Editar</button>
                    </td>
                  </tr>
                {/each}
              {/if}
            </tbody>
          </table>
        </div>
      </div>
    </div>
  </div>
</div>

<Modal open={showModal} title={editingUser ? 'Editar Usuario' : 'Nuevo Usuario'} on:close={() => showModal = false}>
  <form on:submit|preventDefault={handleSubmit} class="space-y-4">
    <div class="grid grid-cols-2 gap-4">
      <div>
        <label for="first_name" class="block text-sm font-medium text-gray-700">Nombre</label>
        <input type="text" id="first_name" bind:value={formData.first_name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
      </div>
      <div>
        <label for="last_name" class="block text-sm font-medium text-gray-700">Apellido</label>
        <input type="text" id="last_name" bind:value={formData.last_name} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
      </div>
    </div>

    <div>
      <label for="email" class="block text-sm font-medium text-gray-700">Email</label>
      <input type="email" id="email" bind:value={formData.email} required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
    </div>

    <div>
      <label for="password" class="block text-sm font-medium text-gray-700">Contraseña {editingUser ? '(dejar en blanco para mantener)' : ''}</label>
      <input type="password" id="password" bind:value={formData.password} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
    </div>

    <div>
      <label for="role" class="block text-sm font-medium text-gray-700">Rol</label>
      <select id="role" bind:value={formData.role} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
        {#each ROLES as role}
          <option value={role.value}>{role.label}</option>
        {/each}
      </select>
    </div>

    <div>
      <label for="tenant" class="block text-sm font-medium text-gray-700">Cliente (Opcional - solo para usuarios externos)</label>
      <select id="tenant" bind:value={formData.tenant_id} class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm border p-2">
        <option value="">-- Ninguno (Usuario Interno) --</option>
        {#each tenants as tenant}
          <option value={tenant.id}>{tenant.name}</option>
        {/each}
      </select>
    </div>

    <div class="flex items-center">
      <input type="checkbox" id="is_active" bind:checked={formData.is_active} class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
      <label for="is_active" class="ml-2 block text-sm text-gray-900">Activo</label>
    </div>

    <div class="mt-5 sm:mt-6 sm:grid sm:grid-cols-2 sm:gap-3 sm:grid-flow-row-dense">
      <button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:col-start-2 sm:text-sm">
        Guardar
      </button>
      <button type="button" on:click={() => showModal = false} class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:col-start-1 sm:text-sm">
        Cancelar
      </button>
    </div>
  </form>
</Modal>


==================================================
ARCHIVO: .\scripts\README.md
==================================================
# ServiceManagerWeb - Scripts de Utilidad

Este directorio contiene scripts para desarrollo y despliegue del sistema.

## Scripts de Desarrollo

### setup-dev.sh / setup-dev.ps1
```bash
# Linux/Mac
./scripts/setup-dev.sh

# Windows
./scripts/setup-dev.ps1
```

Configura el entorno de desarrollo:
- Copia .env.example a .env
- Genera claves secretas seguras
- Levanta servicios con Docker Compose
- Ejecuta migraciones iniciales
- Carga datos de prueba

### db-migrate.sh / db-migrate.ps1
```bash
# Ejecutar migraciones pendientes
./scripts/db-migrate.sh

# Crear nueva migración
./scripts/db-migrate.sh "add_user_preferences"
```

### test-all.sh / test-all.ps1
```bash
# Ejecutar todos los tests
./scripts/test-all.sh

# Solo backend
./scripts/test-all.sh backend

# Con coverage
./scripts/test-all.sh --coverage
```

### lint-fix.sh / lint-fix.ps1
```bash
# Linting y formateo automático
./scripts/lint-fix.sh
```

## Scripts de Producción

### deploy.sh
```bash
# Despliegue en producción
./scripts/deploy.sh production

# Con backup automático
./scripts/deploy.sh production --backup
```

### backup.sh
```bash
# Backup completo
./scripts/backup.sh

# Solo base de datos
./scripts/backup.sh --db-only
```

### health-check.sh
```bash
# Verificar estado de servicios
./scripts/health-check.sh
```

## Scripts de Mantenimiento

### cleanup.sh
```bash
# Limpiar logs antiguos y archivos temporales
./scripts/cleanup.sh

# Limpiar containers e imágenes sin usar
./scripts/cleanup.sh --docker
```

### seed-data.sh
```bash
# Cargar datos de prueba
./scripts/seed-data.sh

# Cargar datos específicos
./scripts/seed-data.sh --fixture=users
```

## Uso

Todos los scripts incluyen help integrado:

```bash
./scripts/script-name.sh --help
```

Los scripts están disponibles tanto para Unix (sh) como Windows (ps1).

==================================================
ARCHIVO: .\workers\README.md
==================================================
# ServiceManagerWeb Workers

Workers asíncronos con Celery para el sistema de Mesa de Ayuda B2B.

## Estructura

```
workers/
├── app/
│   ├── celery.py            # Configuración principal de Celery
│   ├── core/                # Configuración compartida
│   │   ├── config.py        # Settings para workers
│   │   └── logging.py       # Logging estructurado
│   └── tasks/               # Tareas por dominio
│       ├── email_tasks.py           # Envío de emails
│       ├── sla_tasks.py             # Monitoreo de SLAs
│       ├── maintenance_tasks.py     # Mantenimiento del sistema
│       └── notification_tasks.py    # Notificaciones y digests
├── requirements.txt         # Dependencias Python
└── README.md               # Esta documentación
```

## Tareas Implementadas

### Email Tasks (`email_tasks.py`)
- [x] `send_email_task`: Envío básico de emails SMTP
- [x] `send_templated_email_task`: Emails con plantillas Jinja2
- [x] `send_bulk_email_task`: Envío masivo con progreso

### SLA Tasks (`sla_tasks.py`)
- [x] `check_sla_violations`: Monitoreo de violaciones SLA
- [x] `calculate_sla_metrics`: Cálculo de métricas SLA
- [x] `send_sla_warnings`: Alertas de SLAs próximos a vencer

### Maintenance Tasks (`maintenance_tasks.py`)
- [x] `health_check`: Health check de workers
- [x] `cleanup_old_logs`: Limpieza de logs antiguos
- [x] `generate_weekly_reports`: Reportes semanales
- [x] `cleanup_temp_files`: Limpieza de archivos temporales
- [x] `database_maintenance`: Mantenimiento de BD

### Notification Tasks (`notification_tasks.py`)
- [x] `send_daily_digest`: Digest diario para agentes
- [x] `send_ticket_notifications`: Notificaciones de tickets
- [x] `send_system_alert`: Alertas del sistema

## Programación Automática (Celery Beat)

### Tareas Periódicas Configuradas

```python
# Cada 5 minutos
"check-sla-violations": check_sla_violations

# Diario a las 8:00 AM
"send-daily-digest": send_daily_digest  

# Semanal los domingos a las 2:00 AM
"cleanup-old-logs": cleanup_old_logs

# Semanal los lunes a las 9:00 AM  
"generate-weekly-reports": generate_weekly_reports

# Cada minuto (health check)
"worker-health-check": health_check
```

## Quick Start

### Desarrollo Local

```bash
# Instalar dependencias
pip install -r requirements.txt

# Variables de entorno (usar las del proyecto principal)
cp ../.env.example .env

# Ejecutar worker
celery -A app.celery worker --loglevel=info

# Ejecutar beat scheduler (en otra terminal)
celery -A app.celery beat --loglevel=info

# Monitoreo con Flower (opcional)
celery -A app.celery flower
```

### Con Docker

```bash
# Worker y Beat se ejecutan automáticamente con docker-compose
docker-compose up worker beat

# Ver logs
docker-compose logs -f worker
docker-compose logs -f beat
```

## Configuración

### Variables de Entorno Importantes

```bash
# Celery
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0

# Email
SMTP_HOST=mailhog
SMTP_PORT=1025
DEFAULT_FROM_EMAIL=noreply@servicemanager.local

# SLA
SLA_CHECK_ENABLED=true
SLA_WARNING_THRESHOLD=0.8

# Mantenimiento  
LOG_RETENTION_DAYS=30
DIGEST_ENABLED=true
```

### Colas de Trabajo

- **default**: Tareas generales
- **email**: Envío de emails
- **sla**: Monitoreo SLA
- **maintenance**: Mantenimiento
- **notifications**: Notificaciones

## Monitoreo

### Logs Estructurados

Todos los workers utilizan structured logging con:
- Task ID único
- Correlation ID para tracking
- Contexto de tenant
- Métricas de performance

### Health Checks

```bash
# Health check manual
celery -A app.celery inspect ping

# Estadísticas de workers
celery -A app.celery inspect stats

# Tareas activas
celery -A app.celery inspect active
```

### Métricas

- Task execution times
- Success/failure rates  
- Queue lengths
- Worker load

## Desarrollo

### Agregar Nueva Tarea

1. Crear función en módulo apropiado:
```python
@celery_app.task(bind=True, time_limit=300)
def new_task(self, param1: str, param2: int):
    logger = get_logger(__name__)
    # Implementation
    return result
```

2. Registrar en `celery.py` si es periódica:
```python
beat_schedule = {
    "new-periodic-task": {
        "task": "app.tasks.module.new_task",
        "schedule": crontab(minute=0, hour=9),
    }
}
```

3. Agregar tests en `tests/`

### Retry y Error Handling

```python
@celery_app.task(
    bind=True,
    autoretry_for=(ConnectionError, TimeoutError),
    retry_kwargs={'max_retries': 3, 'countdown': 60}
)
def reliable_task(self):
    # Task que se reintenta automáticamente
    pass
```

### Templates de Email

Los templates están definidos en código por ahora. En el futuro se moverán a base de datos para ser editables por tenants.

Templates disponibles:
- `ticket_created`
- `ticket_assigned` 
- `ticket_resolved`
- `sla_warning`
- `sla_violation`
- `daily_digest`
- `system_alert`

## Testing

```bash
# Ejecutar tests
pytest

# Tests específicos de workers
pytest tests/test_tasks/

# Ejecutar tarea manualmente para testing
celery -A app.celery call app.tasks.email_tasks.send_email_task --args='["test@example.com", "Test Subject", "<h1>Test</h1>"]'
```

## Producción

### Configuración Recomendada

```bash
# Múltiples workers por queue
celery -A app.celery worker --loglevel=info --concurrency=4 --queues=email
celery -A app.celery worker --loglevel=info --concurrency=2 --queues=sla,maintenance  
celery -A app.celery worker --loglevel=info --concurrency=1 --queues=default

# Beat scheduler (solo una instancia)
celery -A app.celery beat --loglevel=info

# Con systemd o supervisor para auto-restart
```

### Optimizaciones

- Pool de conexiones Redis
- Compresión de mensajes grandes
- Rate limiting por tarea
- Monitoring con Prometheus/Grafana

## Troubleshooting

### Problemas Comunes

1. **Tasks stuck in queue**: 
   - Verificar workers activos
   - Revisar configuración de routing

2. **Memory leaks**:
   - Configurar `worker_max_tasks_per_child`
   - Monitorear uso de memoria

3. **Email delivery failures**:
   - Verificar configuración SMTP
   - Revisar logs de tareas email

4. **SLA false positives**:
   - Verificar timezones
   - Validar lógica de cálculo

### Debug

```bash
# Ejecutar worker en modo debug
celery -A app.celery worker --loglevel=debug

# Inspeccionar tareas fallidas
celery -A app.celery inspect failed

# Purgar queue
celery -A app.celery purge -Q queue_name
```

==================================================
ARCHIVO: .\workers\app\celery.py
==================================================
"""
Celery Application - ServiceManagerWeb Workers

Configuración principal de Celery para tareas asíncronas
"""

from celery import Celery
from celery.schedules import crontab
import os
from app.core.config import get_settings
from app.core.logging import setup_logging

# Setup logging
setup_logging()

# Get settings
settings = get_settings()

# Create Celery application
celery_app = Celery(
    "servicemanager-workers",
    broker=settings.CELERY_BROKER_URL,
    backend=settings.CELERY_RESULT_BACKEND,
    include=[
        "app.tasks.email_tasks",
        "app.tasks.sla_tasks", 
        "app.tasks.maintenance_tasks",
        "app.tasks.notification_tasks"
    ]
)

# Configure Celery
celery_app.conf.update(
    # Task settings
    task_serializer="json",
    accept_content=["json"],
    result_serializer="json",
    timezone="UTC",
    enable_utc=True,
    
    # Result backend settings
    result_expires=3600,  # 1 hour
    result_persistent=True,
    
    # Worker settings
    worker_prefetch_multiplier=1,
    worker_max_tasks_per_child=1000,
    worker_disable_rate_limits=False,
    
    # Task routing
    task_routes={
        "app.tasks.email_tasks.*": {"queue": "email"},
        "app.tasks.sla_tasks.*": {"queue": "sla"},
        "app.tasks.maintenance_tasks.*": {"queue": "maintenance"},
        "app.tasks.notification_tasks.*": {"queue": "notifications"},
    },
    
    # Queue configuration
    task_default_queue="default",
    task_default_exchange="default",
    task_default_routing_key="default",
    
    # Beat schedule for periodic tasks
    beat_schedule={
        # Check SLA violations every 5 minutes
        "check-sla-violations": {
            "task": "app.tasks.sla_tasks.check_sla_violations",
            "schedule": crontab(minute="*/5"),
        },
        
        # Send daily digest at 8:00 AM
        "send-daily-digest": {
            "task": "app.tasks.notification_tasks.send_daily_digest",
            "schedule": crontab(hour=8, minute=0),
        },
        
        # Clean old logs weekly on Sunday at 2:00 AM
        "cleanup-old-logs": {
            "task": "app.tasks.maintenance_tasks.cleanup_old_logs",
            "schedule": crontab(hour=2, minute=0, day_of_week=0),
        },
        
        # Generate weekly reports on Monday at 9:00 AM
        "generate-weekly-reports": {
            "task": "app.tasks.maintenance_tasks.generate_weekly_reports",
            "schedule": crontab(hour=9, minute=0, day_of_week=1),
        },
        
        # Health check every minute
        "worker-health-check": {
            "task": "app.tasks.maintenance_tasks.health_check",
            "schedule": crontab(minute="*/1"),
        },
    },
    
    # Error handling
    task_reject_on_worker_lost=True,
    task_acks_late=True,
    
    # Monitoring
    worker_send_task_events=True,
    task_send_sent_event=True,
    
    # Security
    worker_hijack_root_logger=False,
    worker_log_format="[%(asctime)s: %(levelname)s/%(processName)s] %(message)s",
    worker_task_log_format="[%(asctime)s: %(levelname)s/%(processName)s][%(task_name)s(%(task_id)s)] %(message)s",
)

# Optional: Configure SSL if needed
if settings.ENVIRONMENT == "production":
    # Enable SSL for production
    celery_app.conf.update(
        broker_use_ssl=True,
        redis_backend_use_ssl=True,
    )


# Import all tasks to register them
from app.tasks import (
    email_tasks,
    sla_tasks,
    maintenance_tasks,
    notification_tasks
)


if __name__ == "__main__":
    celery_app.start()

==================================================
ARCHIVO: .\workers\app\core\config.py
==================================================
"""
Core Configuration for Workers - ServiceManagerWeb

Configuración compartida entre workers usando Pydantic Settings
"""

from functools import lru_cache
from typing import List, Optional
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
import os


class WorkerSettings(BaseSettings):
    """Configuración para workers Celery."""
    
    # ===================================
    # GENERAL
    # ===================================
    ENVIRONMENT: str = Field(default="development", env="ENVIRONMENT")
    DEBUG: bool = Field(default=False, env="DEBUG")
    
    # ===================================
    # DATABASE
    # ===================================
    DATABASE_URL: str = Field(..., env="DATABASE_URL")
    
    # ===================================
    # CELERY & REDIS
    # ===================================
    CELERY_BROKER_URL: str = Field(..., env="CELERY_BROKER_URL")
    CELERY_RESULT_BACKEND: str = Field(..., env="CELERY_RESULT_BACKEND")
    REDIS_URL: str = Field(..., env="REDIS_URL")
    
    # ===================================
    # EMAIL SETTINGS
    # ===================================
    SMTP_HOST: str = Field(default="localhost", env="SMTP_HOST")
    SMTP_PORT: int = Field(default=587, env="SMTP_PORT")
    SMTP_USER: Optional[str] = Field(default=None, env="SMTP_USER")
    SMTP_PASSWORD: Optional[str] = Field(default=None, env="SMTP_PASSWORD")
    SMTP_USE_TLS: bool = Field(default=True, env="SMTP_USE_TLS")
    SMTP_USE_SSL: bool = Field(default=False, env="SMTP_USE_SSL")
    
    DEFAULT_FROM_EMAIL: str = Field(default="noreply@servicemanager.local", env="DEFAULT_FROM_EMAIL")
    DEFAULT_FROM_NAME: str = Field(default="ServiceManager", env="DEFAULT_FROM_NAME")
    
    # Email retry settings
    EMAIL_MAX_RETRIES: int = Field(default=3, env="EMAIL_MAX_RETRIES")
    EMAIL_RETRY_DELAY: int = Field(default=60, env="EMAIL_RETRY_DELAY")  # seconds
    
    # ===================================
    # FILE PROCESSING
    # ===================================
    UPLOAD_PATH: str = Field(default="/app/uploads", env="UPLOAD_PATH")
    MAX_UPLOAD_SIZE_MB: int = Field(default=10, env="MAX_UPLOAD_SIZE_MB")
    
    # ===================================
    # SLA SETTINGS
    # ===================================
    SLA_CHECK_ENABLED: bool = Field(default=True, env="SLA_CHECK_ENABLED")
    SLA_WARNING_THRESHOLD: float = Field(default=0.8, env="SLA_WARNING_THRESHOLD")  # 80% of SLA time
    
    # ===================================
    # LOGGING
    # ===================================
    LOG_LEVEL: str = Field(default="INFO", env="LOG_LEVEL")
    LOG_FORMAT: str = Field(default="json", env="LOG_FORMAT")
    LOG_FILE: Optional[str] = Field(default=None, env="LOG_FILE")
    
    # ===================================
    # MONITORING
    # ===================================
    SENTRY_DSN: Optional[str] = Field(default=None, env="SENTRY_DSN")
    PROMETHEUS_PORT: int = Field(default=8888, env="PROMETHEUS_PORT")
    
    # ===================================
    # TASK SETTINGS
    # ===================================
    TASK_TIME_LIMIT: int = Field(default=300, env="TASK_TIME_LIMIT")  # 5 minutes
    TASK_SOFT_TIME_LIMIT: int = Field(default=240, env="TASK_SOFT_TIME_LIMIT")  # 4 minutes
    
    # ===================================
    # MAINTENANCE SETTINGS
    # ===================================
    LOG_RETENTION_DAYS: int = Field(default=30, env="LOG_RETENTION_DAYS")
    ATTACHMENT_RETENTION_DAYS: int = Field(default=90, env="ATTACHMENT_RETENTION_DAYS")
    
    # ===================================
    # NOTIFICATION SETTINGS
    # ===================================
    NOTIFICATION_BATCH_SIZE: int = Field(default=100, env="NOTIFICATION_BATCH_SIZE")
    DIGEST_ENABLED: bool = Field(default=True, env="DIGEST_ENABLED")
    
    model_config = {
        "env_file": ".env",
        "env_file_encoding": "utf-8",
        "case_sensitive": True
    }
    
    def is_production(self) -> bool:
        """Check if environment is production."""
        return self.ENVIRONMENT.lower() == "production"
    
    def is_development(self) -> bool:
        """Check if environment is development."""
        return self.ENVIRONMENT.lower() == "development"


@lru_cache()
def get_settings() -> WorkerSettings:
    """
    Get cached settings instance.
    
    Using lru_cache to create a singleton pattern for settings.
    """
    return WorkerSettings()

==================================================
ARCHIVO: .\workers\app\core\logging.py
==================================================
"""
Logging Configuration for Workers - ServiceManagerWeb

Configuración de logging estructurado para workers Celery
"""

import logging
import logging.config
import sys
from typing import Any, Dict
import structlog
from app.core.config import get_settings

settings = get_settings()


def setup_logging():
    """Configure structured logging for workers."""
    
    processors = [
        structlog.stdlib.filter_by_level,
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.UnicodeDecoder(),
        # Add worker-specific context
        structlog.processors.CallsiteParameterAdder(
            parameters=[
                structlog.processors.CallsiteParameter.FUNC_NAME,
                structlog.processors.CallsiteParameter.PATHNAME,
                structlog.processors.CallsiteParameter.LINENO,
            ]
        ),
    ]
    
    if settings.LOG_FORMAT == "json":
        processors.append(structlog.processors.JSONRenderer())
    else:
        processors.append(structlog.dev.ConsoleRenderer(colors=True))
    
    structlog.configure(
        processors=processors,
        wrapper_class=structlog.stdlib.BoundLogger,
        logger_factory=structlog.stdlib.LoggerFactory(),
        context_class=dict,
        cache_logger_on_first_use=True,
    )
    
    # Configure standard library logging for Celery
    logging_config = {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "json": {
                "()": structlog.stdlib.ProcessorFormatter,
                "processor": structlog.processors.JSONRenderer(),
            },
            "console": {
                "()": structlog.stdlib.ProcessorFormatter,
                "processor": structlog.dev.ConsoleRenderer(colors=True),
            },
        },
        "handlers": {
            "console": {
                "level": settings.LOG_LEVEL,
                "class": "logging.StreamHandler",
                "stream": sys.stdout,
                "formatter": "json" if settings.LOG_FORMAT == "json" else "console",
            },
        },
        "loggers": {
            "": {  # root logger
                "handlers": ["console"],
                "level": settings.LOG_LEVEL,
                "propagate": False,
            },
            "celery": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False,
            },
            "celery.worker": {
                "handlers": ["console"], 
                "level": "INFO",
                "propagate": False,
            },
            "celery.task": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False,
            },
            "app": {
                "handlers": ["console"],
                "level": settings.LOG_LEVEL,
                "propagate": False,
            },
        },
    }
    
    # Add file handler if specified
    if settings.LOG_FILE:
        logging_config["handlers"]["file"] = {
            "level": settings.LOG_LEVEL,
            "class": "logging.handlers.RotatingFileHandler",
            "filename": settings.LOG_FILE,
            "maxBytes": 10 * 1024 * 1024,  # 10MB
            "backupCount": 5,
            "formatter": "json",
        }
        
        for logger_config in logging_config["loggers"].values():
            logger_config["handlers"].append("file")
    
    logging.config.dictConfig(logging_config)


def get_logger(name: str = None) -> structlog.BoundLogger:
    """Get a configured structlog logger."""
    return structlog.get_logger(name)

==================================================
ARCHIVO: .\workers\app\tasks\email_tasks.py
==================================================
"""
Email Tasks - ServiceManagerWeb Workers

Tareas asíncronas para envío de emails y notificaciones
"""

from celery import current_task
from celery.exceptions import Retry
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email import encoders
import smtplib
import ssl
from typing import Dict, List, Optional, Any
from jinja2 import Template, Environment, BaseLoader
import structlog

from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger

settings = get_settings()
logger = get_logger(__name__)


class MemoryLoader(BaseLoader):
    """Jinja2 loader for templates from memory/database."""
    
    def __init__(self, templates: Dict[str, str]):
        self.templates = templates
    
    def get_source(self, environment, template):
        if template in self.templates:
            source = self.templates[template]
            return source, None, lambda: True
        raise TemplateNotFoundError(template)


@celery_app.task(
    bind=True,
    autoretry_for=(Exception,),
    retry_kwargs={'max_retries': 3, 'countdown': 60},
    time_limit=120,
    soft_time_limit=90
)
def send_email_task(
    self,
    to_email: str,
    subject: str,
    html_content: str,
    text_content: Optional[str] = None,
    from_email: Optional[str] = None,
    from_name: Optional[str] = None,
    attachments: Optional[List[Dict[str, Any]]] = None,
    tenant_id: Optional[str] = None,
    correlation_id: Optional[str] = None
) -> Dict[str, Any]:
    """
    Send email using SMTP.
    
    Args:
        to_email: Recipient email address
        subject: Email subject
        html_content: HTML content
        text_content: Plain text content (optional)
        from_email: Sender email (optional, uses default)
        from_name: Sender name (optional)
        attachments: List of attachment dicts
        tenant_id: Tenant ID for logging
        correlation_id: Correlation ID for tracking
        
    Returns:
        Dict with send result
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name,
        tenant_id=tenant_id,
        correlation_id=correlation_id
    )
    
    task_logger.info(
        "Starting email send task",
        to_email=to_email,
        subject=subject
    )
    
    try:
        # Prepare email
        msg = MIMEMultipart('alternative')
        msg['Subject'] = subject
        msg['From'] = f"{from_name or settings.DEFAULT_FROM_NAME} <{from_email or settings.DEFAULT_FROM_EMAIL}>"
        msg['To'] = to_email
        
        # Add text content
        if text_content:
            text_part = MIMEText(text_content, 'plain', 'utf-8')
            msg.attach(text_part)
        
        # Add HTML content
        html_part = MIMEText(html_content, 'html', 'utf-8')
        msg.attach(html_part)
        
        # Add attachments
        if attachments:
            for attachment in attachments:
                part = MIMEBase('application', 'octet-stream')
                part.set_payload(attachment['content'])
                encoders.encode_base64(part)
                part.add_header(
                    'Content-Disposition',
                    f'attachment; filename= {attachment["filename"]}'
                )
                msg.attach(part)
        
        # Send email
        context = ssl.create_default_context()
        
        with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server:
            if settings.SMTP_USE_TLS:
                server.starttls(context=context)
            
            if settings.SMTP_USER and settings.SMTP_PASSWORD:
                server.login(settings.SMTP_USER, settings.SMTP_PASSWORD)
            
            server.send_message(msg)
        
        task_logger.info(
            "Email sent successfully",
            to_email=to_email,
            subject=subject
        )
        
        return {
            "success": True,
            "to_email": to_email,
            "subject": subject,
            "sent_at": current_task.request.eta or "now"
        }
        
    except Exception as exc:
        task_logger.error(
            "Failed to send email",
            to_email=to_email,
            subject=subject,
            error=str(exc),
            exc_info=True
        )
        
        # Check if we should retry
        if self.request.retries < self.max_retries:
            task_logger.info(
                "Retrying email send",
                retry_count=self.request.retries + 1,
                max_retries=self.max_retries
            )
            raise self.retry(countdown=60 * (2 ** self.request.retries))
        
        return {
            "success": False,
            "to_email": to_email,
            "subject": subject,
            "error": str(exc)
        }


@celery_app.task(
    bind=True,
    time_limit=300,
    soft_time_limit=240
)
def send_templated_email_task(
    self,
    to_email: str,
    template_name: str,
    context: Dict[str, Any],
    tenant_id: Optional[str] = None,
    correlation_id: Optional[str] = None
) -> Dict[str, Any]:
    """
    Send email using a template.
    
    Args:
        to_email: Recipient email
        template_name: Template identifier
        context: Template context variables
        tenant_id: Tenant ID
        correlation_id: Correlation ID
        
    Returns:
        Dict with send result
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name,
        tenant_id=tenant_id,
        correlation_id=correlation_id
    )
    
    task_logger.info(
        "Starting templated email task",
        to_email=to_email,
        template_name=template_name
    )
    
    try:
        # TODO: Fetch template from database
        # For now, use mock templates
        templates = {
            "ticket_created": {
                "subject": "Nuevo ticket #{{ ticket_number }}: {{ subject }}",
                "html": """
                <h2>Nuevo ticket creado</h2>
                <p>Hola {{ user_name }},</p>
                <p>Se ha creado un nuevo ticket:</p>
                <ul>
                    <li><strong>Número:</strong> #{{ ticket_number }}</li>
                    <li><strong>Asunto:</strong> {{ subject }}</li>
                    <li><strong>Prioridad:</strong> {{ priority }}</li>
                </ul>
                <p><a href="{{ ticket_url }}">Ver ticket</a></p>
                <p>Saludos,<br>Equipo de Soporte</p>
                """,
                "text": """
                Nuevo ticket creado
                
                Hola {{ user_name }},
                
                Se ha creado un nuevo ticket:
                
                Número: #{{ ticket_number }}
                Asunto: {{ subject }}
                Prioridad: {{ priority }}
                
                Ver ticket: {{ ticket_url }}
                
                Saludos,
                Equipo de Soporte
                """
            },
            "ticket_assigned": {
                "subject": "Ticket #{{ ticket_number }} asignado a ti",
                "html": """
                <h2>Ticket asignado</h2>
                <p>Hola {{ agent_name }},</p>
                <p>Se te ha asignado el ticket:</p>
                <ul>
                    <li><strong>Número:</strong> #{{ ticket_number }}</li>
                    <li><strong>Asunto:</strong> {{ subject }}</li>
                    <li><strong>Cliente:</strong> {{ customer_name }}</li>
                    <li><strong>Prioridad:</strong> {{ priority }}</li>
                </ul>
                <p><a href="{{ ticket_url }}">Ver ticket</a></p>
                """,
                "text": """
                Ticket asignado
                
                Hola {{ agent_name }},
                
                Se te ha asignado el ticket:
                
                Número: #{{ ticket_number }}
                Asunto: {{ subject }}
                Cliente: {{ customer_name }}
                Prioridad: {{ priority }}
                
                Ver ticket: {{ ticket_url }}
                """
            }
        }
        
        if template_name not in templates:
            raise ValueError(f"Template '{template_name}' not found")
        
        template_data = templates[template_name]
        
        # Render templates
        env = Environment(loader=MemoryLoader({
            f"{template_name}_subject": template_data["subject"],
            f"{template_name}_html": template_data["html"],
            f"{template_name}_text": template_data["text"]
        }))
        
        subject_template = env.get_template(f"{template_name}_subject")
        html_template = env.get_template(f"{template_name}_html")
        text_template = env.get_template(f"{template_name}_text")
        
        subject = subject_template.render(**context)
        html_content = html_template.render(**context)
        text_content = text_template.render(**context)
        
        # Send email using the basic send task
        return send_email_task.apply_async(
            kwargs={
                "to_email": to_email,
                "subject": subject,
                "html_content": html_content,
                "text_content": text_content,
                "tenant_id": tenant_id,
                "correlation_id": correlation_id
            }
        ).get()
        
    except Exception as exc:
        task_logger.error(
            "Failed to send templated email",
            to_email=to_email,
            template_name=template_name,
            error=str(exc),
            exc_info=True
        )
        
        return {
            "success": False,
            "to_email": to_email,
            "template_name": template_name,
            "error": str(exc)
        }


@celery_app.task(
    bind=True,
    time_limit=600,
    soft_time_limit=540
)
def send_bulk_email_task(
    self,
    email_list: List[Dict[str, Any]],
    tenant_id: Optional[str] = None,
    correlation_id: Optional[str] = None
) -> Dict[str, Any]:
    """
    Send bulk emails.
    
    Args:
        email_list: List of email dicts with to_email, subject, content
        tenant_id: Tenant ID
        correlation_id: Correlation ID
        
    Returns:
        Dict with bulk send results
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name,
        tenant_id=tenant_id,
        correlation_id=correlation_id
    )
    
    total_emails = len(email_list)
    task_logger.info(f"Starting bulk email task", total_emails=total_emails)
    
    results = []
    
    for i, email_data in enumerate(email_list):
        try:
            result = send_email_task.apply_async(
                kwargs={
                    **email_data,
                    "tenant_id": tenant_id,
                    "correlation_id": correlation_id
                }
            ).get()
            
            results.append(result)
            
            # Update task progress
            current_task.update_state(
                state='PROGRESS',
                meta={'current': i + 1, 'total': total_emails}
            )
            
        except Exception as exc:
            task_logger.error(
                "Failed to send bulk email item",
                index=i,
                email_data=email_data,
                error=str(exc)
            )
            
            results.append({
                "success": False,
                "to_email": email_data.get("to_email"),
                "error": str(exc)
            })
    
    # Calculate stats
    successful = sum(1 for r in results if r.get("success"))
    failed = total_emails - successful
    
    task_logger.info(
        "Bulk email task completed",
        total=total_emails,
        successful=successful,
        failed=failed
    )
    
    return {
        "total": total_emails,
        "successful": successful,
        "failed": failed,
        "results": results
    }

==================================================
ARCHIVO: .\workers\app\tasks\maintenance_tasks.py
==================================================
"""
Maintenance Tasks - ServiceManagerWeb Workers

Tareas de mantenimiento del sistema
"""

from celery import current_task
from datetime import datetime, timedelta
from typing import Dict, Any, List
import os
import structlog

from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger

settings = get_settings()
logger = get_logger(__name__)


@celery_app.task(bind=True)
def health_check(self) -> Dict[str, Any]:
    """
    Worker health check task.
    
    Returns basic health information about the worker.
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name
    )
    
    try:
        current_time = datetime.utcnow()
        
        # Basic health checks
        health_data = {
            "status": "healthy",
            "timestamp": current_time.isoformat(),
            "worker_id": self.request.hostname,
            "task_id": self.request.id,
            "environment": settings.ENVIRONMENT,
            "checks": {
                "redis": "unknown",  # TODO: Check Redis connectivity
                "database": "unknown",  # TODO: Check database connectivity
                "disk_space": "unknown",  # TODO: Check disk space
                "memory": "unknown"  # TODO: Check memory usage
            }
        }
        
        task_logger.info("Worker health check completed", status="healthy")
        
        return health_data
        
    except Exception as exc:
        task_logger.error(
            "Worker health check failed",
            error=str(exc),
            exc_info=True
        )
        
        return {
            "status": "unhealthy",
            "timestamp": datetime.utcnow().isoformat(),
            "error": str(exc)
        }


@celery_app.task(
    bind=True,
    time_limit=1800,  # 30 minutes
    soft_time_limit=1500  # 25 minutes
)
def cleanup_old_logs(self) -> Dict[str, Any]:
    """
    Clean up old log files and database records.
    
    Removes:
    - Log files older than LOG_RETENTION_DAYS
    - Old notification logs
    - Old audit logs (if configured)
    - Temp files
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name
    )
    
    task_logger.info("Starting cleanup of old logs")
    
    try:
        current_time = datetime.utcnow()
        cutoff_date = current_time - timedelta(days=settings.LOG_RETENTION_DAYS)
        
        cleanup_results = {
            "started_at": current_time.isoformat(),
            "cutoff_date": cutoff_date.isoformat(),
            "files_removed": 0,
            "bytes_freed": 0,
            "database_records_removed": 0,
            "errors": []
        }
        
        # TODO: Implement actual file cleanup
        # For now, simulate cleanup
        
        # Clean up log files
        log_dir = "/app/logs"
        if os.path.exists(log_dir):
            for filename in os.listdir(log_dir):
                filepath = os.path.join(log_dir, filename)
                if os.path.isfile(filepath):
                    file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
                    if file_mtime < cutoff_date and filename.endswith('.log'):
                        try:
                            file_size = os.path.getsize(filepath)
                            os.remove(filepath)
                            cleanup_results["files_removed"] += 1
                            cleanup_results["bytes_freed"] += file_size
                            task_logger.info(f"Removed old log file", filename=filename)
                        except Exception as e:
                            cleanup_results["errors"].append(f"Failed to remove {filename}: {str(e)}")
        
        # TODO: Clean up database records
        # - Old notification_logs
        # - Old audit_logs (with retention policy)
        # - Expired refresh_tokens
        # - Old file attachments (if configured)
        
        task_logger.info(
            "Cleanup completed",
            files_removed=cleanup_results["files_removed"],
            bytes_freed=cleanup_results["bytes_freed"],
            errors=len(cleanup_results["errors"])
        )
        
        return cleanup_results
        
    except Exception as exc:
        task_logger.error(
            "Cleanup task failed",
            error=str(exc),
            exc_info=True
        )
        raise


@celery_app.task(
    bind=True,
    time_limit=3600,  # 1 hour
    soft_time_limit=3300  # 55 minutes
)
def generate_weekly_reports(self) -> Dict[str, Any]:
    """
    Generate weekly reports for all tenants.
    
    Creates:
    - SLA performance reports
    - Ticket volume reports
    - Agent performance reports
    - Customer satisfaction reports
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name
    )
    
    task_logger.info("Starting weekly reports generation")
    
    try:
        current_time = datetime.utcnow()
        week_start = current_time - timedelta(days=7)
        
        report_results = {
            "generated_at": current_time.isoformat(),
            "period_start": week_start.isoformat(),
            "period_end": current_time.isoformat(),
            "reports_generated": [],
            "errors": []
        }
        
        # TODO: Get list of active tenants from database
        mock_tenants = [
            {"id": "tenant-1", "name": "Aduanasoft Demo", "slug": "aduanasoft-demo"}
        ]
        
        for tenant in mock_tenants:
            try:
                task_logger.info(
                    "Generating report for tenant",
                    tenant_id=tenant["id"],
                    tenant_name=tenant["name"]
                )
                
                # TODO: Generate actual reports
                # For now, simulate report generation
                
                report_data = {
                    "tenant_id": tenant["id"],
                    "tenant_name": tenant["name"],
                    "period_start": week_start.isoformat(),
                    "period_end": current_time.isoformat(),
                    "metrics": {
                        "tickets_created": 25,
                        "tickets_resolved": 23,
                        "avg_response_time_hours": 2.1,
                        "avg_resolution_time_hours": 18.5,
                        "sla_response_met_percentage": 92.0,
                        "sla_resolution_met_percentage": 87.0,
                        "customer_satisfaction_avg": 4.2
                    }
                }
                
                report_results["reports_generated"].append(report_data)
                
                # TODO: Store report in database
                # TODO: Send report email to admins
                
                # Update task progress
                current_task.update_state(
                    state='PROGRESS',
                    meta={
                        'current': len(report_results["reports_generated"]),
                        'total': len(mock_tenants)
                    }
                )
                
            except Exception as e:
                error_msg = f"Failed to generate report for tenant {tenant['id']}: {str(e)}"
                report_results["errors"].append(error_msg)
                task_logger.error(
                    "Report generation failed for tenant",
                    tenant_id=tenant["id"],
                    error=str(e)
                )
        
        task_logger.info(
            "Weekly reports generation completed",
            reports_generated=len(report_results["reports_generated"]),
            errors=len(report_results["errors"])
        )
        
        return report_results
        
    except Exception as exc:
        task_logger.error(
            "Weekly reports generation failed",
            error=str(exc),
            exc_info=True
        )
        raise


@celery_app.task(
    bind=True,
    time_limit=900,  # 15 minutes
    soft_time_limit=780  # 13 minutes
)
def cleanup_temp_files(self) -> Dict[str, Any]:
    """
    Clean up temporary files and orphaned uploads.
    
    Removes:
    - Temp upload files older than 24 hours
    - Orphaned attachment files (no DB reference)
    - Processing artifacts
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name
    )
    
    task_logger.info("Starting temp files cleanup")
    
    try:
        current_time = datetime.utcnow()
        cutoff_date = current_time - timedelta(hours=24)
        
        cleanup_results = {
            "started_at": current_time.isoformat(),
            "temp_files_removed": 0,
            "orphaned_files_removed": 0,
            "bytes_freed": 0,
            "errors": []
        }
        
        # Clean up temp directory
        temp_dirs = ["/tmp", "/app/temp", f"{settings.UPLOAD_PATH}/temp"]
        
        for temp_dir in temp_dirs:
            if os.path.exists(temp_dir):
                for filename in os.listdir(temp_dir):
                    filepath = os.path.join(temp_dir, filename)
                    if os.path.isfile(filepath):
                        try:
                            file_mtime = datetime.fromtimestamp(os.path.getmtime(filepath))
                            if file_mtime < cutoff_date:
                                file_size = os.path.getsize(filepath)
                                os.remove(filepath)
                                cleanup_results["temp_files_removed"] += 1
                                cleanup_results["bytes_freed"] += file_size
                        except Exception as e:
                            cleanup_results["errors"].append(f"Failed to remove temp file {filepath}: {str(e)}")
        
        # TODO: Check for orphaned files in uploads directory
        # - Query database for all attachment file_paths
        # - Compare with actual files in upload directory
        # - Remove orphaned files
        
        task_logger.info(
            "Temp files cleanup completed",
            temp_files_removed=cleanup_results["temp_files_removed"],
            orphaned_files_removed=cleanup_results["orphaned_files_removed"],
            bytes_freed=cleanup_results["bytes_freed"]
        )
        
        return cleanup_results
        
    except Exception as exc:
        task_logger.error(
            "Temp files cleanup failed",
            error=str(exc),
            exc_info=True
        )
        raise


@celery_app.task(
    bind=True,
    time_limit=300,
    soft_time_limit=240
)
def database_maintenance(self) -> Dict[str, Any]:
    """
    Perform database maintenance tasks.
    
    - VACUUM and ANALYZE tables
    - Update statistics
    - Check for slow queries
    - Optimize indices if needed
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name
    )
    
    task_logger.info("Starting database maintenance")
    
    try:
        # TODO: Implement database maintenance
        # For now, return placeholder results
        
        maintenance_results = {
            "started_at": datetime.utcnow().isoformat(),
            "tables_analyzed": 0,
            "indices_optimized": 0,
            "slow_queries_found": 0,
            "space_reclaimed_mb": 0
        }
        
        task_logger.info("Database maintenance completed (placeholder)")
        
        return maintenance_results
        
    except Exception as exc:
        task_logger.error(
            "Database maintenance failed",
            error=str(exc),
            exc_info=True
        )
        raise

==================================================
ARCHIVO: .\workers\app\tasks\notification_tasks.py
==================================================
"""
Notification Tasks - ServiceManagerWeb Workers

Tareas para notificaciones y comunicaciones
"""

from celery import current_task
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional
import structlog

from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
from app.tasks.email_tasks import send_templated_email_task, send_bulk_email_task

settings = get_settings()
logger = get_logger(__name__)


@celery_app.task(
    bind=True,
    time_limit=900,  # 15 minutes
    soft_time_limit=780  # 13 minutes
)
def send_daily_digest(self) -> Dict[str, Any]:
    """
    Send daily digest emails to agents and managers.
    
    Includes:
    - New tickets assigned
    - SLA warnings
    - Performance summary
    - Pending tasks
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name
    )
    
    task_logger.info("Starting daily digest generation")
    
    if not settings.DIGEST_ENABLED:
        task_logger.info("Daily digest disabled, skipping")
        return {"status": "disabled"}
    
    try:
        current_time = datetime.utcnow()
        yesterday = current_time - timedelta(days=1)
        
        digest_results = {
            "generated_at": current_time.isoformat(),
            "period_start": yesterday.isoformat(),
            "period_end": current_time.isoformat(),
            "digests_sent": 0,
            "errors": []
        }
        
        # TODO: Get active agents and managers from database
        mock_recipients = [
            {
                "user_id": "user-1",
                "email": "agent1@example.com",
                "name": "Agent One",
                "role": "AGENT",
                "tenant_id": "tenant-1"
            },
            {
                "user_id": "user-2", 
                "email": "manager@example.com",
                "name": "Support Manager",
                "role": "SUPPORT_MANAGER",
                "tenant_id": "tenant-1"
            }
        ]
        
        for recipient in mock_recipients:
            try:
                task_logger.info(
                    "Generating digest for user",
                    user_id=recipient["user_id"],
                    email=recipient["email"],
                    role=recipient["role"]
                )
                
                # TODO: Generate actual digest data from database
                digest_data = generate_digest_data(
                    recipient["user_id"],
                    recipient["role"],
                    recipient["tenant_id"],
                    yesterday,
                    current_time
                )
                
                # Send digest email
                send_templated_email_task.apply_async(kwargs={
                    "to_email": recipient["email"],
                    "template_name": "daily_digest",
                    "context": {
                        "user_name": recipient["name"],
                        "role": recipient["role"],
                        "date": current_time.strftime("%Y-%m-%d"),
                        **digest_data
                    },
                    "tenant_id": recipient["tenant_id"],
                    "correlation_id": self.request.id
                })
                
                digest_results["digests_sent"] += 1
                
            except Exception as e:
                error_msg = f"Failed to send digest to {recipient['email']}: {str(e)}"
                digest_results["errors"].append(error_msg)
                task_logger.error(
                    "Digest generation failed for user",
                    user_id=recipient["user_id"],
                    error=str(e)
                )
        
        task_logger.info(
            "Daily digest generation completed",
            digests_sent=digest_results["digests_sent"],
            errors=len(digest_results["errors"])
        )
        
        return digest_results
        
    except Exception as exc:
        task_logger.error(
            "Daily digest generation failed",
            error=str(exc),
            exc_info=True
        )
        raise


def generate_digest_data(
    user_id: str,
    role: str,
    tenant_id: str,
    period_start: datetime,
    period_end: datetime
) -> Dict[str, Any]:
    """
    Generate digest data for a specific user.
    
    Args:
        user_id: User ID
        role: User role
        tenant_id: Tenant ID
        period_start: Start of digest period
        period_end: End of digest period
        
    Returns:
        Dict with digest data
    """
    # TODO: Implement actual database queries
    # For now, return mock data
    
    base_data = {
        "summary": {
            "new_tickets": 5,
            "resolved_tickets": 7,
            "pending_tickets": 12,
            "overdue_tickets": 2
        },
        "sla_status": {
            "response_sla_met": 8,
            "response_sla_missed": 1,
            "resolution_sla_met": 6,
            "resolution_sla_missed": 2
        }
    }
    
    if role == "AGENT":
        base_data.update({
            "assigned_tickets": [
                {
                    "ticket_number": "TKT-2024-000001",
                    "subject": "Problema de conexión",
                    "priority": "HIGH",
                    "created_at": "2024-01-15T10:00:00Z",
                    "sla_due": "2024-01-15T12:00:00Z"
                }
            ],
            "urgent_tickets": 1,
            "performance": {
                "avg_response_time_hours": 1.5,
                "avg_resolution_time_hours": 18.2,
                "customer_satisfaction": 4.3
            }
        })
    
    elif role in ["SUPPORT_MANAGER", "ADMIN"]:
        base_data.update({
            "team_summary": {
                "total_agents": 5,
                "active_agents": 4,
                "avg_load_per_agent": 6.2
            },
            "escalations": [
                {
                    "ticket_number": "TKT-2024-000002",
                    "reason": "SLA violation",
                    "assigned_to": "agent1@example.com"
                }
            ],
            "trends": {
                "ticket_volume_change": "+12%",
                "resolution_time_change": "-5%"
            }
        })
    
    return base_data


@celery_app.task(
    bind=True,
    time_limit=600,
    soft_time_limit=540
)
def send_ticket_notifications(
    self,
    ticket_id: str,
    event_type: str,
    tenant_id: str,
    context: Dict[str, Any],
    correlation_id: Optional[str] = None
) -> Dict[str, Any]:
    """
    Send ticket-related notifications.
    
    Args:
        ticket_id: Ticket ID
        event_type: Type of event (created, assigned, updated, resolved, etc.)
        tenant_id: Tenant ID
        context: Context data for notifications
        correlation_id: Correlation ID
        
    Returns:
        Dict with notification results
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name,
        ticket_id=ticket_id,
        event_type=event_type,
        tenant_id=tenant_id,
        correlation_id=correlation_id
    )
    
    task_logger.info("Starting ticket notifications")
    
    try:
        notification_results = {
            "ticket_id": ticket_id,
            "event_type": event_type,
            "notifications_sent": 0,
            "notifications": []
        }
        
        # Determine who should receive notifications based on event type
        recipients = get_notification_recipients(ticket_id, event_type, tenant_id)
        
        for recipient in recipients:
            try:
                template_name = f"ticket_{event_type}"
                
                # Send notification
                result = send_templated_email_task.apply_async(kwargs={
                    "to_email": recipient["email"],
                    "template_name": template_name,
                    "context": {
                        **context,
                        "recipient_name": recipient["name"],
                        "recipient_role": recipient["role"]
                    },
                    "tenant_id": tenant_id,
                    "correlation_id": correlation_id or self.request.id
                }).get()
                
                notification_results["notifications"].append({
                    "recipient": recipient["email"],
                    "template": template_name,
                    "success": result.get("success", False),
                    "error": result.get("error")
                })
                
                if result.get("success"):
                    notification_results["notifications_sent"] += 1
                
            except Exception as e:
                task_logger.error(
                    "Failed to send notification",
                    recipient_email=recipient["email"],
                    error=str(e)
                )
                
                notification_results["notifications"].append({
                    "recipient": recipient["email"],
                    "success": False,
                    "error": str(e)
                })
        
        task_logger.info(
            "Ticket notifications completed",
            notifications_sent=notification_results["notifications_sent"],
            total_recipients=len(recipients)
        )
        
        return notification_results
        
    except Exception as exc:
        task_logger.error(
            "Ticket notifications failed",
            error=str(exc),
            exc_info=True
        )
        raise


def get_notification_recipients(
    ticket_id: str,
    event_type: str,
    tenant_id: str
) -> List[Dict[str, Any]]:
    """
    Get list of users who should receive notifications for a ticket event.
    
    Args:
        ticket_id: Ticket ID
        event_type: Event type
        tenant_id: Tenant ID
        
    Returns:
        List of recipient dicts
    """
    # TODO: Implement actual database queries
    # For now, return mock recipients based on event type
    
    recipients = []
    
    if event_type == "created":
        # Notify assigned agent (if any) and customer
        recipients = [
            {"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"},
            {"email": "agent@example.com", "name": "Agent", "role": "AGENT"}
        ]
    
    elif event_type == "assigned":
        # Notify assigned agent and customer
        recipients = [
            {"email": "agent@example.com", "name": "Assigned Agent", "role": "AGENT"},
            {"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"}
        ]
    
    elif event_type == "updated":
        # Notify all participants
        recipients = [
            {"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"},
            {"email": "agent@example.com", "name": "Agent", "role": "AGENT"}
        ]
    
    elif event_type == "resolved":
        # Notify customer for feedback
        recipients = [
            {"email": "customer@example.com", "name": "Customer", "role": "CLIENT_USER"}
        ]
    
    elif event_type == "escalated":
        # Notify manager
        recipients = [
            {"email": "manager@example.com", "name": "Manager", "role": "SUPPORT_MANAGER"}
        ]
    
    return recipients


@celery_app.task(
    bind=True,
    time_limit=300,
    soft_time_limit=240
)
def send_system_alert(
    self,
    alert_type: str,
    message: str,
    severity: str = "INFO",
    tenant_id: Optional[str] = None,
    context: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    """
    Send system alert to administrators.
    
    Args:
        alert_type: Type of alert (system_error, sla_violation, etc.)
        message: Alert message
        severity: Alert severity (INFO, WARNING, ERROR, CRITICAL)
        tenant_id: Optional tenant ID
        context: Additional context data
        
    Returns:
        Dict with alert results
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name,
        alert_type=alert_type,
        severity=severity,
        tenant_id=tenant_id
    )
    
    task_logger.info("Sending system alert", message=message)
    
    try:
        # TODO: Get administrators from configuration/database
        admin_emails = ["admin@example.com", "alerts@example.com"]
        
        alert_context = {
            "alert_type": alert_type,
            "message": message,
            "severity": severity,
            "timestamp": datetime.utcnow().isoformat(),
            "environment": settings.ENVIRONMENT,
            "tenant_id": tenant_id,
            **(context or {})
        }
        
        notifications_sent = 0
        
        for admin_email in admin_emails:
            try:
                send_templated_email_task.apply_async(kwargs={
                    "to_email": admin_email,
                    "template_name": "system_alert",
                    "context": alert_context,
                    "tenant_id": tenant_id,
                    "correlation_id": self.request.id
                })
                notifications_sent += 1
                
            except Exception as e:
                task_logger.error(
                    "Failed to send alert to admin",
                    admin_email=admin_email,
                    error=str(e)
                )
        
        task_logger.info(
            "System alert sent",
            notifications_sent=notifications_sent,
            total_admins=len(admin_emails)
        )
        
        return {
            "alert_type": alert_type,
            "message": message,
            "severity": severity,
            "notifications_sent": notifications_sent,
            "sent_at": datetime.utcnow().isoformat()
        }
        
    except Exception as exc:
        task_logger.error(
            "System alert failed",
            error=str(exc),
            exc_info=True
        )
        raise

==================================================
ARCHIVO: .\workers\app\tasks\sla_tasks.py
==================================================
"""
SLA Tasks - ServiceManagerWeb Workers

Tareas para monitoreo y gestión de SLAs
"""

from celery import current_task
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import structlog

from app.celery import celery_app
from app.core.config import get_settings
from app.core.logging import get_logger
from app.tasks.email_tasks import send_templated_email_task

settings = get_settings()
logger = get_logger(__name__)


@celery_app.task(
    bind=True,
    time_limit=300,
    soft_time_limit=240
)
def check_sla_violations(self) -> Dict[str, Any]:
    """
    Check for SLA violations and send alerts.
    
    This task runs every 5 minutes to check for:
    - Response SLA violations
    - Resolution SLA violations  
    - SLA warnings (approaching deadline)
    
    Returns:
        Dict with check results
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name
    )
    
    task_logger.info("Starting SLA violations check")
    
    if not settings.SLA_CHECK_ENABLED:
        task_logger.info("SLA check disabled, skipping")
        return {"status": "disabled"}
    
    try:
        current_time = datetime.utcnow()
        results = {
            "checked_at": current_time.isoformat(),
            "response_violations": [],
            "resolution_violations": [],
            "warnings": [],
            "notifications_sent": 0
        }
        
        # TODO: Implement actual database queries
        # For now, simulate some checks
        
        # Mock violations for development
        mock_violations = [
            {
                "ticket_id": "mock-ticket-1",
                "ticket_number": "TKT-2024-000001",
                "subject": "Problema urgente de conexión",
                "priority": "HIGH",
                "sla_type": "response",
                "due_at": (current_time - timedelta(minutes=30)).isoformat(),
                "assigned_to_email": "agent@example.com",
                "created_by_email": "cliente@example.com",
                "tenant_id": "mock-tenant-1"
            }
        ]
        
        # Process violations
        for violation in mock_violations:
            task_logger.info(
                "Processing SLA violation",
                ticket_id=violation["ticket_id"],
                sla_type=violation["sla_type"]
            )
            
            if violation["sla_type"] == "response":
                results["response_violations"].append(violation)
                
                # Send notification to assigned agent
                if violation["assigned_to_email"]:
                    send_templated_email_task.apply_async(kwargs={
                        "to_email": violation["assigned_to_email"],
                        "template_name": "sla_response_violation",
                        "context": {
                            "ticket_number": violation["ticket_number"],
                            "subject": violation["subject"],
                            "priority": violation["priority"],
                            "due_at": violation["due_at"],
                            "ticket_url": f"https://admin.servicemanager.local/tickets/{violation['ticket_id']}"
                        },
                        "tenant_id": violation["tenant_id"],
                        "correlation_id": self.request.id
                    })
                    results["notifications_sent"] += 1
            
            elif violation["sla_type"] == "resolution":
                results["resolution_violations"].append(violation)
                
                # Send escalation notification
                send_templated_email_task.apply_async(kwargs={
                    "to_email": "manager@example.com",  # TODO: Get from tenant config
                    "template_name": "sla_resolution_violation",
                    "context": {
                        "ticket_number": violation["ticket_number"],
                        "subject": violation["subject"],
                        "priority": violation["priority"],
                        "assigned_to": violation["assigned_to_email"],
                        "ticket_url": f"https://admin.servicemanager.local/tickets/{violation['ticket_id']}"
                    },
                    "tenant_id": violation["tenant_id"],
                    "correlation_id": self.request.id
                })
                results["notifications_sent"] += 1
        
        # TODO: Check for SLA warnings (approaching deadline)
        
        task_logger.info(
            "SLA violations check completed",
            response_violations=len(results["response_violations"]),
            resolution_violations=len(results["resolution_violations"]),
            warnings=len(results["warnings"]),
            notifications_sent=results["notifications_sent"]
        )
        
        return results
        
    except Exception as exc:
        task_logger.error(
            "SLA violations check failed",
            error=str(exc),
            exc_info=True
        )
        raise


@celery_app.task(
    bind=True,
    time_limit=600,
    soft_time_limit=540
)
def calculate_sla_metrics(self, tenant_id: str, date_from: str, date_to: str) -> Dict[str, Any]:
    """
    Calculate SLA metrics for a tenant and date range.
    
    Args:
        tenant_id: Tenant ID
        date_from: Start date (ISO format)
        date_to: End date (ISO format)
        
    Returns:
        Dict with SLA metrics
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name,
        tenant_id=tenant_id
    )
    
    task_logger.info(
        "Starting SLA metrics calculation",
        date_from=date_from,
        date_to=date_to
    )
    
    try:
        # TODO: Implement actual database queries
        # For now, return mock metrics
        
        metrics = {
            "tenant_id": tenant_id,
            "date_from": date_from,
            "date_to": date_to,
            "calculated_at": datetime.utcnow().isoformat(),
            "response_sla": {
                "target_hours": 2,
                "met_count": 45,
                "total_count": 50,
                "percentage": 90.0,
                "avg_response_time_hours": 1.8
            },
            "resolution_sla": {
                "target_hours": 24,
                "met_count": 42,
                "total_count": 48,
                "percentage": 87.5,
                "avg_resolution_time_hours": 22.5
            },
            "by_priority": {
                "LOW": {
                    "response_sla_percentage": 95.0,
                    "resolution_sla_percentage": 90.0
                },
                "MEDIUM": {
                    "response_sla_percentage": 88.0,
                    "resolution_sla_percentage": 85.0
                },
                "HIGH": {
                    "response_sla_percentage": 92.0,
                    "resolution_sla_percentage": 88.0
                },
                "URGENT": {
                    "response_sla_percentage": 85.0,
                    "resolution_sla_percentage": 80.0
                }
            },
            "trends": {
                "response_sla_trend": "+2.5%",
                "resolution_sla_trend": "-1.2%"
            }
        }
        
        task_logger.info(
            "SLA metrics calculation completed",
            response_sla_percentage=metrics["response_sla"]["percentage"],
            resolution_sla_percentage=metrics["resolution_sla"]["percentage"]
        )
        
        return metrics
        
    except Exception as exc:
        task_logger.error(
            "SLA metrics calculation failed",
            error=str(exc),
            exc_info=True
        )
        raise


@celery_app.task(
    bind=True,
    time_limit=300,
    soft_time_limit=240
)
def send_sla_warnings(self, tenant_id: Optional[str] = None) -> Dict[str, Any]:
    """
    Send SLA warning notifications for tickets approaching deadline.
    
    Args:
        tenant_id: Optional tenant ID to filter by
        
    Returns:
        Dict with warning results
    """
    task_logger = logger.bind(
        task_id=self.request.id,
        task_name=self.name,
        tenant_id=tenant_id
    )
    
    task_logger.info("Starting SLA warnings check")
    
    try:
        current_time = datetime.utcnow()
        warning_threshold = settings.SLA_WARNING_THRESHOLD  # 80% of SLA time
        
        # TODO: Query database for tickets approaching SLA deadlines
        
        # Mock warnings
        warnings = [
            {
                "ticket_id": "mock-ticket-2",
                "ticket_number": "TKT-2024-000002",
                "subject": "Consulta técnica",
                "priority": "MEDIUM",
                "sla_type": "response",
                "due_at": (current_time + timedelta(minutes=30)).isoformat(),
                "time_remaining_percent": 15.0,
                "assigned_to_email": "agent@example.com",
                "tenant_id": "mock-tenant-1"
            }
        ]
        
        notifications_sent = 0
        
        for warning in warnings:
            if warning["time_remaining_percent"] <= (1 - warning_threshold) * 100:
                task_logger.info(
                    "Sending SLA warning",
                    ticket_id=warning["ticket_id"],
                    time_remaining_percent=warning["time_remaining_percent"]
                )
                
                send_templated_email_task.apply_async(kwargs={
                    "to_email": warning["assigned_to_email"],
                    "template_name": "sla_warning",
                    "context": {
                        "ticket_number": warning["ticket_number"],
                        "subject": warning["subject"],
                        "priority": warning["priority"],
                        "sla_type": warning["sla_type"],
                        "due_at": warning["due_at"],
                        "time_remaining_percent": warning["time_remaining_percent"],
                        "ticket_url": f"https://admin.servicemanager.local/tickets/{warning['ticket_id']}"
                    },
                    "tenant_id": warning["tenant_id"],
                    "correlation_id": self.request.id
                })
                notifications_sent += 1
        
        task_logger.info(
            "SLA warnings check completed",
            warnings_found=len(warnings),
            notifications_sent=notifications_sent
        )
        
        return {
            "warnings_found": len(warnings),
            "notifications_sent": notifications_sent,
            "warnings": warnings
        }
        
    except Exception as exc:
        task_logger.error(
            "SLA warnings check failed",
            error=str(exc),
            exc_info=True
        )
        raise

==================================================
ARCHIVO: .\Zpracticante\Backups\respaldo_docker_v1.sql
==================================================
p g _ d u m p :   e r r o r :   c o n n e c t i o n   t o   s e r v e r   o n   s o c k e t   " / v a r / r u n / p o s t g r e s q l / . s . P G S Q L . 5 4 3 2 "   f a i l e d :   F A T A L :     r o l e   " p o s t g r e s "   d o e s   n o t   e x i s t 
 
 
