
# Taller: Desarrollo con Angular e Ionic

## Capítulo 1: Introducción a Angular e Ionic

## 1. ¿Qué es Angular?

Angular es un framework de desarrollo front-end mantenido por Google, diseñado para crear aplicaciones web de una sola página (SPA) y aplicaciones móviles. Algunas características clave:

- **Basado en componentes**: Toda la interfaz se construye mediante componentes reutilizables
- **TypeScript**: Utiliza TypeScript como lenguaje principal, añadiendo tipos estáticos a JavaScript
- **Completo**: Ofrece soluciones integradas para enrutamiento, formularios, HTTP, animaciones, etc.
- **Modular**: Su arquitectura permite dividir la aplicación en módulos funcionales
- **Reactivo**: Facilita la programación reactiva mediante RxJS

Angular utiliza un sistema de "detección de cambios" para mantener sincronizada la interfaz de usuario con el estado de la aplicación, lo que permite crear interfaces dinámicas y reactivas.

## 2. Angular CLI: La Herramienta de Línea de Comandos

El Angular CLI (Command Line Interface) es una herramienta oficial que simplifica enormemente el desarrollo con Angular:

```bash
# Instalación global
npm install -g @angular/cli

# Crear nuevo proyecto
ng new mi-proyecto

# Generar componentes, servicios, etc.
ng generate component mi-componente
ng generate service mi-servicio

# Iniciar servidor de desarrollo
ng serve

# Construir para producción
ng build --prod
```

Beneficios del CLI:

- **Scaffolding**: Generación de código con estructura y configuración correctas
- **Herramientas de desarrollo**: Servidor local con recarga en vivo (live reload)
- **Optimización**: Empaquetado y minificación para producción
- **Testing**: Configuración automática para pruebas unitarias e integración
- **Actualización**: Facilita la actualización entre versiones de Angular

## 3. Angular vs Desarrollo Web Tradicional

Angular se asemeja al desarrollo de páginas web normales, pero con una estructura y enfoque diferentes:

| Desarrollo Web Tradicional | Desarrollo con Angular |
|---------------------------|------------------------|
| Páginas HTML separadas | Aplicación de una sola página (SPA) |
| jQuery para manipular DOM | Vinculación de datos bidireccional |
| JavaScript vanilla | TypeScript con tipos estáticos |
| Recarga completa entre páginas | Navegación sin recarga (enrutamiento SPA) |
| Mezcla de lógica y presentación | Separación modelo-vista-controlador |
| Scripts y estilos globales | Encapsulación de componentes |

A pesar de las diferencias, los conocimientos de HTML, CSS y JavaScript son totalmente aplicables en Angular, ya que seguimos trabajando con estos lenguajes fundamentales pero de manera estructurada.

## 4. Componentes Básicos de Angular

#### Componentes

El elemento fundamental de las aplicaciones Angular:

```typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-contador',
  template: `
    <div>
      <h2>Contador: {{ contador }}</h2>
      <button (click)="incrementar()">+</button>
      <button (click)="decrementar()">-</button>
    </div>
  `,
  styles: [`
    div { text-align: center; }
    button { margin: 0 5px; }
  `]
})
export class ContadorComponent {
  contador = 0;
  
  incrementar() {
    this.contador++;
  }
  
  decrementar() {
    this.contador--;
  }
}
```

#### Módulos

Organizan la aplicación en bloques funcionales:

```typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { ContadorComponent } from './contador/contador.component';

@NgModule({
  declarations: [
    AppComponent,
    ContadorComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
```

#### Servicios

Encapsulan la lógica de negocio y son inyectables en componentes:

```typescript
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class DatosService {
  private datos = ['Primer elemento', 'Segundo elemento'];
  
  getDatos() {
    return this.datos;
  }
  
  agregarDato(dato: string) {
    this.datos.push(dato);
  }
}
```

#### Directivas

Modifican el comportamiento del DOM:

```html
<!-- Directiva estructural *ngFor -->
<ul>
  <li *ngFor="let item of items">{{ item }}</li>
</ul>

<!-- Directiva estructural *ngIf -->
<div *ngIf="mostrarContenido">
  Este contenido es condicional
</div>

<!-- Directiva de atributo -->
<p [ngStyle]="{'color': color, 'font-size': tamano + 'px'}">
  Texto con estilo dinámico
</p>
```

#### Pipes

Transforman datos para su visualización:

```html
<p>{{ fecha | date:'dd/MM/yyyy' }}</p>
<p>{{ precio | currency:'EUR' }}</p>
<p>{{ nombre | uppercase }}</p>
<p>{{ textoLargo | slice:0:50 }}...</p>
```

## 5. ¿Qué es Ionic?

Ionic es un framework de desarrollo de aplicaciones móviles híbridas que se construye sobre Angular:

- **Multiplataforma**: Una única base de código para iOS, Android y web
- **Componentes nativos**: UI con apariencia y comportamiento nativo
- **Capacitor/Cordova**: Acceso a APIs nativas del dispositivo
- **Rendimiento optimizado**: Aplicaciones rápidas y responsivas

## 6. Componentes UI de Ionic

Ionic ofrece una amplia biblioteca de componentes que siguen las directrices de diseño de iOS y Android:

#### Navegación y Estructura

```html
<!-- Tabs -->
<ion-tabs>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="home">
      <ion-icon name="home"></ion-icon>
      <ion-label>Inicio</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="search">
      <ion-icon name="search"></ion-icon>
      <ion-label>Buscar</ion-label>
    </ion-tab-button>
    <ion-tab-button tab="profile">
      <ion-icon name="person"></ion-icon>
      <ion-label>Perfil</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>

<!-- Menú lateral -->
<ion-menu side="start" menuId="first">
  <ion-header>
    <ion-toolbar color="primary">
      <ion-title>Menú</ion-title>
    </ion-toolbar>
  </ion-header>
  <ion-content>
    <ion-list>
      <ion-item>Inicio</ion-item>
      <ion-item>Perfil</ion-item>
      <ion-item>Configuración</ion-item>
    </ion-list>
  </ion-content>
</ion-menu>
```

> **Nota:** Para ver ejemplos interactivos de estos componentes, visite la documentación oficial:
> - [Ionic Tabs](https://ionicframework.com/docs/api/tabs)
> - [Ionic Menu](https://ionicframework.com/docs/api/menu)

#### Componentes Básicos

```html
<!-- Botones -->
<ion-button>Botón Estándar</ion-button>
<ion-button fill="outline">Botón Outline</ion-button>
<ion-button expand="block" color="success">Botón Block</ion-button>

<!-- Cards -->
<ion-card>
  <ion-card-header>
    <ion-card-subtitle>Subtítulo</ion-card-subtitle>
    <ion-card-title>Título Principal</ion-card-title>
  </ion-card-header>
  <ion-card-content>
    Contenido detallado de la tarjeta que puede incluir
    texto, imágenes y otros elementos.
  </ion-card-content>
</ion-card>

<!-- Listas -->
<ion-list>
  <ion-list-header>Usuarios</ion-list-header>
  <ion-item>
    <ion-avatar slot="start">
      <img src="avatar.png">
    </ion-avatar>
    <ion-label>
      <h2>Juan Pérez</h2>
      <p>Desarrollador</p>
    </ion-label>
  </ion-item>
  <ion-item>
    <ion-avatar slot="start">
      <img src="avatar2.png">
    </ion-avatar>
    <ion-label>
      <h2>María García</h2>
      <p>Diseñadora</p>
    </ion-label>
  </ion-item>
</ion-list>
```

> **Nota:** Puede visualizar estos componentes en la documentación oficial:
> - [Ionic Buttons](https://ionicframework.com/docs/api/button)
> - [Ionic Cards](https://ionicframework.com/docs/api/card)
> - [Ionic Lists](https://ionicframework.com/docs/api/list)

#### Formularios y Entrada

```html
<!-- Inputs básicos -->
<ion-item>
  <ion-label position="floating">Nombre</ion-label>
  <ion-input type="text"></ion-input>
</ion-item>

<ion-item>
  <ion-label position="floating">Email</ion-label>
  <ion-input type="email"></ion-input>
</ion-item>

<!-- Selectores -->
<ion-item>
  <ion-label>Categoría</ion-label>
  <ion-select>
    <ion-select-option value="deportes">Deportes</ion-select-option>
    <ion-select-option value="musica">Música</ion-select-option>
    <ion-select-option value="tecnologia">Tecnología</ion-select-option>
  </ion-select>
</ion-item>

<!-- Toggle y checkbox -->
<ion-item>
  <ion-label>Notificaciones</ion-label>
  <ion-toggle slot="end"></ion-toggle>
</ion-item>

<ion-item>
  <ion-label>Acepto términos</ion-label>
  <ion-checkbox slot="start"></ion-checkbox>
</ion-item>
```

> **Nota:** Consulte la documentación oficial para ver ejemplos interactivos:
> - [Ionic Input](https://ionicframework.com/docs/api/input)
> - [Ionic Select](https://ionicframework.com/docs/api/select)
> - [Ionic Toggle](https://ionicframework.com/docs/api/toggle)
> - [Ionic Checkbox](https://ionicframework.com/docs/api/checkbox)

#### Feedback y Alertas

```typescript
import { AlertController, ToastController, LoadingController } from '@ionic/angular';

constructor(
  private alertCtrl: AlertController,
  private toastCtrl: ToastController,
  private loadingCtrl: LoadingController
) {}

async mostrarAlerta() {
  const alert = await this.alertCtrl.create({
    header: 'Alerta',
    subHeader: 'Información importante',
    message: '¿Estás seguro de realizar esta acción?',
    buttons: ['Cancelar', 'Aceptar']
  });
  await alert.present();
}

async mostrarToast() {
  const toast = await this.toastCtrl.create({
    message: 'Operación completada con éxito',
    duration: 2000,
    position: 'bottom',
    color: 'success'
  });
  toast.present();
}

async mostrarCargando() {
  const loading = await this.loadingCtrl.create({
    message: 'Cargando datos...',
    duration: 2000
  });
  await loading.present();
}
```

> **Nota:** Para más información sobre alertas y elementos interactivos, consulte:
> - [Ionic Alert](https://ionicframework.com/docs/api/alert)
> - [Ionic Toast](https://ionicframework.com/docs/api/toast)
> - [Ionic Loading](https://ionicframework.com/docs/api/loading)

## 7. Capacitor: El Puente Nativo

Capacitor es el framework que permite a Ionic acceder a las capacidades nativas del dispositivo:

- Cámara y galería de fotos
- Geolocalización
- Almacenamiento persistente
- Notificaciones push
- Sensores del dispositivo
- Archivos y sistema de archivos

Ejemplo básico de uso de Capacitor:

```typescript
import { Camera, CameraResultType } from '@capacitor/camera';

async function tomarFoto() {
  const imagen = await Camera.getPhoto({
    quality: 90,
    allowEditing: true,
    resultType: CameraResultType.Uri
  });
  
  // Usar la imagen (imagen.webPath)
  const imagenUrl = imagen.webPath;
}
```

## 8. Ciclo de Desarrollo con Ionic y Angular

El proceso típico de desarrollo con Ionic y Angular incluye:

1. **Creación del proyecto**:
   ```bash
   # Crear un nuevo proyecto (por defecto usa Angular)
   ionic start mi-proyecto-ionic
   
   # Especificar el framework (angular, react, vue)
   ionic start mi-proyecto-ionic --type=angular
   ```

2. **Desarrollo en navegador web**:
   ```bash
   # Iniciar servidor de desarrollo con recarga en vivo
   ionic serve
   ```

3. **Pruebas en dispositivo real**:
   ```bash
   # Añadir plataforma Android
   ionic capacitor add android
   
   # Ejecutar en dispositivo con recarga en vivo
   ionic capacitor run android --livereload
   ```

4. **Compilación para producción**:
   ```bash
   # Construir la aplicación optimizada
   ionic build --prod
   
   # Copiar los archivos a las plataformas nativas
   npx cap copy
   
   # Abrir el proyecto en Android Studio para ajustes finales
   npx cap open android
   ```

## Resumen

Angular e Ionic forman una poderosa combinación para desarrollar aplicaciones móviles multiplataforma:

- Angular proporciona la estructura, organización y lógica
- Ionic aporta componentes UI con aspecto nativo
- Capacitor permite acceder a características nativas del dispositivo
- El desarrollo es similar al web tradicional, pero más estructurado y optimizado

En los siguientes capítulos, exploraremos más a fondo cada uno de estos conceptos y construiremos paso a paso una aplicación completa de reservas para un gimnasio, además de la creación básica de un proyecto vacío.

## Recursos y Documentación

- [Documentación oficial de Angular](https://angular.io/docs)
- [Documentación de Ionic Framework](https://ionicframework.com/docs)
- [Capacitor Docs](https://capacitorjs.com/docs)
- [Angular CLI](https://cli.angular.io/)
- [TypeScript](https://www.typescriptlang.org/docs/)


# Taller: Desarrollo con Angular e Ionic

## Capítulo 2: Componentes y Estructura de una Aplicación Angular/Ionic

## 1. Estructura de una Aplicación Angular/Ionic

Al crear un nuevo proyecto de Ionic con Angular mediante el comando `ionic start`, se genera una estructura de carpetas organizada:

```
mi-app/
├── src/                   # Código fuente principal
│   ├── app/               # Lógica y componentes de la aplicación
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   ├── app-routing.module.ts
│   ├── assets/            # Recursos estáticos (imágenes, fuentes, etc.)
│   ├── environments/      # Configuraciones por entorno (dev, prod)
│   ├── theme/             # Variables globales de estilo (colores, etc.)
│   ├── global.scss        # Estilos globales
│   ├── index.html         # Archivo HTML raíz
│   ├── main.ts            # Punto de entrada de la aplicación
├── angular.json           # Configuración de Angular
├── capacitor.config.ts    # Configuración de Capacitor
├── package.json           # Dependencias y scripts
├── tsconfig.json          # Configuración de TypeScript

```

### Archivos Clave:

-   **app.module.ts**: Módulo principal que configura la aplicación
-   **app-routing.module.ts**: Define las rutas de navegación
-   **app.component.ts**: Componente raíz que contiene toda la aplicación
-   **index.html**: Archivo HTML base donde se monta la aplicación
-   **main.ts**: Punto de entrada que arranca la aplicación Angular

## 2. Fundamentos de los Componentes Angular

### Anatomía de un Componente

Cada componente Angular consta de:

1.  **Clase TypeScript**: Contiene la lógica y datos
2.  **Plantilla HTML**: Define la estructura visual
3.  **Estilos CSS**: Define la apariencia (opcional)
4.  **Metadatos**: Configuración mediante decorador @Component

```typescript
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-producto',          // Cómo se referencia en HTML
  templateUrl: './producto.component.html',  // Plantilla HTML
  styleUrls: ['./producto.component.scss']   // Estilos
})
export class ProductoComponent implements OnInit {
  nombre: string = 'Smartphone';
  precio: number = 599.99;
  disponible: boolean = true;
  
  constructor() { }
  
  ngOnInit() {
    // Inicialización del componente
  }
  
  aplicarDescuento(porcentaje: number): void {
    this.precio = this.precio * (1 - porcentaje/100);
  }
}

```

### Ciclo de Vida de los Componentes

Los componentes tienen un ciclo de vida gestionado por Angular:

1.  **ngOnChanges**: Cuando cambian las propiedades de entrada (@Input)
2.  **ngOnInit**: Después de la primera inicialización
3.  **ngAfterViewInit**: Cuando la vista se ha inicializado
4.  **ngOnDestroy**: Justo antes de que Angular destruya el componente

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { DatosService } from './datos.service';

@Component({
  selector: 'app-ejemplo',
  template: '<div>{{datos}}</div>'
})
export class EjemploComponent implements OnInit, OnDestroy {
  datos: any[] = [];
  private suscripcion!: Subscription;
  
  constructor(private datosService: DatosService) { }
  
  ngOnInit() {
    // Perfecto para inicializar datos, suscripciones, etc.
    this.suscripcion = this.datosService.obtenerDatos()
      .subscribe(datos => {
        this.datos = datos;
      });
  }
  
  ngOnDestroy() {
    // Limpieza antes de destruir el componente
    if (this.suscripcion) {
      this.suscripcion.unsubscribe();
    }
  }
}

```

## 3. Vinculación de Datos (Data Binding)

Angular ofrece cuatro formas de vinculación de datos:

### Interpolación {{ }}

Muestra valores de propiedades en la plantilla:

```html
<h1>{{ titulo }}</h1>
<p>Precio: {{ precio | currency }}</p>
<div>Estado: {{ disponible ? 'En stock' : 'Agotado' }}</div>

```

### Property Binding [ ]

Vincula propiedades HTML con valores del componente:

```html
<img [src]="imagenUrl">
<button [disabled]="!formularioValido">Enviar</button>
<div [ngClass]="{'destacado': esDestacado, 'oculto': !visible}">
  Contenido con clases dinámicas
</div>

```

### Event Binding ( )

Responde a eventos del usuario:

```html
<button (click)="agregarAlCarrito()">Agregar al carrito</button>
<input (input)="actualizarBusqueda($event)">
<form (submit)="enviarFormulario()">
  <!-- Campos del formulario -->
</form>

```

### Two-Way Binding [( )]

Combina property binding y event binding para actualizar datos en ambas direcciones:

```html
<input [(ngModel)]="nombreUsuario">

<!-- Equivalente a: -->
<input [value]="nombreUsuario" (input)="nombreUsuario = $event.target.value">

```

> Nota: Para usar ngModel, debes importar FormsModule en tu módulo Angular.

## 4. Directivas en Angular

Las directivas son clases que extienden HTML con nueva funcionalidad.

### Directivas Estructurales

Modifican el DOM añadiendo o quitando elementos:

```html
<!-- *ngIf: Condicional -->
<div *ngIf="producto.disponible">
  El producto está disponible
</div>

<!-- *ngFor: Repetición -->
<ul>
  <li *ngFor="let producto of productos; let i = index">
    {{i+1}}. {{producto.nombre}} - {{producto.precio | currency}}
  </li>
</ul>

<!-- *ngSwitch: Condicional múltiple -->
<div [ngSwitch]="rol">
  <div *ngSwitchCase="'admin'">Panel de administrador</div>
  <div *ngSwitchCase="'editor'">Panel de editor</div>
  <div *ngSwitchDefault>Panel de usuario</div>
</div>

```

### Directivas de Atributo

Modifican la apariencia o comportamiento de elementos existentes:

```html
<!-- ngClass: Clases dinámicas -->
<div [ngClass]="{'activo': estaActivo, 'destacado': esDestacado}">
  Elemento con clases dinámicas
</div>

<!-- ngStyle: Estilos dinámicos -->
<div [ngStyle]="{'color': colorTexto, 'font-size.px': tamanoFuente}">
  Texto con estilo dinámico
</div>

<!-- ngModel: Two-way binding (requiere FormsModule) -->
<input [(ngModel)]="nombre" placeholder="Escribe tu nombre">

```

## 5. Componentes UI de Ionic - Visión General

Ionic proporciona una amplia gama de componentes UI pre-diseñados que siguen las guías de diseño de iOS y Android. A continuación, se muestra una visión general de las categorías principales:

### Categorías de Componentes

1.  **Navegación**
    
    -   `ion-header`, `ion-toolbar`, `ion-buttons`
    -   `ion-tabs`
    -   `ion-menu`
    -   `ion-back-button`
2.  **Contenido y Presentación**
    
    -   `ion-card` y componentes relacionados
    -   `ion-list`, `ion-item`
    -   `ion-grid`, `ion-row`, `ion-col`
    -   `ion-avatar`, `ion-thumbnail`
3.  **Formularios e Inputs**
    
    -   `ion-input`, `ion-textarea`
    -   `ion-select`, `ion-radio`, `ion-checkbox`
    -   `ion-toggle`, `ion-range`
    -   `ion-datetime`
4.  **Feedback al Usuario**
    
    -   `ion-loading`
    -   `ion-toast`
    -   `ion-alert`
    -   `ion-action-sheet`

### Ejemplo Básico de Uso

```html
<!-- Ejemplo de una página con algunos componentes básicos -->
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Mi Aplicación</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-card>
    <ion-card-header>
      <ion-card-title>Bienvenido</ion-card-title>
    </ion-card-header>
    <ion-card-content>
      Contenido de ejemplo para mostrar un card básico.
    </ion-card-content>
  </ion-card>
  
  <ion-list>
    <ion-item>
      <ion-label>Elemento 1</ion-label>
    </ion-item>
    <ion-item>
      <ion-label>Elemento 2</ion-label>
    </ion-item>
  </ion-list>
</ion-content>

```

### Controladores de Componentes

Para componentes interactivos como alertas, modales o toasts, se utilizan controladores:

```typescript
import { Component } from '@angular/core';
import { AlertController } from '@ionic/angular';

@Component({
  selector: 'app-ejemplo',
  templateUrl: './ejemplo.page.html',
})
export class EjemploPage {
  constructor(private alertController: AlertController) {}
  
  async mostrarAlerta() {
    const alert = await this.alertController.create({
      header: 'Información',
      message: 'Esta es una alerta de ejemplo',
      buttons: ['OK']
    });
    
    await alert.present();
  }
}

```

> **Nota:** Para más detalles sobre cada componente, consulta la [documentación oficial de Ionic](https://ionicframework.com/docs/components), donde encontrarás ejemplos completos de uso y todas las propiedades disponibles.

## 6. Servicios y Dependency Injection

Los servicios en Angular son clases que encapsulan lógica de negocio y compartir estado entre componentes. La inyección de dependencias facilita su uso.

### Creación de un Servicio

```bash
ionic generate service services/productos

```

```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Producto {
  id: number;
  nombre: string;
  precio: number;
  imagen: string;
  descripcion: string;
}

@Injectable({
  providedIn: 'root'  // Disponible en toda la aplicación
})
export class ProductosService {
  private apiUrl = 'https://mi-api.com/productos';
  
  constructor(private http: HttpClient) { }
  
  obtenerProductos(): Observable<Producto[]> {
    return this.http.get<Producto[]>(this.apiUrl);
  }
  
  obtenerProductoPorId(id: number): Observable<Producto> {
    return this.http.get<Producto>(`${this.apiUrl}/${id}`);
  }
  
  buscarProductos(termino: string): Observable<Producto[]> {
    return this.http.get<Producto[]>(this.apiUrl).pipe(
      map(productos => productos.filter(p => 
        p.nombre.toLowerCase().includes(termino.toLowerCase())
      ))
    );
  }
  
  agregarProducto(producto: Producto): Observable<Producto> {
    return this.http.post<Producto>(this.apiUrl, producto);
  }
  
  actualizarProducto(producto: Producto): Observable<Producto> {
    return this.http.put<Producto>(`${this.apiUrl}/${producto.id}`, producto);
  }
  
  eliminarProducto(id: number): Observable<any> {
    return this.http.delete(`${this.apiUrl}/${id}`);
  }
}

```

### Uso del Servicio en un Componente

```typescript
import { Component, OnInit } from '@angular/core';
import { ProductosService, Producto } from '../../services/productos.service';
import { LoadingController } from '@ionic/angular';

@Component({
  selector: 'app-lista-productos',
  templateUrl: './lista-productos.page.html',
  styleUrls: ['./lista-productos.page.scss'],
})
export class ListaProductosPage implements OnInit {
  productos: Producto[] = [];
  cargando = false;
  error = false;
  
  constructor(
    private productosService: ProductosService,
    private loadingController: LoadingController
  ) { }
  
  async ngOnInit() {
    await this.cargarProductos();
  }
  
  async cargarProductos() {
    this.cargando = true;
    
    const loading = await this.loadingController.create({
      message: 'Cargando productos...'
    });
    await loading.present();
    
    this.productosService.obtenerProductos().subscribe({
      next: (data) => {
        this.productos = data;
        this.error = false;
      },
      error: (error) => {
        console.error('Error al cargar productos', error);
        this.error = true;
      },
      complete: () => {
        this.cargando = false;
        loading.dismiss();
      }
    });
  }
  
  async refrescarProductos(event: any) {
    this.productosService.obtenerProductos().subscribe({
      next: (data) => {
        this.productos = data;
        this.error = false;
        event.target.complete();
      },
      error: (error) => {
        console.error('Error al refrescar productos', error);
        this.error = true;
        event.target.complete();
      }
    });
  }
  
  buscarProductos(event: any) {
    const termino = event.detail.value;
    
    if (termino && termino.trim() !== '') {
      this.productosService.buscarProductos(termino).subscribe(productos => {
        this.productos = productos;
      });
    } else {
      this.cargarProductos();
    }
  }
}

```

## 7. Navegación y Enrutamiento

Angular Router permite definir rutas para navegar entre páginas:

```typescript
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'inicio',
    pathMatch: 'full'
  },
  {
    path: 'inicio',
    loadChildren: () => import('./pages/inicio/inicio.module').then(m => m.InicioPageModule)
  },
  {
    path: 'productos',
    loadChildren: () => import('./pages/productos/productos.module').then(m => m.ProductosPageModule)
  },
  {
    path: 'producto/:id',
    loadChildren: () => import('./pages/detalle-producto/detalle-producto.module').then(m => m.DetalleProductoPageModule)
  },
  {
    path: 'carrito',
    loadChildren: () => import('./pages/carrito/carrito.module').then(m => m.CarritoPageModule)
  },
  {
    path: 'perfil',
    loadChildren: () => import('./pages/perfil/perfil.module').then(m => m.PerfilPageModule)
  },
  {
    path: '**',
    loadChildren: () => import('./pages/not-found/not-found.module').then(m => m.NotFoundPageModule)
  }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule { }

```

### Navegación Declarativa

```html
<!-- Enlace simple -->
<ion-button routerLink="/productos">Ver productos</ion-button>

<!-- Con parámetros -->
<ion-item [routerLink]="['/producto', producto.id]">
  {{ producto.nombre }}
</ion-item>

<!-- Con parámetros de consulta -->
<a [routerLink]="['/productos']" [queryParams]="{categoria: 'electronica'}">
  Ver electrónica
</a>

```

### Navegación Programática

```typescript
import { Component } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router';

@Component({
  selector: 'app-navegacion',
  templateUrl: './navegacion.page.html',
})
export class NavegacionPage {
  constructor(private router: Router) { }
  
  irAProductos() {
    this.router.navigate(['/productos']);
  }
  
  verProducto(id: number) {
    this.router.navigate(['/producto', id]);
  }
  
  filtrarProductos() {
    const navigationExtras: NavigationExtras = {
      queryParams: {
        categoria: 'electronica',
        orden: 'precio'
      }
    };
    this.router.navigate(['/productos'], navigationExtras);
  }
}

```

### Recibir Parámetros

```typescript
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductosService, Producto } from '../../services/productos.service';

@Component({
  selector: 'app-detalle-producto',
  templateUrl: './detalle-producto.page.html',
})
export class DetalleProductoPage implements OnInit {
  producto: Producto | undefined;
  
  constructor(
    private route: ActivatedRoute,
    private productosService: ProductosService
  ) { }
  
  ngOnInit() {
    // Parámetros de ruta
    this.route.paramMap.subscribe(params => {
      const id = Number(params.get('id'));
      if (id) {
        this.cargarProducto(id);
      }
    });
    
    // Parámetros de consulta
    this.route.queryParamMap.subscribe(params => {
      const vista = params.get('vista');
      console.log('Vista:', vista);
    });
  }
  
  cargarProducto(id: number) {
    this.productosService.obtenerProductoPorId(id).subscribe(producto => {
      this.producto = producto;
    });
  }
}

```

## 8. Integración con Capacitor

Para utilizar capacidades nativas con Capacitor, necesitas instalar y configurar plugins específicos.

### Ejemplo: Cámara

```bash
npm install @capacitor/camera
npx cap sync

```

```typescript
import { Component } from '@angular/core';
import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera';

@Component({
  selector: 'app-camara',
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>Cámara</ion-title>
      </ion-toolbar>
    </ion-header>
    
    <ion-content class="ion-padding">
      <ion-button expand="block" (click)="tomarFoto()">
        <ion-icon name="camera" slot="start"></ion-icon>
        Tomar Foto
      </ion-button>
      
      <div *ngIf="photoUrl" class="ion-margin-top">
        <img [src]="photoUrl" alt="Foto tomada">
      </div>
    </ion-content>
  `
})
export class CamaraPage {
  photoUrl: string | undefined;
  
  constructor() { }
  
  async tomarFoto() {
    try {
      const photo = await Camera.getPhoto({4. Las directivas extienden HTML con funcionalidades dinámicas
        quality: 90,
        allowEditing: true,
        resultType: CameraResultType.Uri,
        source: CameraSource.Camera
      });
      
      // La propiedad webPath es la URL que se puede usar para mostrar la foto
      this.photoUrl = photo.webPath;
    } catch (error) {
      console.error('Error al tomar la foto', error);
    }
  }
}

```

### Ejemplo: Geolocalización

```bash
npm install @capacitor/geolocation
npx cap sync

```

```typescript
import { Component } from '@angular/core';
import { Geolocation, Position } from '@capacitor/geolocation';

@Component({
  selector: 'app-geolocalizacion',
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>Ubicación</ion-title>
      </ion-toolbar>
    </ion-header>
    
    <ion-content class="ion-padding">
      <ion-button expand="block" (click)="obtenerUbicacion()">
        <ion-icon name="location" slot="start"></ion-icon>
        Obtener Ubicación
      </ion-button>
      
      <ion-card *ngIf="coordenadas">
        <ion-card-header>
          <ion-card-title>Tu ubicación actual</ion-card-title>
        </ion-card-header>
        <ion-card-content>
          <p><strong>Latitud:</strong> {{ coordenadas.latitude }}</p>
          <p><strong>Longitud:</strong> {{ coordenadas.longitude }}</p>
          <p><strong>Precisión:</strong> {{ coordenadas.accuracy }} metros</p>
        </ion-card-content>
      </ion-card>
    </ion-content>
  `
})
export class GeolocalizacionPage {
  coordenadas: {
    latitude: number;
    longitude: number;
    accuracy: number;
  } | undefined;
  
  constructor() { }
  
  async obtenerUbicacion() {
    try {
      // Solicitar permisos primero
      const permisos = await Geolocation.checkPermissions();
      
      if (permisos.location !== 'granted') {
        const solicitado = await Geolocation.requestPermissions();
        if (solicitado.location !== 'granted') {
          throw new Error('Permiso de ubicación denegado');
        }
      }
      
      const position = await Geolocation.getCurrentPosition({
        enableHighAccuracy: true
      });
      
      this.coordenadas = {
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
        accuracy: position.coords.accuracy
      };
    } catch (error) {
      console.error('Error al obtener ubicación', error);
    }
  }
}

```

## Resumen

En este capítulo hemos explorado a fondo los componentes y estructura de una aplicación Angular/Ionic:

1.  La estructura de carpetas sigue una organización lógica que separa código, estilos y recursos
2.  Los componentes Angular encapsulan HTML, CSS y lógica en unidades coherentes
3.  El enlace de datos permite la comunicación bidireccional entre vistas y lógica
4.  Las directivas extienden HTML con funcionalidades dinámicas
5.  Ionic proporciona componentes UI que se adaptan a iOS y Android automáticamente
6.  Los servicios permiten compartir lógica y estado entre componentes
7.  El enrutamiento facilita la navegación entre diferentes vistas
8.  Capacitor extiende las aplicaciones con acceso a funcionalidades nativas

Con estos fundamentos, estamos listos para combinar todos estos conceptos en una aplicación completa en el siguiente capítulo.

## Recursos Adicionales

-   [Guía de Componentes Angular](https://angular.io/guide/component-overview)
-   [Ciclo de Vida en Angular](https://angular.io/guide/lifecycle-hooks)
-   [Directivas en Angular](https://angular.io/guide/attribute-directives)
-   [Componentes UI de Ionic](https://ionicframework.com/docs/components)
-   [Servicios e Inyección de Dependencias](https://angular.io/guide/dependency-injection)
-   [Router de Angular](https://angular.io/guide/router)
-   [Documentación de Capacitor](https://capacitorjs.com/docs/apis)

# Taller: Desarrollo con Angular e Ionic

## Capítulo 3: Estructura de la Aplicación de Reservas para Gimnasio

En este capítulo, detallaremos la estructura y planificación de nuestra aplicación de reservas para gimnasio. Esto nos servirá como guía para el desarrollo que realizaremos en los siguientes capítulos.

## 1. Definición de Requisitos

Nuestra aplicación de reservas para gimnasio debe cumplir con los siguientes requisitos:

### Funcionalidades Principales

-   **Ver clases disponibles**: Listado de clases con información sobre horarios, instructores y plazas
-   **Reservar clases**: Posibilidad de reservar una clase si hay plazas disponibles
-   **Gestionar reservas**: Ver, modificar y cancelar reservas existentes
-   **Perfil de usuario**: Información básica del usuario y preferencias
-   **Notificaciones**: Recordatorios de clases reservadas

### Tipos de Usuario

-   **Clientes**: Pueden ver clases, hacer reservas y gestionar su perfil
-   **Administradores**: Pueden gestionar clases, instructores y ver estadísticas (para una versión futura)

## 2. Planificación de la Estructura

### Entidades Principales

1.  **Clases (GymClass)**
    
    -   ID
    -   Nombre
    -   Descripción
    -   Instructor
    -   Horario (Inicio/Fin)
    -   Capacidad máxima
    -   Reservas actuales
2.  **Reservas (Booking)**
    
    -   ID
    -   ID de Usuario
    -   ID de Clase
    -   Fecha
    -   Estado (Confirmada, Cancelada, Pendiente)
3.  **Usuario (User)**
    
    -   ID
    -   Nombre
    -   Email
    -   Foto de perfil
    -   Preferencias

### Páginas Principales

1.  **Inicio/Login**
    
    -   Autenticación de usuarios
    -   Pantalla de bienvenida
2.  **Lista de Clases**
    
    -   Mostrar todas las clases disponibles
    -   Filtros por día, tipo, instructor
3.  **Detalle de Clase**
    
    -   Información completa de la clase
    -   Botón para reservar
    -   Lista de participantes (opcional)
4.  **Mis Reservas**
    
    -   Lista de reservas del usuario
    -   Opciones para cancelar/modificar
5.  **Perfil**
    
    -   Datos del usuario
    -   Preferencias
    -   Estadísticas (opcional)

## 3. Diseño de la Arquitectura

### Estructura de Carpetas

```
gym-reservation-app/
├── src/
│   ├── app/
│   │   ├── core/             # Servicios core, guards, interceptores
│   │   │   ├── services/
│   │   │   ├── guards/
│   │   │   └── interceptors/
│   │   ├── shared/           # Componentes compartidos
│   │   │   ├── components/
│   │   │   ├── directives/
│   │   │   └── pipes/
│   │   ├── models/           # Interfaces y modelos
│   │   ├── pages/            # Páginas principales
│   │   │   ├── auth/
│   │   │   ├── classes/
│   │   │   ├── class-detail/
│   │   │   ├── bookings/
│   │   │   └── profile/
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   └── app-routing.module.ts
│   ├── assets/
│   │   ├── icon/
│   │   └── images/
│   ├── environments/
│   └── theme/
└── capacitor.config.ts

```

### Arquitectura y Patrones

Utilizaremos una arquitectura basada en:

1.  **Componentes**: Encapsulan partes de la UI y su lógica asociada
2.  **Servicios**: Manejan la lógica de negocio y el acceso a datos
3.  **Modelos**: Definen las estructuras de datos
4.  **Enrutamiento**: Gestiona la navegación entre páginas
5.  **Patrón Observable**: Para gestionar flujos de datos asincrónicos

## 4. Diseño de la Base de Datos

Para esta versión inicial, utilizaremos almacenamiento local mediante Capacitor/Storage. En una versión futura, podríamos migrar a Firebase o una API REST.

**Estructura del almacenamiento local**:

```json
{
  "classes": [
    {
      "id": "1",
      "name": "Yoga",
      "description": "Clase de yoga para todos los niveles",
      "instructor": "María López",
      "startTime": "2025-04-22T08:00:00",
      "endTime": "2025-04-22T09:00:00",
      "maxCapacity": 15,
      "currentBookings": 8
    },
    // Más clases...
  ],
  "bookings": [
    {
      "id": "1",
      "userId": "user123",
      "classId": "1",
      "className": "Yoga",
      "date": "2025-04-22T08:00:00",
      "status": "confirmed"
    },
    // Más reservas...
  ],
  "users": [
    {
      "id": "user123",
      "name": "Usuario Demo",
      "email": "usuario@ejemplo.com",
      "profilePic": "avatar.jpg",
      "preferences": {
        "notifications": true,
        "favoriteClasses": ["1", "3"]
      }
    },
    // Más usuarios...
  ]
}

```

## 5. Flujos de Navegación

### Flujo Principal

1.  **Inicio**: El usuario abre la aplicación
2.  **Autenticación**: El usuario inicia sesión (simulado en esta versión)
3.  **Tabs**: Navegación principal con tabs
    -   Tab 1: Clases disponibles
    -   Tab 2: Mis reservas
    -   Tab 3: Perfil

### Flujo de Reserva

1.  Usuario navega a "Clases disponibles"
2.  Usuario selecciona una clase específica
3.  En la pantalla de detalle, pulsa "Reservar Clase"
4.  Se muestra confirmación y se redirige a "Mis Reservas"

### Flujo de Cancelación

1.  Usuario navega a "Mis Reservas"
2.  Usuario desliza una reserva hacia la izquierda
3.  Pulsa en "Cancelar"
4.  Se muestra confirmación y se actualiza la lista



## 6. Integración con Capacitor

Para mejorar la experiencia de usuario y aprovechar las capacidades nativas, integraremos los siguientes plugins de Capacitor:

1.  **@capacitor/local-notifications**
    
    -   Enviar recordatorios de clases
    -   Notificaciones de confirmación de reservas
2.  **@capacitor/camera**
    
    -   Permitir a los usuarios actualizar su foto de perfil
    -   Opción de tomar foto o seleccionar de la galería
3.  **@capacitor/preferences (antes Storage)**
    
    -   Almacenar datos de usuario
    -   Guardar preferencias
    -   Persistir información de clases y reservas
4.  **@capacitor/device** (opcional)
    
    -   Obtener información del dispositivo para personalizar la experiencia

## 7. Plan de Implementación

Para desarrollar la aplicación seguiremos estos pasos:

1.  **Configuración del proyecto**
    
    -   Crear proyecto Ionic con Angular
    -   Instalar dependencias necesarias
2.  **Implementación de los modelos de datos**
    
    -   Definir interfaces TypeScript para las entidades
3.  **Servicios Core**
    
    -   Implementar servicios para gestionar clases
    -   Implementar servicios para gestionar reservas
    -   Implementar servicio de autenticación (mock)
4.  **Componentes Shared**
    
    -   Crear componentes reutilizables (tarjetas, listas, etc.)
5.  **Páginas principales**
    
    -   Implementar página de listado de clases
    -   Implementar página de detalle de clase
    -   Implementar página de mis reservas
    -   Implementar página de perfil
6.  **Navegación y enrutamiento**
    
    -   Configurar las rutas
    -   Implementar navegación entre páginas
7.  **Capacitor y funcionalidades nativas**
    
    -   Integrar notificaciones
    -   Integrar cámara
    -   Implementar almacenamiento persistente
8.  **Pruebas y ajustes**
    
    -   Probar funcionalidades
    -   Ajustar estilos y mejorar UX

## 8. Consideraciones para Versiones Futuras

En futuras versiones se podrían incluir:

1.  **Autenticación real**
    
    -   Integración con Firebase Auth o similares
    -   Roles de usuario (cliente/admin)
2.  **Backend real**
    
    -   API REST o Firebase para datos en tiempo real
    -   Sincronización entre dispositivos
3.  **Funcionalidades avanzadas**
    
    -   Pagos para clases premium
    -   Calendarios personalizados
    -   Estadísticas de asistencia
    -   Chat con instructores
4.  **Mejoras UX/UI**
    
    -   Temas personalizables
    -   Animaciones
    -   Modo oscuro/claro

## Conclusión

En este capítulo hemos definido la estructura de nuestra aplicación de reservas para gimnasio, estableciendo una base sólida para el desarrollo. Con esta planificación, podemos proceder a la implementación paso a paso, siguiendo las mejores prácticas de Angular e Ionic.

En el siguiente capítulo, comenzaremos con la implementación práctica, creando el proyecto base y desarrollando los modelos y servicios esenciales.

# Capítulo 4: Desarrollo Práctico de la Aplicación de Reservas

En este capítulo comenzaremos el desarrollo práctico de nuestra aplicación de reservas para gimnasio utilizando Ionic y Angular. Seguiremos paso a paso la implementación desde cero.

## 1. Creación y Configuración del Proyecto

### Crear el proyecto

Lo primero es crear el proyecto mediante el CLI de Ionic:

> Es necesario recordar de que se tiene que tener node instalado, en este caso yo personalmente estoy usando la versión LTS de node (22.14)

```bash
# Asegúrate de tener instalado Ionic CLI y Angular CLI
npm install -g @ionic/cli @angular/cli

# Crear el proyecto con el template de tabs
ionic start taller tabs --type=angular

# Entrar al directorio del proyecto
cd taller
```

![\[img\]https://i.ibb.co/Xx1kxbMn/instalar-cli.png\[/img\]](https://i.ibb.co/Xx1kxbMn/instalar-cli.png)
![enter image description here](https://i.ibb.co/Q3mcpwKt/imagen.png)

> Últimamente Angular impulsa mucho el uso de los conocidos como `componentes independientes`, personalmente no me gustan mucho, así que para efectos de la demostración lo haré con la versión clásica con módulos

### Estructura inicial del proyecto

Veamos la estructura de carpetas generada:

```
taller/
├── src/
│   ├── app/
│   │   ├── tab1/           # Tab 1 generado automáticamente
│   │   ├── tab2/           # Tab 2 generado automáticamente
│   │   ├── tab3/           # Tab 3 generado automáticamente
│   │   ├── tabs/           # Controlador de tabs
│   │   ├── app-routing.module.ts
│   │   ├── app.component.ts
│   │   └── app.module.ts
│   ├── assets/
│   ├── environments/
│   ├── theme/
│   ├── global.scss
│   ├── index.html
│   └── main.ts
├── angular.json
├── capacitor.config.ts
├── package.json
└── ...

```

### Ejecutar el proyecto

Iniciemos la aplicación para verificar que todo funciona correctamente:

```bash
ionic serve
```

> Literalmente usa exactamente el mismo servidor de desarrollo que una típica aplicación de Angular, aunque lo único que cambia en este punto es el puerto por defecto, ahora es el 8100 en vez del 4200, de igual manera se puede cambiar sin problemas

![enter image description here](https://i.ibb.co/VWc1Nfx9/imagen.png)

![proyecto,corriendo](https://i.ibb.co/gLfFxtVv/imagen.png)
Deberías ver la aplicación por defecto en tu navegador, con tres tabs básicos.

## 2. Organización del Proyecto

Vamos a reorganizar la estructura para adaptarla a nuestras necesidades:

```bash
# Crear directorios para nuestra estructura, puedes crearlos manualmente,
# o en mi caso que estoy usando una distribución linux los haré con estos comandos
mkdir -p src/app/pages
mkdir -p src/app/services
mkdir -p src/app/models
mkdir -p src/app/shared/components
```

## 3. Definición de Modelos

Creemos los modelos de datos que necesitaremos:

```bash
# Crear archivos para los modelos
# lo mismo de arriba, puedes crearlos manualmente, en mi caso usaré estos comandos
touch src/app/models/gym-class.model.ts
touch src/app/models/booking.model.ts
touch src/app/models/user.model.ts
```

Ahora implementemos las interfaces para cada modelo:

**gym-class.model.ts**:

```typescript
export interface GymClass {
  id: string;
  name: string;
  description: string;
  instructor: string;
  startTime: Date;
  endTime: Date;
  maxCapacity: number;
  currentBookings: number;
  category?: string;
  imageUrl?: string;
}

```

**booking.model.ts**:

```typescript
export interface Booking {
  id: string;
  userId: string;
  classId: string;
  className: string;
  date: Date;
  status: 'confirmed' | 'cancelled' | 'pending';
}

```

**user.model.ts**:

```typescript
export interface User {
  id: string;
  name: string;
  email: string;
  profilePic?: string;
  profilePicUrl?: string; // Para compatibilidad con el backend
  notificationsEnabled?: boolean; // Para compatibilidad con el backend
  preferences?: {
    notifications: boolean;
    favoriteClasses?: string[];
  };
}
```

## 4. Creación de Servicios

Ahora, implementaremos los servicios que gestionarán los datos:

```bash
# Generar servicios usando el CLI de Ionic
ionic generate service services/classes
ionic generate service services/bookings
ionic generate service services/auth
ionic generate service services/storage
ionic generate service services/upload
ionic generate service services/notification
```

### Implementación del Servicio de Almacenamiento

Primero vamos a instalar el plugin de Preferences de Capacitor:

> Este plugin es básicamente para guardar datos o caché de nuestra app, en caso de que la exportemos para móviles como Android o iOS

```bash
npm install @capacitor/preferences
```

Ahora implementemos el servicio de almacenamiento:

**services/storage.service.ts**:

```typescript
import { Injectable } from '@angular/core';
import { Preferences } from '@capacitor/preferences';

@Injectable({
  providedIn: 'root'
})
export class StorageService {

  constructor() { }

  async set(key: string, value: any): Promise<void> {
    await Preferences.set({
      key,
      value: JSON.stringify(value)
    });
  }

  async get(key: string): Promise<any> {
    const { value } = await Preferences.get({ key });
    
    if (value) {
      return JSON.parse(value);
    }
    
    return null;
  }

  async remove(key: string): Promise<void> {
    await Preferences.remove({ key });
  }

  async clear(): Promise<void> {
    await Preferences.clear();
  }
}
```

## 5. Configuración de Entornos

Configuremos los archivos de entorno para facilitar el cambio entre desarrollo y producción:

**src/environments/environment.ts**:

```typescript
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000'  // URL de la API de desarrollo
};
```

**src/environments/environment.prod.ts**:

```typescript
export const environment = {
  production: true,
  apiUrl: 'https://taller.ionic.lcespedes.dev'  // URL de la API de producción
};
```

## 6. Integración con Backend

Nuestra aplicación se conectará a un backend real en lugar de utilizar datos mockeados. Instalemos las dependencias necesarias:

```bash
npm install @angular/common/http
```

### Configuración del Módulo HTTP

Agreguemos el módulo HTTP al app.module.ts:

```typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';

import { IonicModule, IonicRouteStrategy } from '@ionic/angular';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule, 
    IonicModule.forRoot(), 
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }],
  bootstrap: [AppComponent],
})
export class AppModule {}
```

## 7. Implementación de Autenticación

Crearemos un módulo de autenticación completo:

```bash
ionic generate page pages/auth
ionic generate component pages/auth/components/login
ionic generate component pages/auth/components/register
ionic generate guard pages/auth/guards/auth
```

### Implementación del Guard de Autenticación

**src/app/pages/auth/guards/auth.guard.ts**:

```typescript
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../../../services/auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate {
  
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}
  
  async canActivate(): Promise<boolean> {
    // Esperar a que el servicio de autenticación esté inicializado
    await this.authService.waitForInitialization();
    
    if (this.authService.isLoggedIn()) {
      return true;
    }
    
    // Redirigir al login si no hay sesión
    this.router.navigateByUrl('/auth');
    return false;
  }
}
```

### Configuración de Rutas Principales

Actualizaremos el app-routing.module.ts para implementar las rutas correctas:

```typescript
import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
import { AuthGuard } from './pages/auth/guards/auth.guard';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'auth',
    pathMatch: 'full'
  },
  {
    path: 'app',
    loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule),
    canActivate: [AuthGuard]
  },
  {
    path: 'auth',
    loadChildren: () => import('./pages/auth/auth.module').then(m => m.AuthPageModule)
  },
  {
    path: 'classes',
    loadChildren: () => import('./pages/classes/classes.module').then(m => m.ClassesPageModule)
  },
  {
    path: 'class-detail',
    loadChildren: () => import('./pages/class-detail/class-detail.module').then(m => m.ClassDetailPageModule)
  },
  {
    path: 'bookings',
    loadChildren: () => import('./pages/bookings/bookings.module').then(m => m.BookingsPageModule)
  },
  {
    path: 'profile',
    loadChildren: () => import('./pages/profile/profile.module').then(m => m.ProfilePageModule)
  }
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}
```

## 8. Creación de Páginas Principales

Vamos a crear las páginas principales de nuestra aplicación:

```bash
# Eliminar las páginas de tabs generadas automáticamente
rm -rf src/app/tab1
rm -rf src/app/tab2
rm -rf src/app/tab3

# Generar nuestras propias páginas
ionic generate page pages/classes
ionic generate page pages/class-detail
ionic generate page pages/bookings
ionic generate page pages/profile
```

### Configuración de las rutas y tabs

Actualizamos el archivo de rutas de tabs:

**src/app/tabs/tabs-routing.module.ts**:

```typescript
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { TabsPage } from './tabs.page';

const routes: Routes = [
  {
    path: '',
    component: TabsPage,
    children: [
      {
        path: 'classes',
        loadChildren: () => import('../pages/classes/classes.module').then(m => m.ClassesPageModule)
      },
      {
        path: 'bookings',
        loadChildren: () => import('../pages/bookings/bookings.module').then(m => m.BookingsPageModule)
      },
      {
        path: 'profile',
        loadChildren: () => import('../pages/profile/profile.module').then(m => m.ProfilePageModule)
      },
      {
        path: '',
        redirectTo: '/app/classes',
        pathMatch: 'full'
      }
    ]
  },
  {
    path: '',
    redirectTo: '/app/classes',
    pathMatch: 'full'
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
})
export class TabsPageRoutingModule {}
```

Actualizamos el template de tabs:

**src/app/tabs/tabs.page.html**:

```html
<ion-tabs>
  <ion-tab-bar slot="bottom">
    <ion-tab-button tab="classes">
      <ion-icon name="calendar"></ion-icon>
      <ion-label>Clases</ion-label>
    </ion-tab-button>
    
    <ion-tab-button tab="bookings">
      <ion-icon name="bookmark"></ion-icon>
      <ion-label>Mis Reservas</ion-label>
    </ion-tab-button>
    
    <ion-tab-button tab="profile">
      <ion-icon name="person"></ion-icon>
      <ion-label>Perfil</ion-label>
    </ion-tab-button>
  </ion-tab-bar>
</ion-tabs>
```

## 9. Implementar Servicio de Autenticación

Ahora implementaremos el servicio de autenticación real que se comunica con el backend:

**services/auth.service.ts**:

```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { User } from '../models/user.model';
import { environment } from '../../environments/environment';
import { StorageService } from './storage.service';

// Clave para almacenar información de sesión
const AUTH_TOKEN_KEY = 'auth_token';
const USER_KEY = 'user_data';

// Interfaz para credenciales de login
interface LoginCredentials {
  email: string;
  password: string;
}

// Interfaz para datos de registro
interface RegisterData {
  name: string;
  email: string;
  password: string;
}

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private apiUrl = `${environment.apiUrl}/api/users`;
  private authUrl = `${environment.apiUrl}/api/auth`;
  private currentUserSubject = new BehaviorSubject<User | null>(null);
  public currentUser$ = this.currentUserSubject.asObservable();
  private isInitialized = false;
  
  constructor(
    private http: HttpClient,
    private storageService: StorageService
  ) {
    // Intentar recuperar sesión anterior
    this.initializeSession();
  }

  /**
   * Inicializa la sesión si hay datos guardados
   */
  async initializeSession() {
    if (this.isInitialized) return;
    
    try {
      const userData = await this.storageService.get(USER_KEY) as User | null;
      
      if (userData) {
        console.log('Sesión recuperada del almacenamiento local:', userData);
        // Normalizar campos para compatibilidad
        this.normalizeUserData(userData);
        this.currentUserSubject.next(userData);
      } else {
        console.log('No hay sesión guardada, redirigiendo a login');
        this.currentUserSubject.next(null);
      }
    } catch (error) {
      console.error('Error al inicializar sesión:', error);
      this.currentUserSubject.next(null);
    }
    
    this.isInitialized = true;
  }
  
  /**
   * Espera a que se complete la inicialización
   * Importante para evitar redirecciones erróneas cuando la app inicia
   */
  async waitForInitialization(): Promise<void> {
    if (this.isInitialized) return;
    
    // Si no se ha inicializado, iniciamos el proceso
    await this.initializeSession();
    
    // Esperar hasta que se inicialice (con timeout)
    let attempts = 0;
    while (!this.isInitialized && attempts < 10) {
      await new Promise(resolve => setTimeout(resolve, 100));
      attempts++;
    }
  }

  /**
   * Normaliza los datos de usuario para compatibilidad entre frontend/backend
   */
  private normalizeUserData(user: User) {
    // Sincronizar campos de imagen
    if (user.profilePicUrl && !user.profilePic) {
      user.profilePic = user.profilePicUrl;
    } else if (user.profilePic && !user.profilePicUrl) {
      user.profilePicUrl = user.profilePic;
    }
    
    // Sincronizar preferencias de notificaciones
    if (user.notificationsEnabled !== undefined && !user.preferences) {
      user.preferences = {
        notifications: user.notificationsEnabled,
        favoriteClasses: []
      };
    } else if (user.preferences?.notifications !== undefined && user.notificationsEnabled === undefined) {
      user.notificationsEnabled = user.preferences.notifications;
    }
  }

  getCurrentUser(): User | null {
    return this.currentUserSubject.value;
  }

  /**
   * Iniciar sesión con email y contraseña
   */
  login(credentials: LoginCredentials): Observable<User> {
    console.log('Intentando login con:', credentials);
    return this.http.post<any>(`${this.authUrl}/login`, credentials).pipe(
      tap(async (response) => {
        console.log('Respuesta de login:', response);
        
        // Guardar token si el backend lo devuelve
        if (response.token) {
          await this.storageService.set(AUTH_TOKEN_KEY, response.token);
        }
        
        // Verificar y normalizar datos del usuario
        const user = response.user || response;
        this.normalizeUserData(user);
        
        // Guardar datos del usuario
        await this.storageService.set(USER_KEY, user);
        
        // Actualizar estado de sesión
        this.currentUserSubject.next(user);
      }),
      map(response => response.user || response)
    );
  }
  
  /**
   * Registrar un nuevo usuario
   */
  register(userData: RegisterData): Observable<User> {
    console.log('Registrando usuario:', userData);
    const registerData = {
      name: userData.name,
      email: userData.email,
      password: userData.password
    };
    
    return this.http.post<any>(`${this.authUrl}/register`, registerData).pipe(
      tap(async (response) => {
        console.log('Respuesta de registro:', response);
        
        // Guardar token si el backend lo devuelve
        if (response.token) {
          await this.storageService.set(AUTH_TOKEN_KEY, response.token);
        }
        
        // Verificar y normalizar datos del usuario
        const user = response.user || response;
        this.normalizeUserData(user);
        
        // Guardar datos del usuario
        await this.storageService.set(USER_KEY, user);
        
        // Actualizar estado de sesión
        this.currentUserSubject.next(user);
      }),
      map(response => response.user || response)
    );
  }

  updateUserProfile(userData: Partial<User>): Observable<User> {
    const currentUser = this.getCurrentUser();
    if (!currentUser) {
      return of(currentUser as unknown as User);
    }

    console.log('Actualizando perfil de usuario con:', userData);
    
    // Asegurar que ambos campos de imagen estén sincronizados
    if (userData.profilePic && !userData.profilePicUrl) {
      userData.profilePicUrl = userData.profilePic;
    } else if (userData.profilePicUrl && !userData.profilePic) {
      userData.profilePic = userData.profilePicUrl;
    }
    
    // Sincronizar notificaciones entre los dos formatos
    if (userData.preferences?.notifications !== undefined && userData.notificationsEnabled === undefined) {
      userData.notificationsEnabled = userData.preferences.notifications;
    } else if (userData.notificationsEnabled !== undefined && 
               (!userData.preferences || userData.preferences.notifications === undefined)) {
      if (!userData.preferences) userData.preferences = { notifications: false, favoriteClasses: [] };
      userData.preferences.notifications = userData.notificationsEnabled;
    }

    // Solo enviamos al servidor los campos que espera
    const backendUserData = {
      name: userData.name,
      profilePicUrl: userData.profilePicUrl,
      notificationsEnabled: userData.notificationsEnabled || 
                          (userData.preferences ? userData.preferences.notifications : undefined)
    };

    return this.http.put<User>(`${this.apiUrl}/${currentUser.id}`, backendUserData).pipe(
      tap(async (user) => {
        console.log('Usuario actualizado correctamente:', user);
        
        // Sincronizar campos para mantener compatibilidad
        if (user.profilePicUrl && !user.profilePic) {
          user.profilePic = user.profilePicUrl;
        } else if (user.profilePic && !user.profilePicUrl) {
          user.profilePicUrl = user.profilePic;
        }
        
        // Mantener los campos que espera el frontend
        if (user.notificationsEnabled !== undefined && !user.preferences) {
          user.preferences = {
            notifications: user.notificationsEnabled,
            favoriteClasses: currentUser.preferences?.favoriteClasses || []
          };
        }
        
        // Actualizar almacenamiento local
        await this.storageService.set(USER_KEY, user);
        
        this.currentUserSubject.next(user);
      }),
      catchError(error => {
        console.error('Error al actualizar usuario:', error);
        // Devolver el usuario actualizado localmente para que la UI no se rompa
        // en caso de error de red
        const updatedUser = { ...currentUser, ...userData };
        this.currentUserSubject.next(updatedUser);
        return of(updatedUser);
      })
    );
  }

  /**
   * Cerrar sesión y eliminar datos
   */
  async logout(): Promise<boolean> {
    try {
      // En una aplicación real, enviaríamos petición al backend
      // this.http.post(`${this.authUrl}/logout`, {}).subscribe();
      
      // Limpiar datos de sesión
      await this.storageService.clear(); // Limpiamos TODA la caché de preferencias
      
      // Actualizar estado
      this.currentUserSubject.next(null);
      this.isInitialized = false; // Permitir nueva inicialización
      
      return true;
    } catch (error) {
      console.error('Error al cerrar sesión:', error);
      return false;
    }
  }
  
  /**
   * Verificar si hay sesión activa
   */
  isLoggedIn(): boolean {
    return !!this.currentUserSubject.value;
  }
}
```

## 10. Creación de Componentes de Login y Registro

Implementemos los componentes de autenticación:

**pages/auth/components/login/login.component.html**:

```html
<div class="auth-container">
  <h2>Iniciar Sesión</h2>
  
  <form [formGroup]="loginForm" (ngSubmit)="onSubmit()" #formDirective="ngForm">
    <ion-item class="ion-margin-bottom">
      <ion-label position="floating">Email</ion-label>
      <ion-input type="email" formControlName="email"></ion-input>
      <ion-note slot="error" *ngIf="loginForm.get('email')?.touched && loginForm.get('email')?.hasError('required')">
        Email es requerido
      </ion-note>
      <ion-note slot="error" *ngIf="loginForm.get('email')?.touched && loginForm.get('email')?.hasError('email')">
        Formato de email inválido
      </ion-note>
    </ion-item>
    
    <ion-item class="ion-margin-bottom">
      <ion-label position="floating">Contraseña</ion-label>
      <ion-input type="password" formControlName="password"></ion-input>
      <ion-note slot="error" *ngIf="loginForm.get('password')?.touched && loginForm.get('password')?.hasError('required')">
        Contraseña es requerida
      </ion-note>
      <ion-note slot="error" *ngIf="loginForm.get('password')?.touched && loginForm.get('password')?.hasError('minlength')">
        La contraseña debe tener al menos 6 caracteres
      </ion-note>
    </ion-item>
    
    <ion-button (click)="onSubmit()" expand="block" color="primary" type="button">
      Iniciar Sesión
    </ion-button>
  </form>
</div>
```

**pages/auth/components/register/register.component.html**:

```html
<div class="auth-container">
  <h2>Crear Cuenta</h2>
  
  <form [formGroup]="registerForm" (ngSubmit)="onSubmit()" #formDirective="ngForm">
    <ion-item class="ion-margin-bottom">
      <ion-label position="floating">Nombre</ion-label>
      <ion-input type="text" formControlName="name"></ion-input>
      <ion-note slot="error" *ngIf="registerForm.get('name')?.touched && registerForm.get('name')?.hasError('required')">
        Nombre es requerido
      </ion-note>
    </ion-item>
    
    <ion-item class="ion-margin-bottom">
      <ion-label position="floating">Email</ion-label>
      <ion-input type="email" formControlName="email"></ion-input>
      <ion-note slot="error" *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.hasError('required')">
        Email es requerido
      </ion-note>
      <ion-note slot="error" *ngIf="registerForm.get('email')?.touched && registerForm.get('email')?.hasError('email')">
        Formato de email inválido
      </ion-note>
    </ion-item>
    
    <ion-item class="ion-margin-bottom">
      <ion-label position="floating">Contraseña</ion-label>
      <ion-input type="password" formControlName="password"></ion-input>
      <ion-note slot="error" *ngIf="registerForm.get('password')?.touched && registerForm.get('password')?.hasError('required')">
        Contraseña es requerida
      </ion-note>
      <ion-note slot="error" *ngIf="registerForm.get('password')?.touched && registerForm.get('password')?.hasError('minlength')">
        La contraseña debe tener al menos 6 caracteres
      </ion-note>
    </ion-item>
    
    <ion-button (click)="onSubmit()" expand="block" color="primary" type="button">
      Registrarse
    </ion-button>
  </form>
</div>
```

**pages/auth/auth.page.html**:

```html
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Gym Reservation</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <div class="logo-container">
    <img src="assets/shapes.svg" alt="Logo" class="logo">
    <h1>Gym Reservation</h1>
    <p>Tu app para reservar clases en tu gimnasio favorito</p>
  </div>

  <ion-segment [(ngModel)]="segment" (ionChange)="segmentChanged($event)">
    <ion-segment-button value="login">
      <ion-label>Iniciar Sesión</ion-label>
    </ion-segment-button>
    <ion-segment-button value="register">
      <ion-label>Registrarse</ion-label>
    </ion-segment-button>
  </ion-segment>
  
  <div [ngSwitch]="segment">
    <app-login *ngSwitchCase="'login'" (loginSuccess)="onLoginSuccess()"></app-login>
    <app-register *ngSwitchCase="'register'" (registerSuccess)="onRegisterSuccess()"></app-register>
  </div>
  
  <div class="demo-access">
    <p>¿Quieres probar la app? Usa estos datos:</p>
    <p><strong>Email:</strong> usuario&#64;ejemplo.com</p>
    <p><strong>Contraseña:</strong> password123</p>
  </div>
</ion-content>
```

## 11. Servicio para Subir Imágenes

Implementemos un servicio para manejar la carga de imágenes al backend:

**services/upload.service.ts**:

```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class UploadService {
  private apiUrl = `${environment.apiUrl}/api/upload`;

  constructor(private http: HttpClient) { }

  /**
   * Sube una imagen al servidor
   * @param file Archivo a subir
   * @param type Tipo de imagen (avatar, class, etc)
   * @returns URL de la imagen subida
   */
  uploadImage(file: File, type: 'avatar' | 'class' = 'avatar'): Observable<{ imageUrl: string }> {
    const formData = new FormData();
    formData.append('image', file);
    formData.append('type', type);
    
    return this.http.post<{ imageUrl: string }>(`${this.apiUrl}`, formData);
  }

  /**
   * Convierte una imagen dataURL (base64) a un archivo File
   * @param dataUrl URL de datos base64
   * @param filename Nombre del archivo
   */
  dataURLtoFile(dataUrl: string, filename: string): File {
    const arr = dataUrl.split(',');
    const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/jpeg';
    const bstr = atob(arr[1]);
    let n = bstr.length;
    const u8arr = new Uint8Array(n);
    
    while (n--) {
      u8arr[n] = bstr.charCodeAt(n);
    }
    
    return new File([u8arr], filename, { type: mime });
  }
}
```

## Resumen

En este capítulo, hemos establecido las bases sólidas para nuestra aplicación de reservas para gimnasio:

1. Creamos la estructura del proyecto
2. Implementamos los modelos de datos
3. Configuramos los servicios básicos
4. Establecimos la autenticación con un backend real
5. Implementamos las rutas principales
6. Creamos las páginas y componentes esenciales
7. Integramos el sistema de subida de imágenes

En el siguiente capítulo, completaremos la implementación con:
- Página de listado de clases
- Detalle de clase y reserva
- Gestión de reservas
- Perfil de usuario
- Integración con capacidades nativas mediante Capacitor

## Disclaimer

> **Nota importante:** Si durante el desarrollo encuentras errores o discrepancias en la ejecución del código, verifica la implementación correcta de cada módulo. El código completo de cada componente está disponible en el repositorio del curso. Asegúrate de revisar que los nombres de las clases, servicios e importaciones coincidan exactamente con lo especificado en este tutorial.
> 
> Si tienes problemas con algún módulo específico, puedes verificar la estructura del proyecto y consultar la documentación oficial de Ionic y Angular para obtener más información sobre cómo resolver los errores más comunes.
> Adicionalmente igual puedes descargar el repositorio completo desde este [link](https://git.lcespedes.dev/lxluxo23/taller-ionic)
> Digo eso ya que es en ocasiones el ionic-cli funciona de manera distinta dependiendo de la versión de este mismo, además de la versión de node.


# Capítulo 5: Implementando Funcionalidades y Componentes Principales

En este capítulo completaremos nuestra aplicación de reservas para gimnasio implementando las páginas restantes y añadiendo funcionalidades para interactuar con nuestro backend.

## 1. Implementación de Servicios para Interactuar con el Backend

Antes de implementar las páginas, necesitamos actualizar nuestros servicios para interactuar con el backend en lugar de usar datos de ejemplo.

### Servicio de Clases

Actualizaremos el servicio de clases para obtener datos desde el backend:

**services/classes.service.ts**:

```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { GymClass } from '../models/gym-class.model';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class ClassesService {
  private apiUrl = `${environment.apiUrl}/api/classes`;
  private classesSubject = new BehaviorSubject<GymClass[]>([]);
  public classes$ = this.classesSubject.asObservable();
  
  constructor(private http: HttpClient) {
    // Inicializar cargando clases
    this.loadClasses();
  }

  private loadClasses(): void {
    this.http.get<GymClass[]>(this.apiUrl)
      .pipe(
        tap(classes => this.classesSubject.next(classes)),
        catchError(error => {
          console.error('Error al cargar clases:', error);
          return throwError(() => error);
        })
      )
      .subscribe();
  }

  getClasses(): Observable<GymClass[]> {
    // Refrescar datos desde el servidor
    this.loadClasses();
    return this.classes$;
  }

  getClassById(id: string): Observable<GymClass | null> {
    return this.http.get<GymClass>(`${this.apiUrl}/${id}`).pipe(
      catchError(error => {
        console.error(`Error al obtener clase con ID ${id}:`, error);
        return throwError(() => error);
      })
    );
  }

  searchClasses(term: string): Observable<GymClass[]> {
    return this.http.get<GymClass[]>(`${this.apiUrl}/search`, { 
      params: { term } 
    }).pipe(
      catchError(error => {
        console.error('Error al buscar clases:', error);
        return throwError(() => error);
      })
    );
  }

  filterByCategory(category: string): Observable<GymClass[]> {
    return this.http.get<GymClass[]>(`${this.apiUrl}`, { 
      params: { category } 
    }).pipe(
      catchError(error => {
        console.error('Error al filtrar clases por categoría:', error);
        return throwError(() => error);
      })
    );
  }

  // Este método actualiza el contador de reservas de una clase
  updateClassBookings(classId: string, change: number): Observable<GymClass> {
    const url = `${this.apiUrl}/${classId}/bookings`;
    return this.http.patch<GymClass>(url, { change }).pipe(
      tap(updatedClass => {
        // Actualizar la clase en la lista local
        const currentClasses = this.classesSubject.value;
        const index = currentClasses.findIndex(c => c.id === classId);
        
        if (index !== -1) {
          const updatedClasses = [...currentClasses];
          updatedClasses[index] = updatedClass;
          this.classesSubject.next(updatedClasses);
        }
      }),
      catchError(error => {
        console.error('Error al actualizar reservas de clase:', error);
        return throwError(() => error);
      })
    );
  }
}
```

### Servicio de Reservas

Ahora implementaremos el servicio de reservas para interactuar con el backend:

**services/bookings.service.ts**:

```typescript
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { catchError, map, switchMap, tap } from 'rxjs/operators';
import { Booking } from '../models/booking.model';
import { environment } from '../../environments/environment';
import { AuthService } from './auth.service';
import { ClassesService } from './classes.service';
import { NotificationService } from './notification.service';

@Injectable({
  providedIn: 'root'
})
export class BookingsService {
  private apiUrl = `${environment.apiUrl}/api/bookings`;
  private bookingsSubject = new BehaviorSubject<Booking[]>([]);
  public bookings$ = this.bookingsSubject.asObservable();
  
  constructor(
    private http: HttpClient,
    private authService: AuthService,
    private classesService: ClassesService,
    private notificationService: NotificationService
  ) {
    // Cargamos las reservas del usuario al inicializar el servicio
    this.loadUserBookings();
    
    // Nos suscribimos a cambios de usuario para actualizar las reservas
    this.authService.currentUser$.subscribe(user => {
      if (user) {
        this.loadUserBookings();
      } else {
        this.bookingsSubject.next([]);
      }
    });
  }

  private loadUserBookings(): void {
    const currentUser = this.authService.getCurrentUser();
    if (!currentUser) return;

    this.http.get<Booking[]>(`${this.apiUrl}/user/${currentUser.id}`)
      .pipe(
        tap(bookings => this.bookingsSubject.next(bookings)),
        catchError(error => {
          console.error('Error al cargar reservas:', error);
          return throwError(() => error);
        })
      )
      .subscribe();
  }

  getUserBookings(): Observable<Booking[]> {
    // Refrescar datos
    this.loadUserBookings();
    return this.bookings$;
  }

  getBookingById(id: string): Observable<Booking> {
    return this.http.get<Booking>(`${this.apiUrl}/${id}`).pipe(
      catchError(error => {
        console.error(`Error al obtener reserva con ID ${id}:`, error);
        return throwError(() => error);
      })
    );
  }

  addBooking(classId: string, className: string): Observable<Booking> {
    const currentUser = this.authService.getCurrentUser();
    if (!currentUser) {
      return throwError(() => new Error('No hay usuario autenticado'));
    }

    const bookingData = {
      userId: currentUser.id,
      classId,
      className,
      date: new Date(),
      status: 'confirmed'
    };

    return this.http.post<Booking>(this.apiUrl, bookingData).pipe(
      switchMap(booking => {
        // Actualizar el contador de la clase
        return this.classesService.updateClassBookings(classId, 1).pipe(
          switchMap(gymClass => {
            // Programar notificación para esta reserva
            this.notificationService.scheduleClassReminder(booking, gymClass);
            
            // Actualizar la lista local de reservas
            const bookings = [...this.bookingsSubject.value, booking];
            this.bookingsSubject.next(bookings);
            
            return of(booking);
          })
        );
      }),
      catchError(error => {
        console.error('Error al crear reserva:', error);
        return throwError(() => error);
      })
    );
  }

  cancelBooking(bookingId: string): Observable<Booking> {
    return this.http.patch<Booking>(`${this.apiUrl}/${bookingId}/cancel`, {}).pipe(
      switchMap(booking => {
        // Actualizar contador de la clase
        return this.classesService.updateClassBookings(booking.classId, -1).pipe(
          map(() => booking)
        );
      }),
      tap(booking => {
        // Cancelar notificación programada
        this.notificationService.cancelNotification(booking.id);
        
        // Actualizar lista local
        const bookings = this.bookingsSubject.value.map(b => 
          b.id === bookingId ? { ...b, status: 'cancelled' } : b
        );
        this.bookingsSubject.next(bookings);
      }),
      catchError(error => {
        console.error('Error al cancelar reserva:', error);
        return throwError(() => error);
      })
    );
  }
}
```

## 2. Implementación de la Página de Clases

Vamos a implementar la página que muestra el listado de clases disponibles:

**pages/classes/classes.page.html**:

```html
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Clases Disponibles</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-searchbar placeholder="Buscar clases" (ionInput)="buscarClases($event)" [debounce]="500"></ion-searchbar>

  <ion-segment (ionChange)="filtrarPorCategoria($event)" value="todas">
    <ion-segment-button value="todas">
      <ion-label>Todas</ion-label>
    </ion-segment-button>
    <ion-segment-button value="Mente y Cuerpo">
      <ion-label>Mente/Cuerpo</ion-label>
    </ion-segment-button>
    <ion-segment-button value="Cardiovascular">
      <ion-label>Cardio</ion-label>
    </ion-segment-button>
    <ion-segment-button value="Fuerza">
      <ion-label>Fuerza</ion-label>
    </ion-segment-button>
  </ion-segment>

  <div *ngIf="cargando" class="ion-text-center ion-padding">
    <ion-spinner></ion-spinner>
    <p>Cargando clases...</p>
  </div>

  <ion-list *ngIf="!cargando">
    <ion-item *ngFor="let gymClass of clasesFiltradas" [routerLink]="['/app/classes', gymClass.id]" detail>
      <ion-thumbnail slot="start">
        <img [src]="gymClass.imageUrl || 'assets/classes/default.jpg'" 
            onerror="this.src='assets/classes/default.jpg'">
      </ion-thumbnail>
      <ion-label>
        <h2>{{ gymClass.name }}</h2>
        <p>{{ gymClass.startTime | date:'EEE, d MMM, h:mm a' }}</p>
        <p>{{ gymClass.instructor }}</p>
        <ion-note>
          <ion-icon name="people-outline"></ion-icon>
          {{ gymClass.currentBookings }}/{{ gymClass.maxCapacity }}
        </ion-note>
      </ion-label>
      <ion-badge slot="end" *ngIf="gymClass.category">{{ gymClass.category }}</ion-badge>
    </ion-item>
  </ion-list>

  <ion-refresher slot="fixed" (ionRefresh)="refrescarClases($event)">
    <ion-refresher-content></ion-refresher-content>
  </ion-refresher>

  <div *ngIf="!cargando && clasesFiltradas.length === 0" class="ion-text-center ion-padding">
    <ion-icon name="sad-outline" style="font-size: 48px;"></ion-icon>
    <h3>No se encontraron clases</h3>
    <p>Intenta con otros filtros o términos de búsqueda.</p>
  </div>
</ion-content>
```

**pages/classes/classes.page.ts**:

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { GymClass } from '../../models/gym-class.model';
import { ClassesService } from '../../services/classes.service';

@Component({
  selector: 'app-classes',
  templateUrl: './classes.page.html',
  styleUrls: ['./classes.page.scss'],
})
export class ClassesPage implements OnInit, OnDestroy {
  clases: GymClass[] = [];
  clasesFiltradas: GymClass[] = [];
  cargando = true;
  terminoBusqueda = '';
  categoriaSeleccionada = 'todas';
  private subscription: Subscription = new Subscription();

  constructor(private classesService: ClassesService) { }

  ngOnInit() {
    this.cargarClases();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  ionViewWillEnter() {
    this.cargarClases();
  }

  cargarClases() {
    this.cargando = true;
    
    const sub = this.classesService.getClasses().subscribe({
      next: (classes) => {
        this.clases = classes;
        this.aplicarFiltros();
        this.cargando = false;
      },
      error: (error) => {
        console.error('Error al cargar clases', error);
        this.cargando = false;
      }
    });
    
    this.subscription.add(sub);
  }

  refrescarClases(event: any) {
    const sub = this.classesService.getClasses().subscribe({
      next: (classes) => {
        this.clases = classes;
        this.aplicarFiltros();
        event.target.complete();
      },
      error: (error) => {
        console.error('Error al refrescar clases', error);
        event.target.complete();
      }
    });
    
    this.subscription.add(sub);
  }

  buscarClases(event: any) {
    this.terminoBusqueda = event.detail.value.toLowerCase();
    this.aplicarFiltros();
  }

  filtrarPorCategoria(event: any) {
    this.categoriaSeleccionada = event.detail.value;
    this.aplicarFiltros();
  }

  private aplicarFiltros() {
    let resultado = [...this.clases];
    
    // Filtrar por término de búsqueda
    if (this.terminoBusqueda) {
      resultado = resultado.filter(clase => 
        clase.name.toLowerCase().includes(this.terminoBusqueda) ||
        clase.instructor.toLowerCase().includes(this.terminoBusqueda) ||
        clase.description.toLowerCase().includes(this.terminoBusqueda)
      );
    }
    
    // Filtrar por categoría
    if (this.categoriaSeleccionada !== 'todas') {
      resultado = resultado.filter(clase => 
        clase.category === this.categoriaSeleccionada
      );
    }
    
    // Ordenar por fecha/hora
    resultado.sort((a, b) => {
      const dateA = new Date(a.startTime);
      const dateB = new Date(b.startTime);
      return dateA.getTime() - dateB.getTime();
    });
    
    this.clasesFiltradas = resultado;
  }
}
```

## 3. Implementación de la Página de Detalle de Clase

Ahora crearemos la página de detalle que muestra la información de una clase y permite reservarla:

**pages/class-detail/class-detail.page.html**:

```html
<ion-header>
  <ion-toolbar color="primary">
    <ion-buttons slot="start">
      <ion-back-button defaultHref="/app/classes"></ion-back-button>
    </ion-buttons>
    <ion-title>Detalle de Clase</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <div *ngIf="cargando" class="ion-text-center">
    <ion-spinner></ion-spinner>
    <p>Cargando información...</p>
  </div>

  <div *ngIf="gymClass && !cargando">
    <ion-card>
      <ion-img [src]="gymClass.imageUrl || 'assets/classes/default.jpg'" 
               onerror="this.src='assets/classes/default.jpg'"
               class="class-image"></ion-img>
      
      <ion-card-header>
        <ion-badge>{{ gymClass.category }}</ion-badge>
        <ion-card-title class="ion-margin-top">{{ gymClass.name }}</ion-card-title>
        <ion-card-subtitle>Instructor: {{ gymClass.instructor }}</ion-card-subtitle>
      </ion-card-header>
      
      <ion-card-content>
        <p>{{ gymClass.description }}</p>
        
        <ion-list lines="none">
          <ion-item>
            <ion-icon name="time-outline" slot="start" color="primary"></ion-icon>
            <ion-label>
              <h3>Horario</h3>
              <p>{{ gymClass.startTime | date:'EEEE, d MMM, h:mm a' }} - {{ gymClass.endTime | date:'h:mm a' }}</p>
            </ion-label>
          </ion-item>
          
          <ion-item>
            <ion-icon name="people-outline" slot="start" color="primary"></ion-icon>
            <ion-label>
              <h3>Capacidad</h3>
              <p>{{ gymClass.currentBookings }}/{{ gymClass.maxCapacity }} plazas ocupadas</p>
              <ion-progress-bar [value]="gymClass.currentBookings / gymClass.maxCapacity" 
                              [color]="getCapacityColor()"></ion-progress-bar>
            </ion-label>
          </ion-item>
        </ion-list>
      </ion-card-content>
    </ion-card>
    
    <div class="ion-padding">
      <ion-button expand="block" (click)="reservarClase()" [disabled]="isClassFull() || reservando">
        <ion-spinner name="dots" *ngIf="reservando"></ion-spinner>
        <span *ngIf="!reservando">Reservar Plaza</span>
      </ion-button>
      
      <ion-text color="medium" *ngIf="isClassFull()" class="ion-text-center">
        <p>Lo sentimos, esta clase está completa.</p>
      </ion-text>
    </div>
  </div>
  
  <ion-fab vertical="bottom" horizontal="end" slot="fixed" *ngIf="gymClass">
    <ion-fab-button color="light">
      <ion-icon name="share-social"></ion-icon>
    </ion-fab-button>
    <ion-fab-list side="top">
      <ion-fab-button color="primary" (click)="compartir('whatsapp')">
        <ion-icon name="logo-whatsapp"></ion-icon>
      </ion-fab-button>
      <ion-fab-button color="secondary" (click)="compartir('twitter')">
        <ion-icon name="logo-twitter"></ion-icon>
      </ion-fab-button>
      <ion-fab-button color="tertiary" (click)="compartir('email')">
        <ion-icon name="mail"></ion-icon>
      </ion-fab-button>
    </ion-fab-list>
  </ion-fab>
</ion-content>
```

**pages/class-detail/class-detail.page.ts**:

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NavController, ToastController, AlertController } from '@ionic/angular';
import { Subscription } from 'rxjs';
import { GymClass } from '../../models/gym-class.model';
import { ClassesService } from '../../services/classes.service';
import { BookingsService } from '../../services/bookings.service';

@Component({
  selector: 'app-class-detail',
  templateUrl: './class-detail.page.html',
  styleUrls: ['./class-detail.page.scss'],
})
export class ClassDetailPage implements OnInit, OnDestroy {
  gymClass?: GymClass;
  cargando = true;
  reservando = false;
  private subscription: Subscription = new Subscription();

  constructor(
    private route: ActivatedRoute,
    private navCtrl: NavController,
    private classesService: ClassesService,
    private bookingsService: BookingsService,
    private toastCtrl: ToastController,
    private alertCtrl: AlertController
  ) { }

  ngOnInit() {
    this.cargarDatosClase();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  ionViewWillEnter() {
    this.cargarDatosClase();
  }

  cargarDatosClase() {
    const id = this.route.snapshot.paramMap.get('id');
    if (!id) {
      this.navCtrl.navigateBack('/app/classes');
      return;
    }

    this.cargando = true;
    
    const sub = this.classesService.getClassById(id).subscribe({
      next: (gymClass) => {
        if (gymClass) {
          this.gymClass = gymClass;
        } else {
          this.navCtrl.navigateBack('/app/classes');
          this.mostrarToast('Clase no encontrada', 'danger');
        }
        this.cargando = false;
      },
      error: (error) => {
        console.error('Error al cargar clase', error);
        this.cargando = false;
        this.navCtrl.navigateBack('/app/classes');
        this.mostrarToast('Error al cargar la información', 'danger');
      }
    });
    
    this.subscription.add(sub);
  }

  isClassFull(): boolean {
    if (!this.gymClass) return true;
    return this.gymClass.currentBookings >= this.gymClass.maxCapacity;
  }

  getCapacityColor(): string {
    if (!this.gymClass) return 'primary';
    
    const ratio = this.gymClass.currentBookings / this.gymClass.maxCapacity;
    
    if (ratio >= 0.9) return 'danger';
    if (ratio >= 0.7) return 'warning';
    return 'success';
  }

  async reservarClase() {
    if (!this.gymClass || this.isClassFull() || this.reservando) return;
    
    this.reservando = true;
    
    const sub = this.bookingsService.addBooking(this.gymClass.id, this.gymClass.name).subscribe({
      next: async (booking) => {
        this.mostrarToast(`¡Reserva confirmada para ${this.gymClass?.name}!`, 'success');
        
        // Actualizar contador de la clase en local para mejor UX
        if (this.gymClass) {
          this.gymClass.currentBookings++;
        }
        
        this.reservando = false;
        
        // Mostrar alerta de confirmación
        const alert = await this.alertCtrl.create({
          header: '¡Reserva Exitosa!',
          message: `Has reservado una plaza para ${this.gymClass?.name}. ¿Deseas ver tus reservas?`,
          buttons: [
            {
              text: 'No, seguir explorando',
              role: 'cancel'
            },
            {
              text: 'Ver Mis Reservas',
              handler: () => {
                this.navCtrl.navigateForward('/app/bookings');
              }
            }
          ]
        });
        
        await alert.present();
      },
      error: (error) => {
        console.error('Error al reservar', error);
        this.mostrarToast('Error al realizar la reserva', 'danger');
        this.reservando = false;
      }
    });
    
    this.subscription.add(sub);
  }

  async mostrarToast(mensaje: string, color: string = 'primary') {
    const toast = await this.toastCtrl.create({
      message: mensaje,
      duration: 2000,
      position: 'bottom',
      color: color
    });
    toast.present();
  }

  compartir(medio: string) {
    if (!this.gymClass) return;
    
    const mensaje = `¡He encontrado una clase de ${this.gymClass.name} con ${this.gymClass.instructor}!`;
    
    // En una app real, aquí integraríamos con la API de Compartir nativa
    this.mostrarToast(`Compartiendo por ${medio}...`, 'success');
  }
}
```

## 4. Implementación de la Página de Mis Reservas

Ahora vamos a implementar la página para gestionar las reservas del usuario:

**pages/bookings/bookings.page.html**:

```html
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Mis Reservas</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content>
  <div *ngIf="cargando" class="ion-text-center ion-padding">
    <ion-spinner></ion-spinner>
    <p>Cargando tus reservas...</p>
  </div>

  <ion-list *ngIf="!cargando && reservas.length > 0">
    <ion-list-header>
      <ion-label>Próximas Clases</ion-label>
    </ion-list-header>

    <ion-item-sliding *ngFor="let booking of reservas">
      <ion-item>
        <ion-thumbnail slot="start">
          <img [src]="getClassImage(booking.classId)" 
               onerror="this.src='assets/classes/default.jpg'">
        </ion-thumbnail>
        <ion-label>
          <h2>{{ booking.className }}</h2>
          <p>{{ obtenerFechaClase(booking) | date:'EEE, d MMM, h:mm a' }}</p>
          <ion-badge [color]="getStatusColor(booking.status)">
            {{ getStatusText(booking.status) }}
          </ion-badge>
        </ion-label>
      </ion-item>
      
      <ion-item-options side="end">
        <ion-item-option color="danger" (click)="confirmarCancelacion(booking)" 
                        *ngIf="booking.status === 'confirmed'">
          <ion-icon slot="icon-only" name="trash"></ion-icon>
          Cancelar
        </ion-item-option>
      </ion-item-options>
    </ion-item-sliding>
  </ion-list>
  
  <ion-refresher slot="fixed" (ionRefresh)="refrescarReservas($event)">
    <ion-refresher-content></ion-refresher-content>
  </ion-refresher>
  
  <div *ngIf="!cargando && reservas.length === 0" class="ion-text-center ion-padding empty-state">
    <ion-icon name="calendar-outline" class="large-icon"></ion-icon>
    <h3>No tienes reservas activas</h3>
    <p>Explora las clases disponibles y haz tu primera reserva</p>
    <ion-button expand="block" routerLink="/app/classes">
      <ion-icon name="fitness" slot="start"></ion-icon>
      Ver Clases Disponibles
    </ion-button>
  </div>
</ion-content>
```

**pages/bookings/bookings.page.ts**:

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { AlertController, ToastController } from '@ionic/angular';
import { Subscription } from 'rxjs';
import { Booking } from '../../models/booking.model';
import { BookingsService } from '../../services/bookings.service';
import { ClassesService } from '../../services/classes.service';

@Component({
  selector: 'app-bookings',
  templateUrl: './bookings.page.html',
  styleUrls: ['./bookings.page.scss'],
})
export class BookingsPage implements OnInit, OnDestroy {
  reservas: Booking[] = [];
  cargando = true;
  private clasesCache: { [id: string]: any } = {};
  private subscription: Subscription = new Subscription();

  constructor(
    private bookingsService: BookingsService,
    private classesService: ClassesService,
    private alertController: AlertController,
    private toastController: ToastController
  ) { }

  ngOnInit() {
    this.cargarReservas();
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  ionViewWillEnter() {
    this.cargarReservas();
  }

  cargarReservas() {
    this.cargando = true;
    
    const sub = this.bookingsService.getUserBookings().subscribe({
      next: (bookings) => {
        // Ordenar por fecha y mostrar primero las confirmadas
        this.reservas = bookings.sort((a, b) => {
          // Primero ordenar por estado (confirmadas primero)
          if (a.status === 'confirmed' && b.status !== 'confirmed') return -1;
          if (a.status !== 'confirmed' && b.status === 'confirmed') return 1;
          
          // Luego ordenar por fecha
          const dateA = new Date(a.date);
          const dateB = new Date(b.date);
          return dateA.getTime() - dateB.getTime();
        });
        
        // Prellenar el cache de clases para obtener fechas reales
        this.precargarClases();
        
        this.cargando = false;
      },
      error: (error) => {
        console.error('Error al cargar reservas', error);
        this.cargando = false;
      }
    });
    
    this.subscription.add(sub);
  }

  private precargarClases() {
    // Obtener solo IDs únicos de clases
    const classIds = [...new Set(this.reservas.map(b => b.classId))];
    
    classIds.forEach(id => {
      const sub = this.classesService.getClassById(id).subscribe(gymClass => {
        if (gymClass) {
          this.clasesCache[id] = gymClass;
        }
      });
      
      this.subscription.add(sub);
    });
  }

  getClassImage(classId: string): string {
    return this.clasesCache[classId]?.imageUrl || 'assets/classes/default.jpg';
  }

  obtenerFechaClase(booking: Booking): Date {
    // Si tenemos la clase en caché, usamos su fecha real
    if (this.clasesCache[booking.classId]) {
      return new Date(this.clasesCache[booking.classId].startTime);
    }
    // Si no, usamos la fecha de la reserva
    return new Date(booking.date);
  }

  refrescarReservas(event: any) {
    const sub = this.bookingsService.getUserBookings().subscribe({
      next: (bookings) => {
        this.reservas = bookings;
        event.target.complete();
        this.precargarClases();
      },
      error: (error) => {
        console.error('Error al refrescar reservas', error);
        event.target.complete();
      }
    });
    
    this.subscription.add(sub);
  }

  getStatusColor(status: string): string {
    switch (status) {
      case 'confirmed': return 'success';
      case 'cancelled': return 'danger';
      case 'pending': return 'warning';
      default: return 'medium';
    }
  }

  getStatusText(status: string): string {
    switch (status) {
      case 'confirmed': return 'Confirmada';
      case 'cancelled': return 'Cancelada';
      case 'pending': return 'Pendiente';
      default: return status;
    }
  }

  async confirmarCancelacion(booking: Booking) {
    const alert = await this.alertController.create({
      header: 'Confirmar Cancelación',
      message: `¿Estás seguro de que deseas cancelar tu reserva para la clase de ${booking.className}?`,
      buttons: [
        {
          text: 'No',
          role: 'cancel'
        },
        {
          text: 'Sí, Cancelar',
          handler: () => {
            this.cancelarReserva(booking.id);
          }
        }
      ]
    });

    await alert.present();
  }

  cancelarReserva(bookingId: string) {
    const sub = this.bookingsService.cancelBooking(bookingId).subscribe({
      next: async () => {
        const toast = await this.toastController.create({
          message: 'Reserva cancelada correctamente',
          duration: 2000,
          position: 'bottom',
          color: 'success'
        });
        toast.present();
        this.cargarReservas();
      },
      error: async (error) => {
        console.error('Error al cancelar reserva', error);
        const toast = await this.toastController.create({
          message: 'Error al cancelar la reserva',
          duration: 2000,
          position: 'bottom',
          color: 'danger'
        });
        toast.present();
      }
    });
    
    this.subscription.add(sub);
  }
}
```

## 5. Implementación de la Página de Perfil

Implementemos la página de perfil de usuario con capacidad para subir imágenes:

**pages/profile/profile.page.html**:

```html
<ion-header>
  <ion-toolbar color="primary">
    <ion-title>Mi Perfil</ion-title>
  </ion-toolbar>
</ion-header>

<ion-content class="ion-padding">
  <div class="profile-header ion-text-center">
    <ion-avatar class="profile-avatar" (click)="cambiarFotoPerfil()">
      <img [src]="profileImage || 'assets/avatar-placeholder.png'" 
           onerror="this.src='assets/avatar-placeholder.png'"
           alt="Foto de perfil">
    </ion-avatar>
    <p class="tap-text">Toca para cambiar foto</p>
    <h2>{{ usuario?.name || 'Usuario' }}</h2>
    <p>{{ usuario?.email || 'usuario@ejemplo.com' }}</p>
  </div>
  
  <ion-list lines="full" class="ion-margin-top">
    <ion-item button (click)="editarPerfil()">
      <ion-icon name="person-outline" slot="start" color="primary"></ion-icon>
      <ion-label>Editar Perfil</ion-label>
      <ion-icon name="chevron-forward" slot="end" color="medium"></ion-icon>
    </ion-item>
    
    <ion-item>
      <ion-icon name="notifications-outline" slot="start" color="primary"></ion-icon>
      <ion-label>Notificaciones</ion-label>
      <ion-toggle [(ngModel)]="notificationsEnabled" (ionChange)="toggleNotifications()"></ion-toggle>
    </ion-item>
    
    <ion-item-divider>
      <ion-label>Estadísticas</ion-label>
    </ion-item-divider>
    
    <ion-item>
      <ion-icon name="calendar-number-outline" slot="start" color="primary"></ion-icon>
      <ion-label>
        <h2>Clases Reservadas</h2>
        <p>{{ estadisticas.totalReservas }} reservas</p>
      </ion-label>
    </ion-item>
    
    <ion-item>
      <ion-icon name="fitness-outline" slot="start" color="primary"></ion-icon>
      <ion-label>
        <h2>Clases Completadas</h2>
        <p>{{ estadisticas.clasesCompletadas }} clases</p>
      </ion-label>
    </ion-item>
    
    <ion-item-divider>
      <ion-label>Cuenta</ion-label>
    </ion-item-divider>
    
    <ion-item button (click)="mostrarAyuda()">
      <ion-icon name="help-circle-outline" slot="start" color="primary"></ion-icon>
      <ion-label>Ayuda y Soporte</ion-label>
    </ion-item>
    
    <ion-item button (click)="confirmarCerrarSesion()">
      <ion-icon name="log-out-outline" slot="start" color="danger"></ion-icon>
      <ion-label color="danger">Cerrar Sesión</ion-label>
    </ion-item>
  </ion-list>

  <div class="app-info ion-text-center ion-margin-top">
    <p class="version">Versión 1.0.0</p>
    <p>© 2025 Gym Reservations App</p>
  </div>
</ion-content>
```

**pages/profile/profile.page.ts**:

```typescript
import { Component, OnInit, OnDestroy } from '@angular/core';
import { AlertController, ToastController, NavController, ActionSheetController } from '@ionic/angular';
import { Subscription } from 'rxjs';
import { User } from '../../models/user.model';
import { AuthService } from '../../services/auth.service';
import { BookingsService } from '../../services/bookings.service';
import { UploadService } from '../../services/upload.service';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';

@Component({
  selector: 'app-profile',
  templateUrl: './profile.page.html',
  styleUrls: ['./profile.page.scss'],
})
export class ProfilePage implements OnInit, OnDestroy {
  usuario: User | null = null;
  profileImage: string | undefined;
  notificationsEnabled = true;
  estadisticas = {
    totalReservas: 0,
    clasesCompletadas: 0
  };
  private subscription: Subscription = new Subscription();

  constructor(
    private authService: AuthService,
    private bookingsService: BookingsService,
    private uploadService: UploadService,
    private alertController: AlertController,
    private toastController: ToastController,
    private navController: NavController,
    private actionSheetController: ActionSheetController
  ) { }

  ngOnInit() {
    this.cargarDatosUsuario();
    
    // Suscribirse a cambios en el usuario
    const userSub = this.authService.currentUser$.subscribe(user => {
      if (user) {
        this.usuario = user;
        this.profileImage = user.profilePic || user.profilePicUrl;
        this.notificationsEnabled = user.notificationsEnabled || 
          (user.preferences ? user.preferences.notifications : true);
      }
    });
    
    this.subscription.add(userSub);
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

  ionViewWillEnter() {
    this.cargarDatosUsuario();
    this.cargarEstadisticas();
  }

  cargarDatosUsuario() {
    this.usuario = this.authService.getCurrentUser();
    if (this.usuario) {
      this.profileImage = this.usuario.profilePic || this.usuario.profilePicUrl;
      this.notificationsEnabled = this.usuario.notificationsEnabled || 
        (this.usuario.preferences ? this.usuario.preferences.notifications : true);
    }
  }

  cargarEstadisticas() {
    const sub = this.bookingsService.getUserBookings().subscribe(bookings => {
      this.estadisticas.totalReservas = bookings.length;
      // Contar reservas completadas (las que no están canceladas y son pasadas)
      this.estadisticas.clasesCompletadas = bookings.filter(b => 
        b.status === 'confirmed' && new Date(b.date) < new Date()
      ).length;
    });
    
    this.subscription.add(sub);
  }

  async cambiarFotoPerfil() {
    const actionSheet = await this.actionSheetController.create({
      header: 'Cambiar foto de perfil',
      buttons: [
        {
          text: 'Tomar foto',
          icon: 'camera',
          handler: () => {
            this.tomarFoto(CameraSource.Camera);
          }
        },
        {
          text: 'Elegir de la galería',
          icon: 'image',
          handler: () => {
            this.tomarFoto(CameraSource.Photos);
          }
        },
        {
          text: 'Cancelar',
          icon: 'close',
          role: 'cancel'
        }
      ]
    });
    await actionSheet.present();
  }

  async tomarFoto(source: CameraSource) {
    try {
      const permisos = await Camera.requestPermissions();
      
      if (permisos.photos === 'granted' || permisos.camera === 'granted') {
        const imagen = await Camera.getPhoto({
          quality: 90,
          allowEditing: true,
          resultType: CameraResultType.Uri,
          source: source
        });
        
        // Si tenemos la imagen, preparamos para subir
        if (imagen.webPath && imagen.path) {
          this.mostrarToast('Subiendo imagen...', 'primary');
          
          // Convertir Uri a File
          const blob = await fetch(imagen.webPath).then(r => r.blob());
          const file = new File([blob], `profile-${Date.now()}.${imagen.format || 'jpeg'}`, {
            type: `image/${imagen.format || 'jpeg'}`
          });
          
          // Subir la imagen al servidor
          const sub = this.uploadService.uploadImage(file, 'avatar').subscribe({
            next: (response) => {
              if (response && response.imageUrl) {
                // Actualizar perfil del usuario con la nueva imagen
                if (this.usuario) {
                  this.authService.updateUserProfile({
                    ...this.usuario,
                    profilePicUrl: response.imageUrl
                  }).subscribe({
                    next: (updatedUser) => {
                      this.profileImage = updatedUser.profilePicUrl || updatedUser.profilePic;
                      this.mostrarToast('Foto de perfil actualizada', 'success');
                    },
                    error: (error) => {
                      console.error('Error al actualizar perfil:', error);
                      this.mostrarToast('Error al actualizar perfil', 'danger');
                    }
                  });
                }
              }
            },
            error: (error) => {
              console.error('Error al subir imagen:', error);
              this.mostrarToast('Error al subir la imagen', 'danger');
            }
          });
          
          this.subscription.add(sub);
        }
      } else {
        this.mostrarToast('Necesitamos permiso para acceder a la cámara/galería', 'warning');
      }
    } catch (error) {
      console.error('Error al tomar foto', error);
      this.mostrarToast('Error al procesar la imagen', 'danger');
    }
  }

  async editarPerfil() {
    const alert = await this.alertController.create({
      header: 'Editar Perfil',
      inputs: [
        {
          name: 'name',
          type: 'text',
          placeholder: 'Nombre',
          value: this.usuario?.name
        }
      ],
      buttons: [
        {
          text: 'Cancelar',
          role: 'cancel'
        },
        {
          text: 'Guardar',
          handler: (data) => {
            if (this.usuario && data.name && data.name.trim() !== '') {
              this.authService.updateUserProfile({
                ...this.usuario,
                name: data.name.trim()
              }).subscribe({
                next: () => {
                  this.mostrarToast('Perfil actualizado correctamente', 'success');
                },
                error: (error) => {
                  console.error('Error al actualizar perfil:', error);
                  this.mostrarToast('Error al actualizar perfil', 'danger');
                }
              });
            }
          }
        }
      ]
    });
    
    await alert.present();
  }

  toggleNotifications() {
    if (this.usuario) {
      // Preparar datos para actualizar
      const updatedUserData: Partial<User> = {
        ...this.usuario,
        notificationsEnabled: this.notificationsEnabled,
        preferences: {
          ...this.usuario.preferences,
          notifications: this.notificationsEnabled
        }
      };
      
      // Actualizar en el backend
      this.authService.updateUserProfile(updatedUserData).subscribe({
        next: () => {
          this.mostrarToast(
            this.notificationsEnabled ? 'Notificaciones activadas' : 'Notificaciones desactivadas', 
            'success'
          );
        },
        error: (error) => {
          console.error('Error al actualizar preferencias', error);
          this.mostrarToast('Error al actualizar preferencias', 'danger');
          // Revertir el toggle si hubo error
          this.notificationsEnabled = !this.notificationsEnabled;
        }
      });
    }
  }

  async mostrarAyuda() {
    const alert = await this.alertController.create({
      header: 'Ayuda y Soporte',
      message: 'Para cualquier consulta o problema con la aplicación, contáctanos en: <br><strong>soporte@gymapp.com</strong>',
      buttons: ['Entendido']
    });
    await alert.present();
  }

  async confirmarCerrarSesion() {
    const alert = await this.alertController.create({
      header: 'Cerrar Sesión',
      message: '¿Estás seguro de que deseas cerrar sesión?',
      buttons: [
        {
          text: 'Cancelar',
          role: 'cancel'
        },
        {
          text: 'Cerrar Sesión',
          handler: () => {
            this.cerrarSesion();
          }
        }
      ]
    });
    await alert.present();
  }

  async cerrarSesion() {
    try {
      await this.authService.logout();
      this.mostrarToast('Sesión cerrada', 'success');
      this.navController.navigateRoot('/auth');
    } catch (error) {
      console.error('Error al cerrar sesión', error);
      this.mostrarToast('Error al cerrar sesión', 'danger');
    }
  }

  async mostrarToast(mensaje: string, color: string = 'primary') {
    const toast = await this.toastController.create({
      message: mensaje,
      duration: 2000,
      position: 'bottom',
      color: color
    });
    toast.present();
  }
}
```

## 6. Implementación de Notificaciones Locales

Para implementar notificaciones locales, primero instalamos el plugin de Capacitor:

```bash
npm install @capacitor/local-notifications
npx cap sync
```

Ahora implementamos el servicio de notificaciones:

**services/notification.service.ts**:

```typescript
import { Injectable } from '@angular/core';
import { LocalNotifications } from '@capacitor/local-notifications';
import { Booking } from '../models/booking.model';
import { GymClass } from '../models/gym-class.model';

@Injectable({
  providedIn: 'root'
})
export class NotificationService {

  constructor() {
    this.initialize();
  }

  private async initialize() {
    try {
      // Solicitar permisos al inicializar
      const permResult = await LocalNotifications.requestPermissions();
      console.log('Permisos de notificación:', permResult.display);
    } catch (error) {
      console.error('Error al inicializar notificaciones', error);
    }
  }

  async scheduleClassReminder(booking: Booking, gymClass: GymClass) {
    try {
      // Verificar que tenemos permisos
      const permResult = await LocalNotifications.requestPermissions();
      
      if (permResult.display !== 'granted') {
        console.log('Permisos de notificación no concedidos');
        return;
      }
      
      // Configurar el tiempo de la notificación (1 hora antes de la clase)
      const classTime = new Date(gymClass.startTime);
      const notificationTime = new Date(classTime);
      notificationTime.setHours(notificationTime.getHours() - 1);
      
      // No programar notificaciones en el pasado
      if (notificationTime <= new Date()) {
        console.log('No se programó notificación (fecha en el pasado)');
        return;
      }
      
      // Crear notificación
      await LocalNotifications.schedule({
        notifications: [
          {
            id: parseInt(booking.id.replace(/\D/g, '').substr(0, 8)) || Math.floor(Math.random() * 100000), 
            title: `¡Recordatorio de clase: ${gymClass.name}!`,
            body: `Tu clase de ${gymClass.name} con ${gymClass.instructor} comienza en 1 hora.`,
            schedule: { at: notificationTime },
            sound: 'default',
            actionTypeId: '',
            extra: {
              bookingId: booking.id,
              classId: gymClass.id
            }
          }
        ]
      });
      
      console.log(`Notificación programada para ${notificationTime.toLocaleString()}`);
      
    } catch (error) {
      console.error('Error al programar notificación', error);
    }
  }

  async cancelNotification(bookingId: string) {
    try {
      // Obtener notificaciones pendientes
      const pendingList = await LocalNotifications.getPending();
      
      // Buscar la notificación asociada a esta reserva
      const notificationToCancel = pendingList.notifications.find(notification => 
        notification.extra && notification.extra.bookingId === bookingId
      );
      
      if (notificationToCancel) {
        // Cancelar la notificación
        await LocalNotifications.cancel({
          notifications: [
            { id: notificationToCancel.id }
          ]
        });
        
        console.log(`Notificación para reserva ${bookingId} cancelada`);
      }
    } catch (error) {
      console.error('Error al cancelar notificación', error);
    }
  }

  async checkNotificationStatus() {
    try {
      // Verificar el estado de los permisos
      const permResult = await LocalNotifications.checkPermissions();
      return permResult.display === 'granted';
    } catch (error) {
      console.error('Error al verificar permisos', error);
      return false;
    }
  }
}
```

## 7. Configuración Final para Deployment

### Ajustes del archivo capacitor.config.ts

Configuremos los ajustes de Capacitor para nuestra aplicación:

```typescript
import { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'com.example.gymreservation',
  appName: 'Gym Reservation',
  webDir: 'www',
  server: {
    androidScheme: 'https'
  },
  plugins: {
    LocalNotifications: {
      smallIcon: "ic_stat_icon_config_sample",
      iconColor: "#488AFF",
      sound: "beep.wav"
    },
    Camera: {
      permissions: ['camera', 'photos']
    }
  }
};

export default config;
```

### Compilación para Producción

```bash
# Compilar para producción
ionic build --prod

# Sincronizar con proyectos nativos
npx cap sync

# Abrir proyecto en Android Studio
npx cap open android
```

## 8. Integración con Plataformas Nativas

### Permisos en Android

Para usar la cámara y las notificaciones en Android, necesitamos añadir permisos al archivo `AndroidManifest.xml`:

```xml
<!-- Permisos para cámara -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

<!-- Permisos para notificaciones -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
```

## Resumen

En este capítulo, hemos completado nuestra aplicación de reservas para gimnasio integrándola con un backend real:

1. Implementamos servicios que interactúan con un backend Node.js
2. Creamos una interfaz de usuario completa para la gestión de clases y reservas
3. Añadimos autenticación de usuarios con registro y login
4. Implementamos la subida de imágenes para perfiles de usuario
5. Configuramos notificaciones locales para recordatorios de clases
6. Preparamos la aplicación para su despliegue en dispositivos móviles

La aplicación ahora cuenta con todas las funcionalidades principales planificadas y está lista para ser utilizada en un entorno real. Los usuarios pueden registrarse, explorar clases disponibles, realizar reservas, gestionar su perfil y recibir notificaciones sobre sus próximas clases.

## Próximos Pasos

En el siguiente capítulo, exploraremos:

1. Pruebas y depuración en dispositivos reales
2. Optimizaciones de rendimiento
3. Consideraciones para la publicación en tiendas de aplicaciones
4. Posibles extensiones y funcionalidades adicionales


# Capítulo 6: Estructura del Backend y Despliegue de la Aplicación

En este capítulo, exploraremos la estructura del backend que hemos implementado para nuestra aplicación de reservas de gimnasio y el proceso de despliegue para dispositivos móviles reales.

## 1. Arquitectura del Backend

Nuestra aplicación ahora cuenta con un backend real basado en Node.js, Express y PostgreSQL que gestiona todas las operaciones de datos. Este enfoque tiene varias ventajas sobre el almacenamiento local:

- Datos centralizados y accesibles desde cualquier dispositivo
- Mayor seguridad y control de acceso
- Escalabilidad mejorada para manejar más usuarios
- Sincronización de datos en tiempo real

### Estructura de Carpetas del Backend

```
backend/
├── config/
│   └── db.js                  # Configuración de la base de datos
├── controllers/
│   ├── authController.js      # Controlador de autenticación
│   ├── bookingController.js   # Controlador de reservas
│   ├── gymClassController.js  # Controlador de clases
│   ├── uploadController.js    # Controlador para subida de archivos
│   └── userController.js      # Controlador de usuarios
├── models/
│   ├── Booking.js             # Modelo de datos para reservas
│   ├── GymClass.js            # Modelo de datos para clases
│   ├── User.js                # Modelo de datos para usuarios
│   └── index.js               # Exporta todos los modelos
├── routes/
│   ├── authRoutes.js          # Rutas de autenticación
│   ├── bookingRoutes.js       # Rutas de reservas
│   ├── gymClassRoutes.js      # Rutas de clases
│   ├── uploadRoutes.js        # Rutas para subida de archivos
│   └── userRoutes.js          # Rutas de usuarios
├── uploads/                   # Directorio para archivos subidos
├── server.js                  # Punto de entrada principal
└── package.json               # Dependencias y scripts
```

### Tecnologías Utilizadas en el Backend

- **Node.js**: Entorno de ejecución JavaScript del lado del servidor
- **Express**: Framework web para crear APIs REST
- **PostgreSQL**: Base de datos relacional robusta para almacenar los datos
- **Sequelize**: ORM para modelar los datos y conectar con PostgreSQL
- **JWT (JSON Web Tokens)**: Para gestionar la autenticación de usuarios
- **Multer**: Para manejar la subida de archivos

## 2. Implementación del Backend

### Configuración de la Base de Datos

**config/db.js**:

```javascript
const { Sequelize } = require('sequelize');
const dotenv = require('dotenv');

dotenv.config();

// Create Sequelize instance
let sequelize;

if (process.env.DATABASE_URL) {
  // Use connection string if available
  sequelize = new Sequelize(process.env.DATABASE_URL, {
    logging: process.env.NODE_ENV === 'development' ? console.log : false,
    define: {
      freezeTableName: true // Prevent Sequelize from pluralizing table names
    },
    dialectOptions: {
      ssl: {
        require: true,
        rejectUnauthorized: false
      }
    },
    pool: {
      max: 5,
      min: 0,
      acquire: 30000,
      idle: 10000
    }
  });
} else {
  // Fall back to individual parameters
  sequelize = new Sequelize(
    process.env.DB_NAME || 'postgres',
    process.env.DB_USER || 'postgres',
    process.env.DB_PASSWORD || 'postgres',
    {
      host: process.env.DB_HOST || 'localhost',
      port: process.env.DB_PORT || 5432,
      dialect: 'postgres',
      logging: process.env.NODE_ENV === 'development' ? console.log : false,
      define: {
        freezeTableName: true // Prevent Sequelize from pluralizing table names
      },
      dialectOptions: {
        ssl: process.env.NODE_ENV === 'production' ? {
          require: true,
          rejectUnauthorized: false
        } : false
      },
      pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000
      }
    }
  );
}

// Test the connection
const connectDB = async () => {
  try {
    await sequelize.authenticate();
    console.log('Database connection established successfully.');
  } catch (error) {
    console.error('Unable to connect to the database:', error);
    process.exit(1);
  }
};

module.exports = { sequelize, connectDB };
```

### Modelos de Datos

**models/User.js**:

```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');
const bcrypt = require('bcryptjs');

const User = sequelize.define('user', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true
    }
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false
  },
  profilePicUrl: {
    type: DataTypes.STRING,
    defaultValue: '/uploads/default-avatar.jpg'
  },
  notificationsEnabled: {
    type: DataTypes.BOOLEAN,
    defaultValue: true
  }
}, {
  hooks: {
    beforeCreate: async (user) => {
      if (user.password) {
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
      }
    },
    beforeUpdate: async (user) => {
      if (user.changed('password')) {
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
      }
    }
  },
  tableName: 'users' // Explicitly set lowercase table name
});

// Method to check if password matches
User.prototype.matchPassword = async function(enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = User;
```

**models/GymClass.js**:

```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');

const GymClass = sequelize.define('gymClass', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  description: {
    type: DataTypes.TEXT,
    allowNull: false
  },
  instructor: {
    type: DataTypes.STRING,
    allowNull: false
  },
  startTime: {
    type: DataTypes.DATE,
    allowNull: false
  },
  endTime: {
    type: DataTypes.DATE,
    allowNull: false
  },
  maxCapacity: {
    type: DataTypes.INTEGER,
    allowNull: false
  },
  currentBookings: {
    type: DataTypes.INTEGER,
    defaultValue: 0
  },
  category: {
    type: DataTypes.STRING,
    defaultValue: 'Otros'
  },
  imageUrl: {
    type: DataTypes.STRING,
    defaultValue: '/uploads/default-class.jpg'
  }
}, {
  tableName: 'classes' // Explicitly set lowercase table name
});

module.exports = GymClass;
```

**models/Booking.js**:

```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');

const Booking = sequelize.define('booking', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  userId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: {
      model: 'users',
      key: 'id'
    }
  },
  classId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: {
      model: 'classes',
      key: 'id'
    }
  },
  className: {
    type: DataTypes.STRING,
    allowNull: false
  },
  date: {
    type: DataTypes.DATE,
    defaultValue: DataTypes.NOW
  },
  status: {
    type: DataTypes.ENUM('confirmed', 'cancelled', 'pending'),
    defaultValue: 'confirmed'
  }
}, {
  tableName: 'bookings' // Explicitly set lowercase table name
});

module.exports = Booking;
```

**models/index.js**:

```javascript
const User = require('./User');
const GymClass = require('./GymClass');
const Booking = require('./Booking');

// Define associations
User.hasMany(Booking, { foreignKey: 'userId' });
Booking.belongsTo(User, { foreignKey: 'userId' });

GymClass.hasMany(Booking, { foreignKey: 'classId' });
Booking.belongsTo(GymClass, { foreignKey: 'classId' });

module.exports = {
  User,
  GymClass,
  Booking
};
```

### Controladores 

**controllers/authController.js**:

```javascript
const jwt = require('jsonwebtoken');
const { User } = require('../models');

// Generate JWT Token
const generateToken = (id) => {
  return jwt.sign({ id }, process.env.JWT_SECRET || 'secret_key_12345', {
    expiresIn: '30d'
  });
};

// @desc    Register a new user
// @route   POST /api/auth/register
// @access  Public
exports.registerUser = async (req, res) => {
  try {
    const { name, email, password } = req.body;

    // Check if user already exists
    const existingUser = await User.findOne({ where: { email } });
    if (existingUser) {
      return res.status(400).json({
        success: false,
        message: 'Email is already registered'
      });
    }

    // Create user
    const user = await User.create({
      name,
      email,
      password
    });

    // Generate token
    const token = generateToken(user.id);

    res.status(201).json({
      success: true,
      token,
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
        profilePicUrl: user.profilePicUrl,
        notificationsEnabled: user.notificationsEnabled
      }
    });
  } catch (error) {
    console.error('Error registering user:', error);
    res.status(500).json({
      success: false,
      message: 'Server error',
      error: error.message
    });
  }
};

// @desc    Login user
// @route   POST /api/auth/login
// @access  Public
exports.loginUser = async (req, res) => {
  try {
    const { email, password } = req.body;

    // Check for user email
    const user = await User.findOne({ where: { email } });
    if (!user) {
      return res.status(401).json({
        success: false,
        message: 'Invalid credentials'
      });
    }

    // Check if password matches
    const isMatch = await user.matchPassword(password);
    if (!isMatch) {
      return res.status(401).json({
        success: false,
        message: 'Invalid credentials'
      });
    }

    // Generate token
    const token = generateToken(user.id);

    res.json({
      success: true,
      token,
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
        profilePicUrl: user.profilePicUrl,
        notificationsEnabled: user.notificationsEnabled
      }
    });
  } catch (error) {
    console.error('Error logging in:', error);
    res.status(500).json({
      success: false,
      message: 'Server error',
      error: error.message
    });
  }
};
```

### Rutas API

**routes/authRoutes.js**:

```javascript
const express = require('express');
const { registerUser, loginUser } = require('../controllers/authController');

const router = express.Router();

router.post('/register', registerUser);
router.post('/login', loginUser);

module.exports = router;
```

### Punto de Entrada del Servidor

**server.js**:

```javascript
const path = require('path');
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const morgan = require('morgan');
const fs = require('fs');
const { connectDB, sequelize } = require('./config/db');

// Load env variables
dotenv.config();

// Import route files
const userRoutes = require('./routes/userRoutes');
const gymClassRoutes = require('./routes/gymClassRoutes');
const bookingRoutes = require('./routes/bookingRoutes');
const uploadRoutes = require('./routes/uploadRoutes');
const authRoutes = require('./routes/authRoutes');

// Initialize express app
const app = express();

// Middleware
app.use(express.json());
app.use(cors());

// Dev logging middleware
if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
}

// Create uploads directory if it doesn't exist
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
  fs.mkdirSync(uploadsDir, { recursive: true });
}

// Set static folder for uploads
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

// Mount routers
app.use('/api/users', userRoutes);
app.use('/api/classes', gymClassRoutes);
app.use('/api/bookings', bookingRoutes);
app.use('/api/upload', uploadRoutes);
app.use('/api/auth', authRoutes);

// Root route
app.get('/', (req, res) => {
  res.json({ message: 'Welcome to Gym API' });
});

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    message: 'Server Error',
    error: err.message
  });
});

// Define port
const PORT = process.env.PORT || 3000;

// Connect to database and start server
const startServer = async () => {
  try {
    // Connect to database
    await connectDB();
    
    await sequelize.sync();
    console.log('Database synchronized');
    
    // Start server
    app.listen(PORT, () => {
      console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
    });
  } catch (error) {
    console.error('Unable to start server:', error);
    process.exit(1);
  }
};

startServer();

// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
  console.log(`Error: ${err.message}`);
  // Close server & exit process
  process.exit(1);
});
```

## 3. Despliegue en Plataformas Móviles

Una de las grandes ventajas de usar Ionic con Capacitor es la posibilidad de desplegar la misma base de código en múltiples plataformas. Vamos a revisar el proceso para Android.

### Preparación para Android

1. **Instalar las Dependencias de Android**:
   - Android Studio
   - JDK (Java Development Kit)
   - Configurar variables de entorno (ANDROID_HOME, JAVA_HOME)

2. **Añadir la Plataforma Android a nuestro Proyecto**:

```bash
# Instalar @capacitor/android si no está instalado
npm install @capacitor/android

# Añadir Android al proyecto
npx cap add android
```

3. **Sincronizar el Proyecto**:

Después de hacer cambios en el código web, necesitamos construir el proyecto y sincronizarlo con las plataformas nativas:

```bash
# Construir la aplicación
ionic build --prod

# Sincronizar con Android
npx cap sync android
```

4. **Abrir el Proyecto en Android Studio**:

```bash
npx cap open android
```

### Configuración del AndroidManifest.xml

El archivo `AndroidManifest.xml` es crucial para definir los permisos y configuraciones de la aplicación Android. Ya cubrimos algunos permisos esenciales:

```xml
<!-- Permisos para cámara -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

<!-- Permisos para notificaciones -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
```

### Personalización de Iconos y Splash Screen

Para personalizar los iconos y la pantalla de inicio:

1. **Reemplazar los Archivos de Iconos**: 
   - Crear iconos para diferentes resoluciones
   - Reemplazar los archivos en `android/app/src/main/res/mipmap-*/`

2. **Reemplazar la Pantalla de Inicio (Splash Screen)**:
   - Crear imágenes para diferentes orientaciones y resoluciones
   - Reemplazar los archivos en `android/app/src/main/res/drawable-*/splash.png`

3. **Personalizar el Nombre de la Aplicación**:
   - Editar `android/app/src/main/res/values/strings.xml`:
   ```xml
   <resources>
       <string name="app_name">Gym Reservation</string>
       <string name="title_activity_main">Gym Reservation</string>
       <string name="package_name">com.example.gymreservation</string>
       <string name="custom_url_scheme">com.example.gymreservation</string>
   </resources>
   ```

### Construir un APK de Prueba

Para generar un APK que puedas compartir con probadores:

1. En Android Studio, selecciona **Build > Build Bundle(s) / APK(s) > Build APK(s)**
2. Cuando termine, haz clic en **locate** para encontrar el archivo APK
3. Este archivo se puede compartir directamente para instalarlo en dispositivos Android (se debe habilitar la instalación desde fuentes desconocidas)

### Construir un Bundle para Google Play

Para publicar en la tienda de Google Play:

1. En Android Studio, selecciona **Build > Generate Signed Bundle / APK**
2. Elige **Android App Bundle**
3. Crea o selecciona una clave de firma (keystore)
4. Completa la información necesaria y genera el bundle (.aab)
5. Este archivo es el que se sube a la Google Play Console

## 4. Consideraciones de Seguridad

Al desplegar una aplicación móvil que se comunica con un backend, es crucial considerar aspectos de seguridad:

### Autenticación Segura

Nuestra aplicación implementa JWT (JSON Web Tokens) para autenticar usuarios, pero aquí hay algunas mejoras adicionales:

- **Tokens de Actualización**: Implementar tokens de corta duración con tokens de actualización
- **Almacenamiento Seguro**: Utilizar almacenamiento nativo seguro para tokens 
- **HTTPS**: Garantizar que todas las comunicaciones API usen HTTPS

### Manejo de Datos Sensibles

- **No almacenar contraseñas en texto plano**: Ya estamos usando bcrypt para hash de contraseñas
- **Minimizar los datos almacenados localmente**: Solo guardar lo necesario en el dispositivo
- **Eliminar datos al cerrar sesión**: Limpiar datos sensibles cuando el usuario cierre sesión

### Permisos de Aplicación

- **Solicitar solo los permisos necesarios**: No solicitar permisos que no sean esenciales
- **Explicar por qué se necesitan permisos**: Proporcionar contexto cuando se solicitan permisos
- **Manejar denegaciones graciosamente**: La aplicación debe funcionar aunque se denieguen permisos no críticos

## 5. Optimizaciones de Rendimiento

Para garantizar que nuestra aplicación funcione de manera óptima en dispositivos reales:

### Optimización de Imágenes

- **Redimensionar imágenes**: Utilizar tamaños apropiados para dispositivos móviles
- **Compresión de imágenes**: Reducir el tamaño de archivo sin perder calidad visible
- **Carga progresiva**: Implementar carga progresiva para imágenes grandes

### Optimización de Red

- **Implementar caché**: Almacenar en caché respuestas de API para reducir solicitudes de red
- **Carga perezosa (lazy loading)**: Cargar datos solo cuando sean necesarios
- **Compresión de datos**: Utilizar gzip en el servidor para respuestas API

### Optimización de la UI

- **Virtualización de listas**: Para listas largas, solo renderizar elementos visibles
- **Animaciones eficientes**: Usar animaciones CSS en lugar de JavaScript cuando sea posible
- **Evitar work blocking**: No bloquear el hilo principal con operaciones pesadas

## 6. Pruebas y Depuración

Para identificar y resolver problemas en dispositivos reales:

### Herramientas de Depuración

- **Chrome Remote Debugging**: Para depurar WebView en dispositivos Android
- **Safari Web Inspector**: Para depurar WebView en dispositivos iOS
- **Capacitor Logs**: Usar `npx cap logs android` para ver logs en tiempo real

### Pruebas en Dispositivos Reales

- **Probar en diferentes tamaños de pantalla**: Asegurar que la UI sea responsiva
- **Probar con diferentes velocidades de red**: Simular conexiones lentas
- **Pruebas de batería**: Verificar que la aplicación no consuma demasiada batería

## Resumen

En este capítulo, hemos explorado:

1. La arquitectura de nuestro backend Node.js/Express/PostgreSQL
2. Los detalles de implementación de los modelos, controladores y rutas del backend
3. El proceso de despliegue para dispositivos Android
4. Consideraciones de seguridad para aplicaciones móviles
5. Técnicas de optimización de rendimiento
6. Herramientas y enfoques para pruebas y depuración

Con esto, nuestra aplicación Gym Reservation está lista para ser utilizada por usuarios reales, con un backend robusto y una experiencia móvil optimizada.

