feat: Mejoras en formulario hero, estilos globales y OneSignal
- Formulario hero: validación por campo con estado touched/submitted, mensajes de error inline, máscara de moneda en cuota, autocomplete flotante para dirección y categorías con clearInput, chips para categorías y palabras clave, campos banco/CLABE/RFC - Estilos globales: border-radius y sombra en ion-button, borde para botones light sobre fondo claro, sin borde en toolbars de color - OneSignal: reescritura del servicio con login/logout, addTag y routing por título de notificación usando NgZone - FAQ: color primary en acordeón activo/desplegado Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
ion-accordion.accordion-expanding ion-item[slot='header'],
|
ion-accordion.accordion-expanding ion-item[slot='header'],
|
||||||
ion-accordion.accordion-expanded ion-item[slot='header'] {
|
ion-accordion.accordion-expanded ion-item[slot='header'] {
|
||||||
--background: var(--ion-color-primary);
|
--ion-color-base: var(--ion-color-primary) !important;
|
||||||
--color: var(--ion-color-primary-contrast);
|
--ion-color-contrast: var(--ion-color-primary-contrast) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,88 +9,124 @@
|
|||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content padding>
|
<ion-content class="ion-padding">
|
||||||
<ion-item>
|
<ion-item [class.ion-invalid]="(submitted || touched['name']) && !name" [class.ion-touched]="submitted || touched['name']">
|
||||||
<ion-label position="floating">{{'hero.name' | translate}}</ion-label>
|
<ion-label position="floating">{{'hero.name' | translate}}</ion-label>
|
||||||
<ion-input [(ngModel)]="name"></ion-input>
|
<ion-input [(ngModel)]="name" (ionBlur)="markTouched('name')"></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<br>
|
<span class="error-msg" *ngIf="(submitted || touched['name']) && !name">{{'hero.required' | translate}}</span>
|
||||||
|
|
||||||
|
<ion-item [class.ion-invalid]="(submitted || touched['rfc']) && !rfc" [class.ion-touched]="submitted || touched['rfc']">
|
||||||
|
<ion-label position="floating">RFC</ion-label>
|
||||||
|
<ion-input [(ngModel)]="rfc" (ionBlur)="markTouched('rfc')"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<span class="error-msg" *ngIf="(submitted || touched['rfc']) && !rfc">{{'hero.required' | translate}}</span>
|
||||||
|
|
||||||
<!-- CATEGORÍAS CON CHIPS -->
|
<!-- CATEGORÍAS CON CHIPS -->
|
||||||
<ion-label class="chip-label">{{'hero.categories' | translate}}</ion-label>
|
<div class="dropdown-container">
|
||||||
<div class="chips-container">
|
<ion-item class="tags-item" [class.ion-invalid]="(submitted || touched['categories']) && categories_input.length === 0" [class.ion-touched]="submitted || touched['categories']">
|
||||||
|
<div class="tags-row">
|
||||||
|
<span class="chip-section-label">{{'hero.categories' | translate}}</span>
|
||||||
<ion-chip *ngFor="let cat of categories_input" color="primary">
|
<ion-chip *ngFor="let cat of categories_input" color="primary">
|
||||||
<ion-label>{{cat}}</ion-label>
|
<ion-label>{{cat}}</ion-label>
|
||||||
<ion-icon name="close-circle" (click)="removeCategory(cat)"></ion-icon>
|
<ion-icon name="close-circle" (click)="removeCategory(cat)"></ion-icon>
|
||||||
</ion-chip>
|
</ion-chip>
|
||||||
</div>
|
|
||||||
<ion-item>
|
|
||||||
<ion-input
|
<ion-input
|
||||||
[(ngModel)]="categorySearchText"
|
[(ngModel)]="categorySearchText"
|
||||||
(ionInput)="filterCategories($event)"
|
(ionInput)="filterCategories($event)"
|
||||||
(ionFocus)="showCategoryList()"
|
|
||||||
(ionBlur)="hideCategoryList()"
|
(ionBlur)="hideCategoryList()"
|
||||||
|
[clearInput]="true"
|
||||||
placeholder="{{'hero.categories_placeholder' | translate}}">
|
placeholder="{{'hero.categories_placeholder' | translate}}">
|
||||||
</ion-input>
|
</ion-input>
|
||||||
|
</div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<span class="error-msg" *ngIf="(submitted || touched['categories']) && categories_input.length === 0">Selecciona al menos una categoría</span>
|
||||||
<ion-list *ngIf="showCategoryDropdown && filteredCategories.length > 0" class="dropdown-list">
|
<ion-list *ngIf="showCategoryDropdown && filteredCategories.length > 0" class="dropdown-list">
|
||||||
<ion-item *ngFor="let category of filteredCategories" button (click)="selectCategory(category)">
|
<ion-item *ngFor="let category of filteredCategories" button (click)="selectCategory(category)">
|
||||||
{{category}}
|
{{category}}
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<br>
|
</div>
|
||||||
|
|
||||||
<!-- PALABRAS CLAVE CON CHIPS -->
|
<!-- PALABRAS CLAVE CON CHIPS -->
|
||||||
<ion-label class="chip-label">{{'hero.keywords' | translate}}</ion-label>
|
<ion-item class="tags-item">
|
||||||
<div class="chips-container">
|
<div class="tags-row">
|
||||||
<ion-chip *ngFor="let keyword of keywords" color="secondary">
|
<span class="chip-section-label">{{'hero.keywords' | translate}}</span>
|
||||||
|
<ion-chip *ngFor="let keyword of keywords" color="primary">
|
||||||
<ion-label>{{keyword}}</ion-label>
|
<ion-label>{{keyword}}</ion-label>
|
||||||
<ion-icon name="close-circle" (click)="removeKeyword(keyword)"></ion-icon>
|
<ion-icon name="close-circle" (click)="removeKeyword(keyword)"></ion-icon>
|
||||||
</ion-chip>
|
</ion-chip>
|
||||||
</div>
|
|
||||||
<ion-item>
|
|
||||||
<ion-input
|
<ion-input
|
||||||
[(ngModel)]="keywordInput"
|
[(ngModel)]="keywordInput"
|
||||||
(keydown)="onKeywordKeydown($event)"
|
(keydown)="onKeywordKeydown($event)"
|
||||||
(ionBlur)="addKeyword($event)"
|
(ionBlur)="addKeyword($event)"
|
||||||
placeholder="{{'hero.keywords_placeholder' | translate}}">
|
placeholder="{{'hero.keywords_placeholder' | translate}}">
|
||||||
</ion-input>
|
</ion-input>
|
||||||
|
</div>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-note padding>{{'hero.keywords_hint' | translate}}</ion-note>
|
<ion-note class="ion-padding">{{'hero.keywords_hint' | translate}}</ion-note>
|
||||||
|
|
||||||
<ion-label color="light">_</ion-label>
|
<!-- DIRECCIÓN CON AUTOCOMPLETE FLOTANTE -->
|
||||||
|
<div class="dropdown-container">
|
||||||
<ion-item>
|
<ion-item [class.ion-invalid]="(submitted || touched['address']) && !myAddress" [class.ion-touched]="submitted || touched['address']">
|
||||||
<ion-label position="floating">Dirección</ion-label>
|
<ion-label position="floating">{{'categories.address' | translate}}</ion-label>
|
||||||
<ion-input [(ngModel)]="addressAutocomplete" (ionChange)="autocomplete($event)" value="{{ myAddress }}" (ionFocus)="showlist()"></ion-input>
|
<ion-input [(ngModel)]="addressAutocomplete" (ionInput)="autocomplete($event)" (ionFocus)="showlist()" (ionBlur)="blurAddressList()" [clearInput]="true"></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-list *ngIf="showif">
|
<span class="error-msg" *ngIf="(submitted || touched['address']) && !myAddress">{{'alerts.valid_address' | translate}}</span>
|
||||||
|
<ion-list *ngIf="showif && placesSearch?.length" class="dropdown-list">
|
||||||
<ion-item button=true (click)="geoloc(places.place_id, places.description, places.terms[1].value)" *ngFor="let places of placesSearch" class="place">
|
<ion-item button=true (click)="geoloc(places.place_id, places.description, places.terms[1].value)" *ngFor="let places of placesSearch" class="place">
|
||||||
{{ places.description }}
|
{{ places.description }}
|
||||||
</ion-item>
|
</ion-item>
|
||||||
</ion-list>
|
</ion-list>
|
||||||
<ion-item hidden>
|
</div>
|
||||||
|
|
||||||
|
<ion-item class="ion-hide">
|
||||||
<ion-label position="fixed">Latitud</ion-label>
|
<ion-label position="fixed">Latitud</ion-label>
|
||||||
<ion-input value="{{ myPosition.latitude }}" disabled></ion-input>
|
<ion-input value="{{ myPosition.latitude }}" disabled></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item hidden>
|
<ion-item class="ion-hide">
|
||||||
<ion-label position="fixed">Longitud</ion-label>
|
<ion-label position="fixed">Longitud</ion-label>
|
||||||
<ion-input value="{{ myPosition.longitude }}" disabled></ion-input>
|
<ion-input value="{{ myPosition.longitude }}" disabled></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item>
|
|
||||||
<ion-label>{{'hero.discover' | translate}}</ion-label>
|
<ion-item [class.ion-invalid]="(submitted || touched['bank']) && selectedBank === null" [class.ion-touched]="submitted || touched['bank']">
|
||||||
<ion-select value={selectedReference} okText="Aceptar" cancelText="Cancelar" (ionChange)="selected_reference($event)">
|
<ion-label>{{'hero.bank' | translate}}</ion-label>
|
||||||
<ion-select-option value="1">{{'hero.radio' | translate}}</ion-select-option>
|
<ion-select [(ngModel)]="selectedBank" placeholder="{{'hero.select' | translate}}" okText="Aceptar" cancelText="Cancelar" (ionBlur)="markTouched('bank')">
|
||||||
<ion-select-option value="2">{{'hero.tv' | translate}}</ion-select-option>
|
<ion-select-option *ngFor="let bank of banks" [value]="bank.id">{{bank.name}}</ion-select-option>
|
||||||
<ion-select-option value="3">{{'hero.social' | translate}}</ion-select-option>
|
|
||||||
<ion-select-option value="4">{{'hero.friends' | translate}}</ion-select-option>
|
|
||||||
<ion-select-option value="5">{{'hero.other' | translate}}</ion-select-option>
|
|
||||||
</ion-select>
|
</ion-select>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
<ion-item *ngIf="showinput">
|
<span class="error-msg" *ngIf="(submitted || touched['bank']) && selectedBank === null">{{'hero.required' | translate}}</span>
|
||||||
<ion-label position="floating">{{'hero.discover_details' | translate}}</ion-label>
|
|
||||||
<ion-input [(ngModel)]="reference"></ion-input>
|
<ion-item [class.ion-invalid]="(submitted || touched['bankAccount']) && !bankAccount" [class.ion-touched]="submitted || touched['bankAccount']">
|
||||||
|
<ion-label position="floating">{{'hero.bank_account' | translate}}</ion-label>
|
||||||
|
<ion-input [(ngModel)]="bankAccount" type="number" (ionBlur)="markTouched('bankAccount')"></ion-input>
|
||||||
</ion-item>
|
</ion-item>
|
||||||
|
<span class="error-msg" *ngIf="(submitted || touched['bankAccount']) && !bankAccount">{{'hero.required' | translate}}</span>
|
||||||
|
|
||||||
|
<ion-item [class.ion-invalid]="(submitted || touched['fee']) && fee === null" [class.ion-touched]="submitted || touched['fee']">
|
||||||
|
<ion-label position="floating">{{'hero.fee' | translate}}</ion-label>
|
||||||
|
<ion-input [(ngModel)]="feeDisplay" (ionInput)="onFeeInput($event)" (ionBlur)="markTouched('fee')" type="text" inputmode="decimal"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<span class="error-msg" *ngIf="(submitted || touched['fee']) && fee === null">{{'hero.required' | translate}}</span>
|
||||||
|
|
||||||
|
<ion-item [class.ion-invalid]="(submitted || touched['reference']) && !selectedReference" [class.ion-touched]="submitted || touched['reference']">
|
||||||
|
<ion-label>{{'hero.discover' | translate}}</ion-label>
|
||||||
|
<ion-select [(ngModel)]="selectedReference" placeholder="{{'hero.select' | translate}}" okText="Aceptar" cancelText="Cancelar" (ionChange)="selected_reference($event)" (ionBlur)="markTouched('reference')">
|
||||||
|
<ion-select-option [value]="1">{{'hero.radio' | translate}}</ion-select-option>
|
||||||
|
<ion-select-option [value]="2">{{'hero.tv' | translate}}</ion-select-option>
|
||||||
|
<ion-select-option [value]="3">{{'hero.social' | translate}}</ion-select-option>
|
||||||
|
<ion-select-option [value]="4">{{'hero.friends' | translate}}</ion-select-option>
|
||||||
|
<ion-select-option [value]="5">{{'hero.other' | translate}}</ion-select-option>
|
||||||
|
</ion-select>
|
||||||
|
</ion-item>
|
||||||
|
<span class="error-msg" *ngIf="(submitted || touched['reference']) && !selectedReference">{{'hero.required' | translate}}</span>
|
||||||
|
|
||||||
|
<ion-item *ngIf="showinput" [class.ion-invalid]="(submitted || touched['referenceDetail']) && !reference" [class.ion-touched]="submitted || touched['referenceDetail']">
|
||||||
|
<ion-label position="floating">{{'hero.discover_details' | translate}}</ion-label>
|
||||||
|
<ion-input [(ngModel)]="reference" (ionBlur)="markTouched('referenceDetail')"></ion-input>
|
||||||
|
</ion-item>
|
||||||
|
<span class="error-msg" *ngIf="(submitted || touched['referenceDetail']) && showinput && !reference">{{'hero.required' | translate}}</span>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<ion-button type="submit" expand="full" color="secondary" (click)="addHero()">{{'hero.signup' | translate}}</ion-button>
|
<ion-button type="submit" expand="full" color="secondary" (click)="addHero()">{{'hero.signup' | translate}}</ion-button>
|
||||||
|
|||||||
@@ -1,21 +1,28 @@
|
|||||||
.chip-label {
|
.tags-item {
|
||||||
display: block;
|
--inner-padding-end: 8px;
|
||||||
padding: 10px 16px 5px;
|
--padding-top: 4px;
|
||||||
font-size: 14px;
|
--padding-bottom: 4px;
|
||||||
font-weight: 500;
|
|
||||||
color: var(--ion-color-medium);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chips-container {
|
.chip-section-label {
|
||||||
|
flex-basis: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
padding-top: 8px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 5px 10px;
|
align-items: center;
|
||||||
min-height: 20px;
|
gap: 4px;
|
||||||
gap: 5px;
|
padding: 0 0 6px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
ion-chip {
|
ion-chip {
|
||||||
margin: 2px;
|
margin: 0;
|
||||||
height: 32px;
|
height: 30px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
ion-label {
|
ion-label {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -26,16 +33,29 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-input {
|
||||||
|
min-width: 120px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-container {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-list {
|
.dropdown-list {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 100;
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 1px solid var(--ion-color-light-shade);
|
border: 1px solid var(--ion-color-light-shade);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
margin: 0 16px;
|
background: var(--ion-background-color, #fff);
|
||||||
background: var(--ion-background-color);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
||||||
|
|
||||||
ion-item {
|
ion-item {
|
||||||
--min-height: 40px;
|
--min-height: 40px;
|
||||||
@@ -48,6 +68,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ion-item.ion-invalid.ion-touched {
|
||||||
|
--border-color: var(--ion-color-danger);
|
||||||
|
--highlight-color-invalid: var(--ion-color-danger);
|
||||||
|
|
||||||
|
ion-label {
|
||||||
|
color: var(--ion-color-danger) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-msg {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ion-color-danger);
|
||||||
|
padding: 2px 16px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
ion-chip[color="primary"] {
|
ion-chip[color="primary"] {
|
||||||
--background: var(--ion-color-primary);
|
--background: var(--ion-color-primary);
|
||||||
--color: var(--ion-color-primary-contrast);
|
--color: var(--ion-color-primary-contrast);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Component, OnInit, NgZone } from '@angular/core';
|
import { Component, OnInit, NgZone } from '@angular/core';
|
||||||
import { NavController, LoadingController } from '@ionic/angular';
|
import { NavController, LoadingController } from '@ionic/angular';
|
||||||
import { AuthService } from 'src/app/services/auth.service';
|
import { AuthService } from 'src/app/services/auth.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { LanguageService } from 'src/app/services/language.service';
|
||||||
import { IchambaService } from 'src/app/services/ichamba.service';
|
import { IchambaService } from 'src/app/services/ichamba.service';
|
||||||
import { AlertService } from 'src/app/services/alert.service';
|
import { AlertService } from 'src/app/services/alert.service';
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ declare var google: any;
|
|||||||
selector: 'app-hero',
|
selector: 'app-hero',
|
||||||
templateUrl: './hero.page.html',
|
templateUrl: './hero.page.html',
|
||||||
styleUrls: ['./hero.page.scss'],
|
styleUrls: ['./hero.page.scss'],
|
||||||
|
standalone: false
|
||||||
})
|
})
|
||||||
export class HeroPage implements OnInit {
|
export class HeroPage implements OnInit {
|
||||||
|
|
||||||
@@ -25,19 +28,29 @@ export class HeroPage implements OnInit {
|
|||||||
myPosition: any = {};
|
myPosition: any = {};
|
||||||
myAddress: string | null = null;
|
myAddress: string | null = null;
|
||||||
myIntnumber: string | null = null;
|
myIntnumber: string | null = null;
|
||||||
|
banks: any[] = [];
|
||||||
reference: string | null = null;
|
reference: string | null = null;
|
||||||
name: string | null = null;
|
name: string | null = null;
|
||||||
|
rfc: string | null = null;
|
||||||
|
selectedBank: number | null = null;
|
||||||
|
bankAccount: number | null = null;
|
||||||
|
fee: number | null = null;
|
||||||
|
feeDisplay: string = '';
|
||||||
selectedReference: number = 0;
|
selectedReference: number = 0;
|
||||||
addressAutocomplete: string = '';
|
addressAutocomplete: string = '';
|
||||||
placesSearch: any = '';
|
placesSearch: any = '';
|
||||||
showinput: boolean = false;
|
showinput: boolean = false;
|
||||||
showif = true;
|
showif = true;
|
||||||
|
submitted: boolean = false;
|
||||||
|
touched: { [key: string]: boolean } = {};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private navCtrl: NavController,
|
private navCtrl: NavController,
|
||||||
private loadingCtrl: LoadingController,
|
private loadingCtrl: LoadingController,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private alertService: AlertService,
|
private alertService: AlertService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private languageService: LanguageService,
|
||||||
private ichambaService: IchambaService,
|
private ichambaService: IchambaService,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
) { }
|
) { }
|
||||||
@@ -48,6 +61,10 @@ export class HeroPage implements OnInit {
|
|||||||
this.categories = categories;
|
this.categories = categories;
|
||||||
this.filteredCategories = categories;
|
this.filteredCategories = categories;
|
||||||
})
|
})
|
||||||
|
this.ichambaService.getBanks()
|
||||||
|
.subscribe( banks => {
|
||||||
|
this.banks = banks;
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== CATEGORÍAS CON CHIPS ==========
|
// ========== CATEGORÍAS CON CHIPS ==========
|
||||||
@@ -82,19 +99,16 @@ export class HeroPage implements OnInit {
|
|||||||
this.filteredCategories = this.categories.filter(cat => !this.categories_input.includes(cat));
|
this.filteredCategories = this.categories.filter(cat => !this.categories_input.includes(cat));
|
||||||
}
|
}
|
||||||
|
|
||||||
showCategoryList() {
|
|
||||||
this.filteredCategories = this.categories.filter(cat => !this.categories_input.includes(cat));
|
|
||||||
this.showCategoryDropdown = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideCategoryList() {
|
hideCategoryList() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.showCategoryDropdown = false;
|
this.showCategoryDropdown = false;
|
||||||
|
this.touched['categories'] = true;
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== PALABRAS CLAVE CON CHIPS ==========
|
// ========== PALABRAS CLAVE CON CHIPS ==========
|
||||||
addKeyword(event?: any) {
|
addKeyword(event?: any) {
|
||||||
|
this.touched['keywords'] = true;
|
||||||
const value = this.keywordInput?.trim();
|
const value = this.keywordInput?.trim();
|
||||||
if (value && value.length > 0) {
|
if (value && value.length > 0) {
|
||||||
// Separar por comas si hay varias palabras
|
// Separar por comas si hay varias palabras
|
||||||
@@ -112,6 +126,8 @@ export class HeroPage implements OnInit {
|
|||||||
if (event.key === 'Enter' || event.key === ',') {
|
if (event.key === 'Enter' || event.key === ',') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.addKeyword();
|
this.addKeyword();
|
||||||
|
} else if (event.key === 'Backspace' && !this.keywordInput && this.keywords.length > 0) {
|
||||||
|
this.keywords.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,92 +139,105 @@ export class HeroPage implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dismissHero() {
|
dismissHero() {
|
||||||
this.navCtrl.navigateRoot('/landing');
|
this.navCtrl.navigateRoot('/dashboard');
|
||||||
}
|
}
|
||||||
|
|
||||||
autocomplete(ev: any) {
|
autocomplete(ev: any) {
|
||||||
if (!this.addressAutocomplete.trim().length) {
|
const value = (ev.detail?.value ?? this.addressAutocomplete).trim();
|
||||||
|
this.myAddress = null;
|
||||||
|
if (!value.length) {
|
||||||
this.placesSearch = null;
|
this.placesSearch = null;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
console.log(this.addressAutocomplete)
|
new google.maps.places.AutocompleteService().getPredictions({ input: value }, (predictions: any) => {
|
||||||
new google.maps.places.AutocompleteService().getPredictions({ input: this.addressAutocomplete }, predictions => {
|
|
||||||
this.ngZone.run(() => {
|
this.ngZone.run(() => {
|
||||||
this.placesSearch = predictions;
|
this.placesSearch = predictions;
|
||||||
console.log(predictions);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
geoloc(place_id: string, place_description: string, place_intnumber: string) {
|
geoloc(place_id: string, place_description: string, place_intnumber: string) {
|
||||||
this.myAddress = place_description;
|
this.myAddress = place_description;
|
||||||
|
this.addressAutocomplete = place_description;
|
||||||
this.myIntnumber = place_intnumber;
|
this.myIntnumber = place_intnumber;
|
||||||
console.log(place_id);
|
this.placesSearch = null;
|
||||||
this.hidelist();
|
this.hidelist();
|
||||||
new google.maps.Geocoder().geocode({ placeId: place_id }, coordinates => {
|
new google.maps.Geocoder().geocode({ placeId: place_id }, (coordinates: any) => {
|
||||||
this.ngZone.run(() => {
|
this.ngZone.run(() => {
|
||||||
console.log(coordinates[0].geometry.location.lat() + ", " + coordinates[0].geometry.location.lng());
|
const result = coordinates[0];
|
||||||
this.myPosition = {
|
this.myPosition = {
|
||||||
latitude: coordinates[0].geometry.location.lat(),
|
latitude: result.geometry.location.lat(),
|
||||||
longitude: coordinates[0].geometry.location.lng()
|
longitude: result.geometry.location.lng()
|
||||||
}
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
selected_reference(ev: any) {
|
selected_reference(ev: any) {
|
||||||
console.log(ev.target.value);
|
this.showinput = ev.detail.value === 5;
|
||||||
this.selectedReference = ev.target.value;
|
|
||||||
if (this.selectedReference == 5) {
|
|
||||||
this.showinput = true;
|
|
||||||
} else {
|
|
||||||
this.showinput = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFeeInput(ev: any) {
|
||||||
|
const raw = (ev.detail?.value ?? '').replace(/[^0-9.]/g, '');
|
||||||
|
const parts = raw.split('.');
|
||||||
|
const integer = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
const decimal = parts.length > 1 ? '.' + parts[1].slice(0, 2) : '';
|
||||||
|
this.feeDisplay = raw ? `$${integer}${decimal}` : '';
|
||||||
|
this.fee = raw ? parseFloat(raw) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
addHero(){
|
addHero(){
|
||||||
// Preparar categorías y keywords
|
this.submitted = true;
|
||||||
const categoriesString = this.categories_input.join(',');
|
const categoriesString = this.categories_input.join(',');
|
||||||
const keywordsString = this.keywords.length > 0 ? this.keywords.join(', ') : null;
|
const keywordsString = this.keywords.length > 0 ? this.keywords.join(', ') : '';
|
||||||
|
|
||||||
if (this.name && this.categories_input.length > 0 && this.myAddress && this.myPosition.latitude && this.myPosition.longitude && this.selectedReference){
|
if (
|
||||||
if (this.selectedReference == 5 && this.reference) {
|
this.name &&
|
||||||
this.loadingCtrl.create().then((overlay) => {
|
this.rfc &&
|
||||||
this.loading = overlay;
|
this.categories_input.length > 0 &&
|
||||||
this.loading.present();
|
this.myAddress &&
|
||||||
});
|
this.myPosition.latitude &&
|
||||||
|
this.myPosition.longitude &&
|
||||||
this.ichambaService.addHero(this.name, categoriesString, keywordsString, this.myAddress, this.myPosition.latitude, this.myPosition.longitude, this.selectedReference, this.reference).subscribe(
|
this.selectedBank !== null &&
|
||||||
data => {
|
this.bankAccount &&
|
||||||
this.loading.dismiss();
|
this.fee !== null &&
|
||||||
this.alertService.presentToast(data['message']);
|
this.selectedReference
|
||||||
this.navCtrl.navigateRoot('/dashboard');
|
) {
|
||||||
}, error => {
|
if (this.selectedReference === 5 && !this.reference) {
|
||||||
this.loading.dismiss();
|
|
||||||
this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']);
|
|
||||||
});
|
|
||||||
} else if (this.selectedReference !== 5) {
|
|
||||||
this.loadingCtrl.create().then((overlay) => {
|
|
||||||
this.loading = overlay;
|
|
||||||
this.loading.present();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ichambaService.addHero(this.name, categoriesString, keywordsString, this.myAddress, this.myPosition.latitude, this.myPosition.longitude, this.selectedReference, this.reference).subscribe(
|
|
||||||
data => {
|
|
||||||
this.loading.dismiss();
|
|
||||||
this.alertService.presentToast(data['message']);
|
|
||||||
this.navCtrl.navigateRoot('/dashboard');
|
|
||||||
}, error => {
|
|
||||||
this.loading.dismiss();
|
|
||||||
this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.alertService.presentToast("Por favor, específique cómo supo de nosotros");
|
this.alertService.presentToast("Por favor, específique cómo supo de nosotros");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadingCtrl.create().then((overlay) => {
|
||||||
|
this.loading = overlay;
|
||||||
|
this.loading.present();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ichambaService.addHero(this.name!, this.rfc!, categoriesString, keywordsString, this.myAddress!, this.myPosition.latitude, this.myPosition.longitude, this.selectedBank ?? 0, this.bankAccount ?? 0, this.fee ?? 0, this.selectedReference, this.reference ?? '').subscribe(
|
||||||
|
(data: any) => {
|
||||||
|
if (this.loading) this.loading.dismiss();
|
||||||
|
this.alertService.presentToast(data['message']);
|
||||||
|
this.navCtrl.navigateRoot('/dashboard');
|
||||||
|
}, (error: any) => {
|
||||||
|
if (this.loading) this.loading.dismiss();
|
||||||
|
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.alertService.presentToast("Llene todos los datos solicitados");
|
this.alertService.presentToast("Llene todos los datos solicitados");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markTouched(field: string) {
|
||||||
|
this.touched[field] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
blurAddressList() {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hidelist();
|
||||||
|
this.touched['address'] = true;
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
showlist() {
|
showlist() {
|
||||||
this.showif = true;
|
this.showif = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, NgZone } from '@angular/core';
|
||||||
import { Capacitor } from '@capacitor/core';
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
import { NavController } from '@ionic/angular';
|
||||||
import { EnvService } from './env.service';
|
import { EnvService } from './env.service';
|
||||||
import { Router } from '@angular/router';
|
import { AlertService } from './alert.service';
|
||||||
|
import { EventService } from './event.service';
|
||||||
|
|
||||||
declare var OneSignalPlugin: any;
|
declare var OneSignalPlugin: any;
|
||||||
|
|
||||||
@@ -13,78 +15,78 @@ export class OneSignalService {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private env: EnvService,
|
private env: EnvService,
|
||||||
private router: Router
|
private navCtrl: NavController,
|
||||||
|
private alertService: AlertService,
|
||||||
|
private events: EventService,
|
||||||
|
private ngZone: NgZone,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async init(): Promise<void> {
|
async init(): Promise<void> {
|
||||||
if (this.initialized || !Capacitor.isNativePlatform()) {
|
if (this.initialized || !Capacitor.isNativePlatform()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Initialize OneSignal
|
|
||||||
OneSignalPlugin.initialize(this.env.ONESIGNAL_APP_ID);
|
OneSignalPlugin.initialize(this.env.ONESIGNAL_APP_ID);
|
||||||
|
|
||||||
// Request notification permission
|
|
||||||
OneSignalPlugin.Notifications.requestPermission(true).then((accepted: boolean) => {
|
OneSignalPlugin.Notifications.requestPermission(true).then((accepted: boolean) => {
|
||||||
console.log('OneSignal notification permission:', accepted ? 'accepted' : 'denied');
|
console.log('OneSignal permission:', accepted ? 'accepted' : 'denied');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle notification clicks
|
// Foreground: show alert + navigate
|
||||||
OneSignalPlugin.Notifications.addEventListener('click', (event: any) => {
|
|
||||||
console.log('OneSignal notification clicked:', event);
|
|
||||||
this.handleNotificationClick(event);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle foreground notifications
|
|
||||||
OneSignalPlugin.Notifications.addEventListener('foregroundWillDisplay', (event: any) => {
|
OneSignalPlugin.Notifications.addEventListener('foregroundWillDisplay', (event: any) => {
|
||||||
console.log('OneSignal notification received in foreground:', event);
|
const notification = event.getNotification();
|
||||||
// Display the notification
|
notification.display();
|
||||||
event.getNotification().display();
|
this.ngZone.run(() => {
|
||||||
|
this.alertService.presentAlert(notification.title, notification.body, ['OK']);
|
||||||
|
this.navigateByTitle(notification.title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tap on notification: navigate only
|
||||||
|
OneSignalPlugin.Notifications.addEventListener('click', (event: any) => {
|
||||||
|
const title = event.notification?.title;
|
||||||
|
this.ngZone.run(() => this.navigateByTitle(title));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
console.log('OneSignal initialized successfully');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error initializing OneSignal:', error);
|
console.error('Error initializing OneSignal:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUserId(userId: number | string): Promise<void> {
|
private navigateByTitle(title: string): void {
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (title === 'Proveedor: hay nueva postulación' || title === 'Hero: there is a new postulation') {
|
||||||
return;
|
this.navCtrl.navigateRoot('/postulations');
|
||||||
|
this.events.publish('refreshpostulations', 'data');
|
||||||
|
} else if (title === 'Búsqueda Finalizada' || title === 'Search finished') {
|
||||||
|
this.navCtrl.navigateRoot('/contracts');
|
||||||
|
} else if (title === 'Usuario: el proveedor ha iniciado el servicio' || title === 'User: the Hero has started the service') {
|
||||||
|
this.navCtrl.navigateRoot('/contracts');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setUserId(userId: number | string): Promise<void> {
|
||||||
|
if (!Capacitor.isNativePlatform()) return;
|
||||||
try {
|
try {
|
||||||
// Set user tag for targeting
|
OneSignalPlugin.login(String(userId));
|
||||||
OneSignalPlugin.User.addTag('iChamba_ID', String(userId));
|
OneSignalPlugin.User.addTag('iChamba_ID', String(userId));
|
||||||
console.log('OneSignal user tag set:', userId);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting OneSignal user tag:', error);
|
console.error('Error setting OneSignal user:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUserRole(roleId: number | string): Promise<void> {
|
async setUserRole(roleId: number | string): Promise<void> {
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OneSignalPlugin.User.addTag('iChamba_Role', String(roleId));
|
OneSignalPlugin.User.addTag('iChamba_Role', String(roleId));
|
||||||
console.log('OneSignal role tag set:', roleId);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error setting OneSignal role tag:', error);
|
console.error('Error setting OneSignal role tag:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearTags(): Promise<void> {
|
async clearTags(): Promise<void> {
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
OneSignalPlugin.User.removeTags(['iChamba_ID', 'iChamba_Role']);
|
OneSignalPlugin.User.removeTags(['iChamba_ID', 'iChamba_Role']);
|
||||||
console.log('OneSignal tags cleared');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error clearing OneSignal tags:', error);
|
console.error('Error clearing OneSignal tags:', error);
|
||||||
}
|
}
|
||||||
@@ -92,38 +94,20 @@ export class OneSignalService {
|
|||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
await this.clearTags();
|
await this.clearTags();
|
||||||
}
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
try {
|
||||||
private handleNotificationClick(event: any): void {
|
OneSignalPlugin.logout();
|
||||||
const data = event?.notification?.additionalData;
|
} catch (error) {
|
||||||
|
console.error('Error logging out OneSignal:', error);
|
||||||
if (data) {
|
|
||||||
// Handle navigation based on notification data
|
|
||||||
if (data.route) {
|
|
||||||
this.router.navigate([data.route]);
|
|
||||||
} else if (data.type) {
|
|
||||||
switch (data.type) {
|
|
||||||
case 'contract':
|
|
||||||
this.router.navigate(['/contracts']);
|
|
||||||
break;
|
|
||||||
case 'postulation':
|
|
||||||
this.router.navigate(['/postulations']);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
this.router.navigate(['/dashboard']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlayerId(): Promise<string | null> {
|
async getPlayerId(): Promise<string | null> {
|
||||||
if (!Capacitor.isNativePlatform()) {
|
if (!Capacitor.isNativePlatform()) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const deviceState = await OneSignalPlugin.User.pushSubscription.getId();
|
const id = await OneSignalPlugin.User.pushSubscription.getId();
|
||||||
return deviceState || null;
|
return id || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting OneSignal player ID:', error);
|
console.error('Error getting OneSignal player ID:', error);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ ion-app {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Consistent button styling to match social login buttons
|
||||||
|
ion-button::part(native) {
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border for light-colored buttons so they don't blend into white backgrounds
|
||||||
|
ion-button[color="light"]::part(native) {
|
||||||
|
border: 1px solid #DADCE0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No border when light button is inside a colored toolbar
|
||||||
|
ion-toolbar:not([color="light"]):not([color=""]) ion-button[color="light"]::part(native) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
.rate_div {
|
.rate_div {
|
||||||
|
|
||||||
rating {
|
rating {
|
||||||
|
|||||||
Reference in New Issue
Block a user