cap 4
This commit is contained in:
parent
cf6be05994
commit
d20150506f
@ -136,7 +136,8 @@
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"@ionic/angular-toolkit"
|
||||
]
|
||||
],
|
||||
"analytics": false
|
||||
},
|
||||
"schematics": {
|
||||
"@ionic/angular-toolkit:component": {
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@ -20,6 +20,7 @@
|
||||
"@capacitor/core": "7.2.0",
|
||||
"@capacitor/haptics": "7.0.1",
|
||||
"@capacitor/keyboard": "7.0.1",
|
||||
"@capacitor/preferences": "^7.0.1",
|
||||
"@capacitor/status-bar": "7.0.1",
|
||||
"@ionic/angular": "^8.0.0",
|
||||
"ionicons": "^7.0.0",
|
||||
@ -2768,6 +2769,15 @@
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/preferences": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-7.0.1.tgz",
|
||||
"integrity": "sha512-XF9jOHzvoIBZLwZr/EX6aVaUO1d8Mx7TwBLQS33pYHOliCW5knT5KUkFOXNNYxh9qqODYesee9xuQIKNJpQBag==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@capacitor/status-bar": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-7.0.1.tgz",
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"@capacitor/core": "7.2.0",
|
||||
"@capacitor/haptics": "7.0.1",
|
||||
"@capacitor/keyboard": "7.0.1",
|
||||
"@capacitor/preferences": "^7.0.1",
|
||||
"@capacitor/status-bar": "7.0.1",
|
||||
"@ionic/angular": "^8.0.0",
|
||||
"ionicons": "^7.0.0",
|
||||
|
||||
@ -5,6 +5,22 @@ const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () => import('./tabs/tabs.module').then(m => m.TabsPageModule)
|
||||
},
|
||||
{
|
||||
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({
|
||||
|
||||
8
src/app/models/booking.model.ts
Normal file
8
src/app/models/booking.model.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface Booking {
|
||||
id: string;
|
||||
userId: string;
|
||||
classId: string;
|
||||
className: string;
|
||||
date: Date;
|
||||
status: 'confirmed' | 'cancelled' | 'pending';
|
||||
}
|
||||
12
src/app/models/gym-class.model.ts
Normal file
12
src/app/models/gym-class.model.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export interface GymClass {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
instructor: string;
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
maxCapacity: number;
|
||||
currentBookings: number;
|
||||
category?: string;
|
||||
imageUrl?: string;
|
||||
}
|
||||
10
src/app/models/user.model.ts
Normal file
10
src/app/models/user.model.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
profilePic?: string;
|
||||
preferences?: {
|
||||
notifications: boolean;
|
||||
favoriteClasses?: string[];
|
||||
};
|
||||
}
|
||||
17
src/app/pages/bookings/bookings-routing.module.ts
Normal file
17
src/app/pages/bookings/bookings-routing.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { BookingsPage } from './bookings.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: BookingsPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class BookingsPageRoutingModule {}
|
||||
20
src/app/pages/bookings/bookings.module.ts
Normal file
20
src/app/pages/bookings/bookings.module.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { BookingsPageRoutingModule } from './bookings-routing.module';
|
||||
|
||||
import { BookingsPage } from './bookings.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
BookingsPageRoutingModule
|
||||
],
|
||||
declarations: [BookingsPage],
|
||||
})
|
||||
export class BookingsPageModule {}
|
||||
@ -1,17 +1,13 @@
|
||||
<ion-header [translucent]="true">
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Tab 2
|
||||
</ion-title>
|
||||
<ion-title>bookings</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content [fullscreen]="true">
|
||||
<ion-header collapse="condense">
|
||||
<ion-toolbar>
|
||||
<ion-title size="large">Tab 2</ion-title>
|
||||
<ion-title size="large">bookings</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<app-explore-container name="Tab 2 page"></app-explore-container>
|
||||
</ion-content>
|
||||
17
src/app/pages/bookings/bookings.page.spec.ts
Normal file
17
src/app/pages/bookings/bookings.page.spec.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { BookingsPage } from './bookings.page';
|
||||
|
||||
describe('BookingsPage', () => {
|
||||
let component: BookingsPage;
|
||||
let fixture: ComponentFixture<BookingsPage>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(BookingsPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
16
src/app/pages/bookings/bookings.page.ts
Normal file
16
src/app/pages/bookings/bookings.page.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bookings',
|
||||
templateUrl: './bookings.page.html',
|
||||
styleUrls: ['./bookings.page.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class BookingsPage implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
17
src/app/pages/class-detail/class-detail-routing.module.ts
Normal file
17
src/app/pages/class-detail/class-detail-routing.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { ClassDetailPage } from './class-detail.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ClassDetailPage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ClassDetailPageRoutingModule {}
|
||||
17
src/app/pages/class-detail/class-detail.module.ts
Normal file
17
src/app/pages/class-detail/class-detail.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { ClassDetailPageRoutingModule } from './class-detail-routing.module';
|
||||
import { ClassDetailPage } from './class-detail.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ClassDetailPageRoutingModule
|
||||
],
|
||||
declarations: [ClassDetailPage],
|
||||
})
|
||||
export class ClassDetailPageModule {}
|
||||
79
src/app/pages/class-detail/class-detail.page.html
Normal file
79
src/app/pages/class-detail/class-detail.page.html
Normal file
@ -0,0 +1,79 @@
|
||||
<ion-header>
|
||||
<ion-toolbar color="primary">
|
||||
<ion-buttons slot="start">
|
||||
<ion-back-button defaultHref="/tabs/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'" 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>
|
||||
9
src/app/pages/class-detail/class-detail.page.scss
Normal file
9
src/app/pages/class-detail/class-detail.page.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.class-image {
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
ion-progress-bar {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
17
src/app/pages/class-detail/class-detail.page.spec.ts
Normal file
17
src/app/pages/class-detail/class-detail.page.spec.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ClassDetailPage } from './class-detail.page';
|
||||
|
||||
describe('ClassDetailPage', () => {
|
||||
let component: ClassDetailPage;
|
||||
let fixture: ComponentFixture<ClassDetailPage>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ClassDetailPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
150
src/app/pages/class-detail/class-detail.page.ts
Normal file
150
src/app/pages/class-detail/class-detail.page.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { NavController, ToastController, AlertController } from '@ionic/angular';
|
||||
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'],
|
||||
standalone: false
|
||||
})
|
||||
export class ClassDetailPage implements OnInit {
|
||||
gymClass?: GymClass;
|
||||
cargando = true;
|
||||
reservando = false;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private navCtrl: NavController,
|
||||
private classesService: ClassesService,
|
||||
private bookingsService: BookingsService,
|
||||
private toastCtrl: ToastController,
|
||||
private alertCtrl: AlertController
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.cargarDatosClase();
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
this.cargarDatosClase();
|
||||
}
|
||||
|
||||
cargarDatosClase() {
|
||||
const id = this.route.snapshot.paramMap.get('id');
|
||||
if (!id) {
|
||||
this.navCtrl.navigateBack('/tabs/classes');
|
||||
return;
|
||||
}
|
||||
|
||||
this.cargando = true;
|
||||
this.classesService.getClassById(id).subscribe({
|
||||
next: (gymClass) => {
|
||||
if (gymClass) {
|
||||
this.gymClass = gymClass;
|
||||
} else {
|
||||
this.navCtrl.navigateBack('/tabs/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('/tabs/classes');
|
||||
this.mostrarToast('Error al cargar la información', 'danger');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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 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('/tabs/bookings');
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await alert.present();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al reservar', error);
|
||||
this.mostrarToast('Error al realizar la reserva', 'danger');
|
||||
this.reservando = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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}!`;
|
||||
|
||||
switch (medio) {
|
||||
case 'whatsapp':
|
||||
// En una app real, integraríamos con el plugin de Social Sharing
|
||||
this.mostrarToast('Compartiendo por WhatsApp...', 'success');
|
||||
break;
|
||||
case 'twitter':
|
||||
this.mostrarToast('Compartiendo por Twitter...', 'success');
|
||||
break;
|
||||
case 'email':
|
||||
this.mostrarToast('Compartiendo por Email...', 'success');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/app/pages/classes/classes-routing.module.ts
Normal file
20
src/app/pages/classes/classes-routing.module.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { ClassesPage } from './classes.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ClassesPage
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
loadChildren: () => import('../class-detail/class-detail.module').then(m => m.ClassDetailPageModule)
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ClassesPageRoutingModule {}
|
||||
17
src/app/pages/classes/classes.module.ts
Normal file
17
src/app/pages/classes/classes.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { ClassesPageRoutingModule } from './classes-routing.module';
|
||||
import { ClassesPage } from './classes.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ClassesPageRoutingModule
|
||||
],
|
||||
declarations: [ClassesPage],
|
||||
})
|
||||
export class ClassesPageModule {}
|
||||
57
src/app/pages/classes/classes.page.html
Normal file
57
src/app/pages/classes/classes.page.html
Normal file
@ -0,0 +1,57 @@
|
||||
<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]="['/tabs/classes', gymClass.id]" detail>
|
||||
<ion-thumbnail slot="start">
|
||||
<ion-img [src]="gymClass.imageUrl || 'assets/classes/default.jpg'"></ion-img>
|
||||
</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>
|
||||
17
src/app/pages/classes/classes.page.spec.ts
Normal file
17
src/app/pages/classes/classes.page.spec.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ClassesPage } from './classes.page';
|
||||
|
||||
describe('ClassesPage', () => {
|
||||
let component: ClassesPage;
|
||||
let fixture: ComponentFixture<ClassesPage>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ClassesPage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
93
src/app/pages/classes/classes.page.ts
Normal file
93
src/app/pages/classes/classes.page.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
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'],
|
||||
standalone: false
|
||||
})
|
||||
export class ClassesPage implements OnInit {
|
||||
clases: GymClass[] = [];
|
||||
clasesFiltradas: GymClass[] = [];
|
||||
cargando = true;
|
||||
terminoBusqueda = '';
|
||||
categoriaSeleccionada = 'todas';
|
||||
|
||||
constructor(private classesService: ClassesService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.cargarClases();
|
||||
}
|
||||
|
||||
ionViewWillEnter() {
|
||||
this.cargarClases();
|
||||
}
|
||||
|
||||
cargarClases() {
|
||||
this.cargando = true;
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refrescarClases(event: any) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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) => {
|
||||
return a.startTime.getTime() - b.startTime.getTime();
|
||||
});
|
||||
|
||||
this.clasesFiltradas = resultado;
|
||||
}
|
||||
}
|
||||
17
src/app/pages/profile/profile-routing.module.ts
Normal file
17
src/app/pages/profile/profile-routing.module.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { ProfilePage } from './profile.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ProfilePage
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class ProfilePageRoutingModule {}
|
||||
20
src/app/pages/profile/profile.module.ts
Normal file
20
src/app/pages/profile/profile.module.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ProfilePageRoutingModule } from './profile-routing.module';
|
||||
|
||||
import { ProfilePage } from './profile.page';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
ProfilePageRoutingModule
|
||||
],
|
||||
declarations: [ProfilePage]
|
||||
})
|
||||
export class ProfilePageModule {}
|
||||
@ -1,17 +1,13 @@
|
||||
<ion-header [translucent]="true">
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Tab 3
|
||||
</ion-title>
|
||||
<ion-title>profile</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content [fullscreen]="true">
|
||||
<ion-header collapse="condense">
|
||||
<ion-toolbar>
|
||||
<ion-title size="large">Tab 3</ion-title>
|
||||
<ion-title size="large">profile</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<app-explore-container name="Tab 3 page"></app-explore-container>
|
||||
</ion-content>
|
||||
17
src/app/pages/profile/profile.page.spec.ts
Normal file
17
src/app/pages/profile/profile.page.spec.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ProfilePage } from './profile.page';
|
||||
|
||||
describe('ProfilePage', () => {
|
||||
let component: ProfilePage;
|
||||
let fixture: ComponentFixture<ProfilePage>;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ProfilePage);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
16
src/app/pages/profile/profile.page.ts
Normal file
16
src/app/pages/profile/profile.page.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile',
|
||||
templateUrl: './profile.page.html',
|
||||
styleUrls: ['./profile.page.scss'],
|
||||
standalone: false
|
||||
})
|
||||
export class ProfilePage implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
16
src/app/services/auth.service.spec.ts
Normal file
16
src/app/services/auth.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AuthService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
66
src/app/services/auth.service.ts
Normal file
66
src/app/services/auth.service.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { delay, tap } from 'rxjs/operators';
|
||||
import { User } from '../models/user.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||
public currentUser$ = this.currentUserSubject.asObservable();
|
||||
|
||||
// Usuario de demostración
|
||||
private demoUser: User = {
|
||||
id: 'user123',
|
||||
name: 'Usuario Demo',
|
||||
email: 'usuario@ejemplo.com',
|
||||
preferences: {
|
||||
notifications: true
|
||||
}
|
||||
};
|
||||
|
||||
constructor() {
|
||||
// Simular usuario ya autenticado para el taller
|
||||
this.currentUserSubject.next(this.demoUser);
|
||||
}
|
||||
|
||||
getCurrentUser(): User | null {
|
||||
return this.currentUserSubject.value;
|
||||
}
|
||||
|
||||
// Simulación de login
|
||||
login(email: string, password: string): Observable<User> {
|
||||
// En una app real, aquí se realizaría la autenticación contra un backend
|
||||
return of(this.demoUser).pipe(
|
||||
delay(1000), // Simular latencia de red
|
||||
tap(user => this.currentUserSubject.next(user))
|
||||
);
|
||||
}
|
||||
|
||||
// Simulación de logout
|
||||
logout(): Observable<boolean> {
|
||||
return of(true).pipe(
|
||||
delay(500), // Simular latencia de red
|
||||
tap(() => this.currentUserSubject.next(null))
|
||||
);
|
||||
}
|
||||
|
||||
// Simulación de actualización de perfil
|
||||
updateUserProfile(userData: Partial<User>): Observable<User> {
|
||||
const currentUser = this.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return of(this.demoUser);
|
||||
}
|
||||
|
||||
const updatedUser: User = {
|
||||
...currentUser,
|
||||
...userData
|
||||
};
|
||||
|
||||
return of(updatedUser).pipe(
|
||||
delay(800), // Simular latencia de red
|
||||
tap(user => this.currentUserSubject.next(user))
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/app/services/bookings.service.spec.ts
Normal file
16
src/app/services/bookings.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BookingsService } from './bookings.service';
|
||||
|
||||
describe('BookingsService', () => {
|
||||
let service: BookingsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(BookingsService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
124
src/app/services/bookings.service.ts
Normal file
124
src/app/services/bookings.service.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, from, of } from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { Booking } from '../models/booking.model';
|
||||
import { StorageService } from './storage.service';
|
||||
import { ClassesService } from './classes.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class BookingsService {
|
||||
private STORAGE_KEY = 'bookings';
|
||||
private bookingsSubject = new BehaviorSubject<Booking[]>([]);
|
||||
public bookings$ = this.bookingsSubject.asObservable();
|
||||
private initialized = false;
|
||||
private userId = 'user123'; // En una app real, vendría de la autenticación
|
||||
|
||||
constructor(
|
||||
private storageService: StorageService,
|
||||
private classesService: ClassesService
|
||||
) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
const storedBookings = await this.storageService.get(this.STORAGE_KEY);
|
||||
|
||||
if (storedBookings) {
|
||||
// Convertir las fechas de string a objetos Date
|
||||
const bookings = storedBookings.map((booking: any) => ({
|
||||
...booking,
|
||||
date: new Date(booking.date)
|
||||
}));
|
||||
this.bookingsSubject.next(bookings);
|
||||
} else {
|
||||
this.bookingsSubject.next([]);
|
||||
await this.storageService.set(this.STORAGE_KEY, []);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
getUserBookings(): Observable<Booking[]> {
|
||||
return from(this.ensureInitialized()).pipe(
|
||||
switchMap(() => this.bookings$),
|
||||
map(bookings => bookings.filter(booking => booking.userId === this.userId))
|
||||
);
|
||||
}
|
||||
|
||||
addBooking(classId: string, className: string): Observable<Booking> {
|
||||
return from(this.ensureInitialized()).pipe(
|
||||
switchMap(() => {
|
||||
// Actualizar el contador de reservas de la clase
|
||||
return this.classesService.updateClassBookings(classId, 1).pipe(
|
||||
switchMap(success => {
|
||||
if (!success) {
|
||||
throw new Error('No se pudo actualizar la clase');
|
||||
}
|
||||
|
||||
const newBooking: Booking = {
|
||||
id: Date.now().toString(),
|
||||
userId: this.userId,
|
||||
classId,
|
||||
className,
|
||||
date: new Date(),
|
||||
status: 'confirmed'
|
||||
};
|
||||
|
||||
const currentBookings = this.bookingsSubject.value;
|
||||
const updatedBookings = [...currentBookings, newBooking];
|
||||
|
||||
return from(this.storageService.set(this.STORAGE_KEY, updatedBookings)).pipe(
|
||||
tap(() => this.bookingsSubject.next(updatedBookings)),
|
||||
map(() => newBooking)
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
cancelBooking(bookingId: string): Observable<boolean> {
|
||||
return from(this.ensureInitialized()).pipe(
|
||||
switchMap(() => {
|
||||
const currentBookings = this.bookingsSubject.value;
|
||||
const index = currentBookings.findIndex(b => b.id === bookingId);
|
||||
|
||||
if (index === -1) return of(false);
|
||||
|
||||
const booking = currentBookings[index];
|
||||
|
||||
// No permitir cancelar reservas ya canceladas
|
||||
if (booking.status === 'cancelled') return of(false);
|
||||
|
||||
// Crear copia actualizada
|
||||
const updatedBooking = { ...booking, status: 'cancelled' as 'cancelled' };
|
||||
const updatedBookings = [...currentBookings];
|
||||
updatedBookings[index] = updatedBooking;
|
||||
|
||||
// Actualizar contador de clase
|
||||
return this.classesService.updateClassBookings(booking.classId, -1).pipe(
|
||||
switchMap(success => {
|
||||
if (!success) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
return from(this.storageService.set(this.STORAGE_KEY, updatedBookings)).pipe(
|
||||
tap(() => this.bookingsSubject.next(updatedBookings)),
|
||||
map(() => true)
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/app/services/classes.service.spec.ts
Normal file
16
src/app/services/classes.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ClassesService } from './classes.service';
|
||||
|
||||
describe('ClassesService', () => {
|
||||
let service: ClassesService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ClassesService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
155
src/app/services/classes.service.ts
Normal file
155
src/app/services/classes.service.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable, from, of } from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { GymClass } from '../models/gym-class.model';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ClassesService {
|
||||
private STORAGE_KEY = 'gym_classes';
|
||||
private classesSubject = new BehaviorSubject<GymClass[]>([]);
|
||||
public classes$ = this.classesSubject.asObservable();
|
||||
private initialized = false;
|
||||
|
||||
// Datos iniciales para mock
|
||||
private initialClasses: GymClass[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Yoga',
|
||||
description: 'Clase de yoga para todos los niveles',
|
||||
instructor: 'María López',
|
||||
startTime: new Date('2025-04-24T08:00:00'),
|
||||
endTime: new Date('2025-04-24T09:00:00'),
|
||||
maxCapacity: 15,
|
||||
currentBookings: 8,
|
||||
category: 'Mente y Cuerpo',
|
||||
imageUrl: 'https://cdn-icons-png.flaticon.com/512/3456/3456464.png'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Spinning',
|
||||
description: 'Clase de alta intensidad de ciclismo estático',
|
||||
instructor: 'Juan Pérez',
|
||||
startTime: new Date('2025-04-24T10:00:00'),
|
||||
endTime: new Date('2025-04-24T11:00:00'),
|
||||
maxCapacity: 20,
|
||||
currentBookings: 15,
|
||||
category: 'Cardiovascular',
|
||||
imageUrl: 'https://cdn-icons-png.flaticon.com/512/805/805504.png'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Pilates (Pesas de agarre)',
|
||||
description: 'Fortalecimiento de core y flexibilidad',
|
||||
instructor: 'Ana García',
|
||||
startTime: new Date('2025-04-24T16:00:00'),
|
||||
endTime: new Date('2025-04-24T17:00:00'),
|
||||
maxCapacity: 12,
|
||||
currentBookings: 5,
|
||||
category: 'Mente y Cuerpo',
|
||||
imageUrl: 'https://cdn-icons-png.flaticon.com/512/625/625454.png'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Zumba',
|
||||
description: 'Baile y ejercicio cardiovascular',
|
||||
instructor: 'Carlos Martínez',
|
||||
startTime: new Date('2025-04-25T18:00:00'),
|
||||
endTime: new Date('2025-04-25T19:00:00'),
|
||||
maxCapacity: 25,
|
||||
currentBookings: 18,
|
||||
category: 'Baile',
|
||||
imageUrl: 'https://cdn-icons-png.flaticon.com/512/5776/5776440.png'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'CrossFit',
|
||||
description: 'Entrenamiento funcional de alta intensidad',
|
||||
instructor: 'Roberto Sánchez',
|
||||
startTime: new Date('2025-04-25T09:00:00'),
|
||||
endTime: new Date('2025-04-25T10:00:00'),
|
||||
maxCapacity: 15,
|
||||
currentBookings: 12,
|
||||
category: 'Fuerza',
|
||||
imageUrl: 'https://cdn-icons-png.flaticon.com/512/372/372612.png'
|
||||
}
|
||||
];
|
||||
|
||||
constructor(private storageService: StorageService) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
private async init() {
|
||||
if (this.initialized) return;
|
||||
|
||||
// Intentar cargar datos desde almacenamiento
|
||||
const storedClasses = await this.storageService.get(this.STORAGE_KEY);
|
||||
|
||||
if (storedClasses && storedClasses.length > 0) {
|
||||
// Convertir las fechas de string a objetos Date
|
||||
const classes = storedClasses.map((cls: any) => ({
|
||||
...cls,
|
||||
startTime: new Date(cls.startTime),
|
||||
endTime: new Date(cls.endTime)
|
||||
}));
|
||||
this.classesSubject.next(classes);
|
||||
} else {
|
||||
// Si no hay datos almacenados, usar datos iniciales
|
||||
await this.storageService.set(this.STORAGE_KEY, this.initialClasses);
|
||||
this.classesSubject.next(this.initialClasses);
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
getClasses(): Observable<GymClass[]> {
|
||||
return from(this.ensureInitialized()).pipe(
|
||||
switchMap(() => this.classes$)
|
||||
);
|
||||
}
|
||||
|
||||
getClassById(id: string): Observable<GymClass | undefined> {
|
||||
return this.getClasses().pipe(
|
||||
map(classes => classes.find(c => c.id === id))
|
||||
);
|
||||
}
|
||||
|
||||
updateClassBookings(classId: string, change: number): Observable<boolean> {
|
||||
return from(this.ensureInitialized()).pipe(
|
||||
switchMap(() => {
|
||||
const currentClasses = this.classesSubject.value;
|
||||
const index = currentClasses.findIndex(c => c.id === classId);
|
||||
|
||||
if (index === -1) return of(false);
|
||||
|
||||
const updatedClass = { ...currentClasses[index] };
|
||||
updatedClass.currentBookings += change;
|
||||
|
||||
// Verificar límites
|
||||
if (updatedClass.currentBookings < 0) {
|
||||
updatedClass.currentBookings = 0;
|
||||
}
|
||||
|
||||
if (updatedClass.currentBookings > updatedClass.maxCapacity) {
|
||||
return of(false);
|
||||
}
|
||||
|
||||
const updatedClasses = [...currentClasses];
|
||||
updatedClasses[index] = updatedClass;
|
||||
|
||||
return from(this.storageService.set(this.STORAGE_KEY, updatedClasses)).pipe(
|
||||
tap(() => this.classesSubject.next(updatedClasses)),
|
||||
map(() => true)
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (!this.initialized) {
|
||||
await this.init();
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/app/services/storage.service.spec.ts
Normal file
16
src/app/services/storage.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
describe('StorageService', () => {
|
||||
let service: StorageService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(StorageService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
35
src/app/services/storage.service.ts
Normal file
35
src/app/services/storage.service.ts
Normal file
@ -0,0 +1,35 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { Tab1Page } from './tab1.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Tab1Page,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class Tab1PageRoutingModule {}
|
||||
@ -1,20 +0,0 @@
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Tab1Page } from './tab1.page';
|
||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
||||
|
||||
import { Tab1PageRoutingModule } from './tab1-routing.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
IonicModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ExploreContainerComponentModule,
|
||||
Tab1PageRoutingModule
|
||||
],
|
||||
declarations: [Tab1Page]
|
||||
})
|
||||
export class Tab1PageModule {}
|
||||
@ -1,17 +0,0 @@
|
||||
<ion-header [translucent]="true">
|
||||
<ion-toolbar>
|
||||
<ion-title>
|
||||
Tab 1
|
||||
</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content [fullscreen]="true">
|
||||
<ion-header collapse="condense">
|
||||
<ion-toolbar>
|
||||
<ion-title size="large">Tab 1</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<app-explore-container name="Tab 1 page"></app-explore-container>
|
||||
</ion-content>
|
||||
@ -1,26 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
||||
|
||||
import { Tab1Page } from './tab1.page';
|
||||
|
||||
describe('Tab1Page', () => {
|
||||
let component: Tab1Page;
|
||||
let fixture: ComponentFixture<Tab1Page>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [Tab1Page],
|
||||
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Tab1Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tab1',
|
||||
templateUrl: 'tab1.page.html',
|
||||
styleUrls: ['tab1.page.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class Tab1Page {
|
||||
|
||||
constructor() {}
|
||||
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { Tab2Page } from './tab2.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Tab2Page,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class Tab2PageRoutingModule {}
|
||||
@ -1,20 +0,0 @@
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Tab2Page } from './tab2.page';
|
||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
||||
|
||||
import { Tab2PageRoutingModule } from './tab2-routing.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
IonicModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ExploreContainerComponentModule,
|
||||
Tab2PageRoutingModule
|
||||
],
|
||||
declarations: [Tab2Page]
|
||||
})
|
||||
export class Tab2PageModule {}
|
||||
@ -1,26 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
||||
|
||||
import { Tab2Page } from './tab2.page';
|
||||
|
||||
describe('Tab2Page', () => {
|
||||
let component: Tab2Page;
|
||||
let fixture: ComponentFixture<Tab2Page>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [Tab2Page],
|
||||
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Tab2Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tab2',
|
||||
templateUrl: 'tab2.page.html',
|
||||
styleUrls: ['tab2.page.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class Tab2Page {
|
||||
|
||||
constructor() {}
|
||||
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { Tab3Page } from './tab3.page';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: Tab3Page,
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class Tab3PageRoutingModule {}
|
||||
@ -1,20 +0,0 @@
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Tab3Page } from './tab3.page';
|
||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
||||
|
||||
import { Tab3PageRoutingModule } from './tab3-routing.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
IonicModule,
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ExploreContainerComponentModule,
|
||||
Tab3PageRoutingModule
|
||||
],
|
||||
declarations: [Tab3Page]
|
||||
})
|
||||
export class Tab3PageModule {}
|
||||
@ -1,26 +0,0 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
|
||||
import { ExploreContainerComponentModule } from '../explore-container/explore-container.module';
|
||||
|
||||
import { Tab3Page } from './tab3.page';
|
||||
|
||||
describe('Tab3Page', () => {
|
||||
let component: Tab3Page;
|
||||
let fixture: ComponentFixture<Tab3Page>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [Tab3Page],
|
||||
imports: [IonicModule.forRoot(), ExploreContainerComponentModule]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Tab3Page);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-tab3',
|
||||
templateUrl: 'tab3.page.html',
|
||||
styleUrls: ['tab3.page.scss'],
|
||||
standalone: false,
|
||||
})
|
||||
export class Tab3Page {
|
||||
|
||||
constructor() {}
|
||||
|
||||
}
|
||||
@ -8,27 +8,27 @@ const routes: Routes = [
|
||||
component: TabsPage,
|
||||
children: [
|
||||
{
|
||||
path: 'tab1',
|
||||
loadChildren: () => import('../tab1/tab1.module').then(m => m.Tab1PageModule)
|
||||
path: 'classes',
|
||||
loadChildren: () => import('../pages/classes/classes.module').then(m => m.ClassesPageModule)
|
||||
},
|
||||
{
|
||||
path: 'tab2',
|
||||
loadChildren: () => import('../tab2/tab2.module').then(m => m.Tab2PageModule)
|
||||
path: 'bookings',
|
||||
loadChildren: () => import('../pages/bookings/bookings.module').then(m => m.BookingsPageModule)
|
||||
},
|
||||
{
|
||||
path: 'tab3',
|
||||
loadChildren: () => import('../tab3/tab3.module').then(m => m.Tab3PageModule)
|
||||
path: 'profile',
|
||||
loadChildren: () => import('../pages/profile/profile.module').then(m => m.ProfilePageModule)
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/tabs/tab1',
|
||||
redirectTo: '/tabs/classes',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: '/tabs/tab1',
|
||||
redirectTo: '/tabs/classes',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
||||
@ -36,4 +36,4 @@ const routes: Routes = [
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
})
|
||||
export class TabsPageRoutingModule {}
|
||||
export class TabsPageRoutingModule {}
|
||||
@ -1,20 +1,18 @@
|
||||
<ion-tabs>
|
||||
|
||||
<ion-tab-bar slot="bottom">
|
||||
<ion-tab-button tab="tab1" href="/tabs/tab1">
|
||||
<ion-icon aria-hidden="true" name="triangle"></ion-icon>
|
||||
<ion-label>Tab 1</ion-label>
|
||||
<ion-tab-button tab="classes">
|
||||
<ion-icon name="calendar"></ion-icon>
|
||||
<ion-label>Clases</ion-label>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="tab2" href="/tabs/tab2">
|
||||
<ion-icon aria-hidden="true" name="ellipse"></ion-icon>
|
||||
<ion-label>Tab 2</ion-label>
|
||||
|
||||
<ion-tab-button tab="bookings">
|
||||
<ion-icon name="bookmark"></ion-icon>
|
||||
<ion-label>Mis Reservas</ion-label>
|
||||
</ion-tab-button>
|
||||
|
||||
<ion-tab-button tab="tab3" href="/tabs/tab3">
|
||||
<ion-icon aria-hidden="true" name="square"></ion-icon>
|
||||
<ion-label>Tab 3</ion-label>
|
||||
|
||||
<ion-tab-button tab="profile">
|
||||
<ion-icon name="person"></ion-icon>
|
||||
<ion-label>Perfil</ion-label>
|
||||
</ion-tab-button>
|
||||
</ion-tab-bar>
|
||||
|
||||
</ion-tabs>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user