From 48979dca6f545470fe1443696e6df10cc5992622 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 3 Jul 2026 11:26:48 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20Branch=20betos=20=E2=80=94=20propiedade?= =?UTF-8?q?s,=20redise=C3=B1o=20postulaciones,=20chat=20contratos=20y=20fl?= =?UTF-8?q?ujo=20cancelar/repostular?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sistema de propiedades: modal add-property con autocomplete y selector de ícono - category.page rediseñado: selector de propiedad, fotos (Capacitor Camera), sin fecha/precio - Modal postulate-offer: proveedor propone 2 fechas requeridas + monto con validación de cuota mínima - viewsuppliers: chips de ordenamiento frontend (fecha, precio, certificados) - hire.page: selección de fecha (date_1/date_2) antes de contratar; selected_date en contracts/create y contracts/coupon - Modal contract-discussion: chat de burbujas para contratos activos - contracted.page: botón de chat por contrato - pending.page: botones cancelar y repostular por postulación - Modal repostulate: pre-llena categoría/propiedad, archiva original como perdido y crea nueva con related_postulation_id - current.page: temporizador de expiración basado en time_created (1000 min), oculta expiradas - ichamba.service: nuevos métodos properties, cancelPostulation, contractDiscussion, updated signatures Co-Authored-By: Claude Sonnet 4.6 --- src/app/pages/category/category.module.ts | 13 +- src/app/pages/category/category.page.html | 98 +++---- src/app/pages/category/category.page.ts | 265 +++++------------- .../contract-discussion.module.ts | 12 + .../contract-discussion.page.html | 80 ++++++ .../contract-discussion.page.scss | 117 ++++++++ .../contract-discussion.page.ts | 76 +++++ .../contracts/contracted/contracted.module.ts | 13 +- .../contracts/contracted/contracted.page.html | 8 +- .../contracts/contracted/contracted.page.ts | 9 + src/app/pages/contracts/hire/hire.page.html | 160 ++++------- src/app/pages/contracts/hire/hire.page.ts | 253 +++++++---------- .../pages/contracts/pending/pending.module.ts | 6 +- .../pages/contracts/pending/pending.page.html | 14 +- .../pages/contracts/pending/pending.page.ts | 45 ++- .../viewsuppliers/viewsuppliers.page.html | 60 ++-- .../viewsuppliers/viewsuppliers.page.ts | 131 +++++---- .../postulations/current/current.module.ts | 11 +- .../postulations/current/current.page.html | 13 +- .../postulations/current/current.page.ts | 57 +++- .../postulate-offer/postulate-offer.module.ts | 12 + .../postulate-offer/postulate-offer.page.html | 48 ++++ .../postulate-offer/postulate-offer.page.scss | 0 .../postulate-offer/postulate-offer.page.ts | 48 ++++ .../repostulate/repostulate.module.ts | 13 + .../repostulate/repostulate.page.html | 70 +++++ .../repostulate/repostulate.page.scss | 0 .../repostulate/repostulate.page.ts | 130 +++++++++ .../add-property/add-property.module.ts | 12 + .../add-property/add-property.page.html | 63 +++++ .../add-property/add-property.page.scss | 0 .../add-property/add-property.page.ts | 100 +++++++ .../assign-technician.module.ts | 14 + .../assign-technician.page.scss | 0 .../assign-technician.page.ts | 62 ++++ src/app/pages/team/team.module.ts | 21 ++ src/app/pages/team/team.page.scss | 0 src/app/services/ichamba.service.ts | 117 +++++++- 38 files changed, 1534 insertions(+), 617 deletions(-) create mode 100644 src/app/pages/contracts/contract-discussion/contract-discussion.module.ts create mode 100644 src/app/pages/contracts/contract-discussion/contract-discussion.page.html create mode 100644 src/app/pages/contracts/contract-discussion/contract-discussion.page.scss create mode 100644 src/app/pages/contracts/contract-discussion/contract-discussion.page.ts create mode 100644 src/app/pages/postulations/postulate-offer/postulate-offer.module.ts create mode 100644 src/app/pages/postulations/postulate-offer/postulate-offer.page.html create mode 100644 src/app/pages/postulations/postulate-offer/postulate-offer.page.scss create mode 100644 src/app/pages/postulations/postulate-offer/postulate-offer.page.ts create mode 100644 src/app/pages/postulations/repostulate/repostulate.module.ts create mode 100644 src/app/pages/postulations/repostulate/repostulate.page.html create mode 100644 src/app/pages/postulations/repostulate/repostulate.page.scss create mode 100644 src/app/pages/postulations/repostulate/repostulate.page.ts create mode 100644 src/app/pages/property/add-property/add-property.module.ts create mode 100644 src/app/pages/property/add-property/add-property.page.html create mode 100644 src/app/pages/property/add-property/add-property.page.scss create mode 100644 src/app/pages/property/add-property/add-property.page.ts create mode 100644 src/app/pages/team/assign-technician/assign-technician.module.ts create mode 100644 src/app/pages/team/assign-technician/assign-technician.page.scss create mode 100644 src/app/pages/team/assign-technician/assign-technician.page.ts create mode 100644 src/app/pages/team/team.module.ts create mode 100644 src/app/pages/team/team.page.scss diff --git a/src/app/pages/category/category.module.ts b/src/app/pages/category/category.module.ts index bcc4520..a10a4fb 100755 --- a/src/app/pages/category/category.module.ts +++ b/src/app/pages/category/category.module.ts @@ -2,17 +2,13 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Routes, RouterModule } from '@angular/router'; - import { IonicModule } from '@ionic/angular'; - import { CategoryPage } from './category.page'; -import { TranslateModule } from '@ngx-translate/core' +import { TranslateModule } from '@ngx-translate/core'; +import { AddPropertyPageModule } from '../property/add-property/add-property.module'; const routes: Routes = [ - { - path: '', - component: CategoryPage - } + { path: '', component: CategoryPage } ]; @NgModule({ @@ -21,7 +17,8 @@ const routes: Routes = [ FormsModule, IonicModule, RouterModule.forChild(routes), - TranslateModule + TranslateModule, + AddPropertyPageModule, ], declarations: [CategoryPage] }) diff --git a/src/app/pages/category/category.page.html b/src/app/pages/category/category.page.html index a32d424..b09d7a2 100755 --- a/src/app/pages/category/category.page.html +++ b/src/app/pages/category/category.page.html @@ -1,64 +1,66 @@ - - {{'categories.header' | translate}} + Nueva postulación - + + + - {{'categories.category' | translate}} - + Categoría + + + - {{'categories.address' | translate}} - + Propiedad + + + {{ p.name }}: {{ p.address }} + + - - - {{ places.description }} - - + + + Agregar propiedad + + - {{'categories.int_number' | translate}} - + Referencias + + - {{'categories.references' | translate}} - + Comentarios + - - - - {{'categories.date' | translate}} - - - - {{'categories.time' | translate}} - - - - {{'categories.commentaries' | translate}} - - - {{'categories.add_postulation' | translate}} + + +
+ + Fotos (opcional, máx. 3) + +
+
+ + + + +
+ + + +
+
+ + + Crear postulación + +
diff --git a/src/app/pages/category/category.page.ts b/src/app/pages/category/category.page.ts index a57ed0c..1776364 100755 --- a/src/app/pages/category/category.page.ts +++ b/src/app/pages/category/category.page.ts @@ -1,13 +1,10 @@ -import { Component, OnInit, NgZone } from '@angular/core'; -import { NavController, LoadingController } from '@ionic/angular'; +import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; +import { NavController, LoadingController, ModalController } from '@ionic/angular'; import { ActivatedRoute } from '@angular/router'; -import { AuthService } from 'src/app/services/auth.service'; import { AlertService } from 'src/app/services/alert.service'; import { IchambaService } from 'src/app/services/ichamba.service'; -import { Geolocation } from '@capacitor/geolocation'; -import { TranslateService } from '@ngx-translate/core'; - -declare var google: any; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { AddPropertyPage } from '../property/add-property/add-property.page'; @Component({ selector: 'app-category', @@ -19,212 +16,102 @@ export class CategoryPage implements OnInit { private loading: any; category: any = null; - //money: number = null; - //amount: number = null; - //amount_check = true; - minyear: string = (new Date().getFullYear() + '-' + ((new Date().getMonth() + 1) < 10 ? '0' : '') + (new Date().getMonth() + 1) + '-' + (new Date().getDate() < 10 ? '0' : '') + new Date().getDate()).toString(); - maxyear: string | null = null; - mintime: number = 0; - maxtime: number = 0; - setHour: any; - setDate: any; references: string = ''; details: string = ''; - differenceDate: number = 0; - differenceHour: number = 0; - myPosition: any = {}; - myAddress: string | null = null; - myIntnumber: string | null = null; - addressAutocomplete: string = ''; - placesSearch: any = ''; - showif = true; + photos: string[] = []; + + properties: any[] = []; + selectedPropertyId: number | null = null; + loadingProperties = true; constructor( - private authService: AuthService, private navCtrl: NavController, private alertService: AlertService, private ichambaService: IchambaService, private activatedRoute: ActivatedRoute, - private translateService: TranslateService, - private ngZone: NgZone, - private loadingCtrl: LoadingController - ) { - console.log(google); - } + private loadingCtrl: LoadingController, + private modalCtrl: ModalController, + private cdr: ChangeDetectorRef, + ) {} ngOnInit() { this.category = this.activatedRoute.snapshot.paramMap.get('category'); - this.ichambaService.getParameters() - .subscribe( (data: any) => { - this.mintime = data['min_time']; - this.maxtime = data['max_time']; - }); - this.getCurrentLocation(); - console.log(this.minyear); - // Máximo 2 días de anticipación - const maxDate = new Date(); - maxDate.setDate(maxDate.getDate() + 2); - this.maxyear = maxDate.toISOString().slice(0, 10); - console.log(this.maxyear); + this.loadProperties(); } - async getCurrentLocation() { - try { - // Request permissions first - const permissionStatus = await Geolocation.checkPermissions(); - if (permissionStatus.location !== 'granted') { - await Geolocation.requestPermissions(); + loadProperties() { + this.loadingProperties = true; + this.ichambaService.getProperties().subscribe({ + next: (data: any[]) => { + this.properties = data; + this.loadingProperties = false; + this.cdr.detectChanges(); + if (data.length === 0) { + this.openAddProperty(); + } + }, + error: () => { + this.loadingProperties = false; + this.cdr.detectChanges(); } - - const position = await Geolocation.getCurrentPosition({ - enableHighAccuracy: true, - timeout: 30000 - }); - - console.log(position); - this.myPosition = { - latitude: position.coords.latitude, - longitude: position.coords.longitude - }; - this.doGeocode(); - } catch (error) { - console.log(this.translateService.instant('alerts.geo_error') + error); - } - } - - doGeocode() { - // Use Google Maps JavaScript API for geocoding - const geocoder = new google.maps.Geocoder(); - const latlng = { lat: this.myPosition.latitude, lng: this.myPosition.longitude }; - - geocoder.geocode({ location: latlng }, (results: any, status: any) => { - this.ngZone.run(() => { - if (status === 'OK' && results[0]) { - const address = results[0].formatted_address; - console.log("data_: ", address); - this.myAddress = address; - this.addressAutocomplete = address; // Sincronizar con el input - } - }); }); } - /* check (ev: any) { - this.amount = parseFloat(ev.target.value.replace(",", "")); - console.log(this.amount); - if (this.amount < 150) { - this.amount_check = false; - } else { - this.amount_check = true; + async openAddProperty() { + const modal = await this.modalCtrl.create({ component: AddPropertyPage }); + await modal.present(); + const { data } = await modal.onDidDismiss(); + if (data?.property) { + this.properties.push(data.property); + this.selectedPropertyId = data.property.id; + this.cdr.detectChanges(); } - }*/ - - changehour (ev: any) { - console.log(this.setHour); } - changedate (ev: any) { - console.log(ev.target.value); - let current_date = (new Date().toISOString().slice(0, 10)).toString(); - let selected_date = (new Date(this.setDate).toISOString().slice(0, 10)).toString(); - this.differenceDate = (Number(new Date(selected_date)) - Number(new Date(current_date))); - console.log(this.differenceDate); - } - - autocomplete(ev: any) { - if (!this.addressAutocomplete.trim().length) { - this.placesSearch = null; - } - console.log(this.addressAutocomplete) - new google.maps.places.AutocompleteService().getPredictions({ input: this.addressAutocomplete }, (predictions: any) => { - this.ngZone.run(() => { - this.placesSearch = predictions; - console.log(predictions); - }); - }); - } - - geoloc(place_id: string, place_description: string) { - this.myAddress = place_description; - this.addressAutocomplete = place_description; // Sincronizar con el input - console.log(place_id); - this.hidelist(); - new google.maps.Geocoder().geocode({ placeId: place_id }, (coordinates: any) => { - this.ngZone.run(() => { - console.log(coordinates[0].geometry.location.lat() + ", " + coordinates[0].geometry.location.lng()); - this.myPosition = { - latitude: coordinates[0].geometry.location.lat(), - longitude: coordinates[0].geometry.location.lng() - } - }); - }); - } - - addpostulation() { - // Validar que fecha y hora estén seleccionadas - if (!this.setDate || !this.setHour) { - this.alertService.presentToast(this.translateService.instant('alerts.select_date_time') || 'Selecciona fecha y hora'); + async addPhoto() { + if (this.photos.length >= 3) { + this.alertService.presentToast('Máximo 3 fotos.'); return; } + try { + const image = await Camera.getPhoto({ + quality: 70, + resultType: CameraResultType.Base64, + source: CameraSource.Prompt, + }); + this.photos.push('data:image/jpeg;base64,' + image.base64String); + this.cdr.detectChanges(); + } catch (e) {} + } - // Calcular diferencia de fecha directamente - const today = new Date(); - today.setHours(0, 0, 0, 0); - const selectedDate = new Date(this.setDate); - selectedDate.setHours(0, 0, 0, 0); - this.differenceDate = selectedDate.getTime() - today.getTime(); + removePhoto(index: number) { + this.photos.splice(index, 1); + this.cdr.detectChanges(); + } - // Calcular diferencia de hora - this.differenceHour = (new Date(this.setHour).getTime() - new Date().getTime()); - - console.log("differenceDate:", this.differenceDate); - console.log("differenceHour:", this.differenceHour); - console.log("mintime:", this.mintime * 3600000); - - // Máximo 2 días de anticipación (172800000 ms = 2 días) - const maxDays = 2 * 24 * 60 * 60 * 1000; - console.log("maxDays:", maxDays); - - // Validación de tiempo mínimo (mismo día y menos de mintime horas) - if (this.differenceDate < 86400000 && this.differenceHour < (this.mintime * 3600000)) { - this.alertService.presentToast(this.translateService.instant('alerts.categories_mintime')); - } else if (this.differenceDate <= maxDays) { - if (this.myAddress && this.myPosition.latitude && this.myPosition.longitude) { - this.loadingCtrl.create().then((overlay) => { - this.loading = overlay; - this.loading.present(); - }); - - if (!Number.isInteger(parseInt(this.myIntnumber ?? ''))) { - this.myIntnumber = null; - } - this.ichambaService.addPostulation(this.category, this.myAddress!, this.myIntnumber ?? '', this.references, this.myPosition.latitude, this.myPosition.longitude, this.setDate, this.setHour, this.details).subscribe( - (data: any) => { - if (this.loading) this.loading.dismiss(); - if (data['message'] == 'No providers') { - this.alertService.presentToast(this.translateService.instant('alerts.no_provider')); - } else { - this.navCtrl.navigateRoot('/dashboard'); - this.alertService.presentToast(this.translateService.instant('alerts.categories_success')); - } - }, (error: any) => { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); - }); - } else { - this.alertService.presentToast(this.translateService.instant('alerts.valid_address')); - } - } else { - this.alertService.presentToast(this.translateService.instant('alerts.categories_maxtime')); + async addpostulation() { + if (!this.selectedPropertyId) { + this.alertService.presentToast('Selecciona una propiedad.'); + return; } + this.loading = await this.loadingCtrl.create(); + await this.loading.present(); + this.ichambaService.addPostulation( + this.category, this.selectedPropertyId, this.references, this.details, this.photos + ).subscribe({ + next: (data: any) => { + this.loading.dismiss(); + if (data['message'] === 'No providers') { + this.alertService.presentToast('No hay proveedores disponibles para esta categoría.'); + } else { + this.navCtrl.navigateRoot('/dashboard'); + this.alertService.presentToast('¡Postulación creada con éxito!'); + } + }, + error: (error: any) => { + this.loading.dismiss(); + this.alertService.presentToast('Error: ' + error['status']); + } + }); } - - showlist() { - this.showif = true; - } - - hidelist() { - this.showif = false; - } - } diff --git a/src/app/pages/contracts/contract-discussion/contract-discussion.module.ts b/src/app/pages/contracts/contract-discussion/contract-discussion.module.ts new file mode 100644 index 0000000..0bc1e76 --- /dev/null +++ b/src/app/pages/contracts/contract-discussion/contract-discussion.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { ContractDiscussionPage } from './contract-discussion.page'; + +@NgModule({ + imports: [CommonModule, FormsModule, IonicModule], + declarations: [ContractDiscussionPage], + exports: [ContractDiscussionPage] +}) +export class ContractDiscussionPageModule {} diff --git a/src/app/pages/contracts/contract-discussion/contract-discussion.page.html b/src/app/pages/contracts/contract-discussion/contract-discussion.page.html new file mode 100644 index 0000000..92234ca --- /dev/null +++ b/src/app/pages/contracts/contract-discussion/contract-discussion.page.html @@ -0,0 +1,80 @@ + + + + + + + + Chat del Contrato + + + + +
+ + +
+ +
+
+ +
+
+ +
+
+ +
+ +

No hay mensajes aún.

+
+ + + + +
+
+ Moderador +

{{ msg.comment }}

+ {{ msg.created_at | date:'d MMM, HH:mm' }} +
+
+ +
+
+

{{ msg.comment }}

+ {{ msg.created_at | date:'d MMM, HH:mm' }} +
+
+ +
+
+ {{ msg.sender_name }} +

{{ msg.comment }}

+ {{ msg.created_at | date:'d MMM, HH:mm' }} +
+
+ +
+
+ +
+
+ + +
+ + + + +
+
diff --git a/src/app/pages/contracts/contract-discussion/contract-discussion.page.scss b/src/app/pages/contracts/contract-discussion/contract-discussion.page.scss new file mode 100644 index 0000000..55b5130 --- /dev/null +++ b/src/app/pages/contracts/contract-discussion/contract-discussion.page.scss @@ -0,0 +1,117 @@ +.messages-container { + display: flex; + flex-direction: column; + padding: 16px; + min-height: 100%; +} + +.message-wrapper { + display: flex; + margin-bottom: 10px; + + &.right { justify-content: flex-end; } + &.left { justify-content: flex-start; } + &.center { justify-content: center; } +} + +.message-bubble { + max-width: 72%; + padding: 10px 14px; + border-radius: 18px; + word-break: break-word; + + p { + margin: 0 0 4px 0; + font-size: 0.95rem; + line-height: 1.4; + } + + .sender-name { + display: block; + font-size: 0.72rem; + font-weight: 600; + margin-bottom: 4px; + opacity: 0.75; + } + + .timestamp { + display: block; + font-size: 0.68rem; + margin-top: 4px; + opacity: 0.65; + text-align: right; + } + + &.mine { + background: var(--ion-color-primary); + color: #fff; + border-bottom-right-radius: 4px; + } + + &.other { + background: var(--ion-color-light); + color: var(--ion-color-dark); + border-bottom-left-radius: 4px; + } + + &.moderator { + background: var(--ion-color-medium); + color: #fff; + max-width: 80%; + text-align: center; + border-radius: 10px; + font-style: italic; + + .sender-name { + text-align: center; + font-weight: 700; + opacity: 1; + } + + .timestamp { + text-align: center; + } + } +} + +.empty-state { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 16px; + color: var(--ion-color-medium); + + p { + margin: 0; + font-size: 0.95rem; + } +} + +.input-bar { + display: flex; + align-items: flex-end; + padding: 6px; + gap: 2px; + background: var(--ion-tab-bar-background, #fff); + border-top: 1px solid var(--ion-color-light-shade); + + ion-textarea { + flex: 1; + --padding-start: 14px; + --padding-end: 14px; + --padding-top: 8px; + --padding-bottom: 8px; + --background: var(--ion-color-step-100, #f4f5f8); + border-radius: 20px; + margin: 0; + max-height: 120px; + } + + ion-button { + --padding-start: 6px; + --padding-end: 6px; + min-height: 40px; + } +} diff --git a/src/app/pages/contracts/contract-discussion/contract-discussion.page.ts b/src/app/pages/contracts/contract-discussion/contract-discussion.page.ts new file mode 100644 index 0000000..4c80cec --- /dev/null +++ b/src/app/pages/contracts/contract-discussion/contract-discussion.page.ts @@ -0,0 +1,76 @@ +import { Component, Input, OnInit, ViewChild, ChangeDetectorRef } from '@angular/core'; +import { IonContent, ModalController } from '@ionic/angular'; +import { IchambaService } from 'src/app/services/ichamba.service'; +import { AuthService } from 'src/app/services/auth.service'; +import { AlertService } from 'src/app/services/alert.service'; + +@Component({ + selector: 'app-contract-discussion', + templateUrl: './contract-discussion.page.html', + styleUrls: ['./contract-discussion.page.scss'], + standalone: false +}) +export class ContractDiscussionPage implements OnInit { + + @Input() contractId: any; + + @ViewChild('content') content!: IonContent; + + messages: any[] = []; + newMessage: string = ''; + currentUserId: any; + loading = true; + sending = false; + + constructor( + private modalCtrl: ModalController, + private ichambaService: IchambaService, + private authService: AuthService, + private alertService: AlertService, + private cdr: ChangeDetectorRef, + ) {} + + ngOnInit() { + this.currentUserId = this.authService.userId; + this.loadMessages(); + } + + loadMessages() { + this.loading = true; + this.ichambaService.getContractDiscussion(this.contractId).subscribe({ + next: data => { + this.messages = data; + this.loading = false; + this.cdr.detectChanges(); + setTimeout(() => this.content.scrollToBottom(300), 100); + }, + error: () => { + this.loading = false; + this.cdr.detectChanges(); + } + }); + } + + sendMessage() { + const text = this.newMessage.trim(); + if (!text || this.sending) return; + this.newMessage = ''; + this.sending = true; + this.ichambaService.sendContractMessage(this.contractId, text).subscribe({ + next: (msg: any) => { + this.messages.push(msg); + this.sending = false; + this.cdr.detectChanges(); + setTimeout(() => this.content.scrollToBottom(300), 100); + }, + error: () => { + this.sending = false; + this.alertService.presentToast('No se pudo enviar el mensaje.'); + } + }); + } + + dismiss() { + this.modalCtrl.dismiss(); + } +} diff --git a/src/app/pages/contracts/contracted/contracted.module.ts b/src/app/pages/contracts/contracted/contracted.module.ts index 3caa8a4..6d894ed 100755 --- a/src/app/pages/contracts/contracted/contracted.module.ts +++ b/src/app/pages/contracts/contracted/contracted.module.ts @@ -2,17 +2,13 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Routes, RouterModule } from '@angular/router'; - import { IonicModule } from '@ionic/angular'; - import { ContractedPage } from './contracted.page'; -import { TranslateModule } from '@ngx-translate/core' +import { TranslateModule } from '@ngx-translate/core'; +import { ContractDiscussionPageModule } from '../contract-discussion/contract-discussion.module'; const routes: Routes = [ - { - path: '', - component: ContractedPage - } + { path: '', component: ContractedPage } ]; @NgModule({ @@ -21,7 +17,8 @@ const routes: Routes = [ FormsModule, IonicModule, RouterModule.forChild(routes), - TranslateModule + TranslateModule, + ContractDiscussionPageModule, ], declarations: [ContractedPage] }) diff --git a/src/app/pages/contracts/contracted/contracted.page.html b/src/app/pages/contracts/contracted/contracted.page.html index 92b0a22..deab279 100755 --- a/src/app/pages/contracts/contracted/contracted.page.html +++ b/src/app/pages/contracts/contracted/contracted.page.html @@ -48,8 +48,12 @@

{{'contracts.info_pin' | translate}}

-
- {{'contracts.cancel_1.1' | translate}}
{{'contracts.cancel_1.2' | translate}}
+
+ + + Chat + + {{'contracts.cancel_1.1' | translate}}
{{'contracts.cancel_1.2' | translate}}
diff --git a/src/app/pages/contracts/contracted/contracted.page.ts b/src/app/pages/contracts/contracted/contracted.page.ts index c2f9e2a..0a3db20 100755 --- a/src/app/pages/contracts/contracted/contracted.page.ts +++ b/src/app/pages/contracts/contracted/contracted.page.ts @@ -1,5 +1,6 @@ import { Component, ChangeDetectorRef } from '@angular/core'; import { ModalController, MenuController, NavController } from '@ionic/angular'; +import { ContractDiscussionPage } from '../contract-discussion/contract-discussion.page'; import { EventService } from '../../../services/event.service'; import { EnvService } from 'src/app/services/env.service'; import { AuthService } from 'src/app/services/auth.service'; @@ -99,6 +100,14 @@ export class ContractedPage { //} } + async openDiscussion(contractId: any) { + const modal = await this.modalController.create({ + component: ContractDiscussionPage, + componentProps: { contractId }, + }); + await modal.present(); + } + async presentAlert(id: String) { const alert = await this.alertController.create({ header: 'Atención', diff --git a/src/app/pages/contracts/hire/hire.page.html b/src/app/pages/contracts/hire/hire.page.html index 496dd8b..3d3cfca 100755 --- a/src/app/pages/contracts/hire/hire.page.html +++ b/src/app/pages/contracts/hire/hire.page.html @@ -6,110 +6,66 @@ {{'contracts.hire' | translate}} +

{{'contracts.hire_info_1' | translate}} {{ supplier }} {{'contracts.hire_info_2' | translate}}
{{'contracts.hire_pay' | translate}}


- - -

{{'contracts.fee_amount' | translate}}:

-
- -

${{ fee | number: '1.2-2'}}

-
-
- - -

{{'contracts.discount_amount' | translate}}:

-
- -

${{ discount | number: '1.2-2'}}

-
-
- - -

{{'contracts.final_amount' | translate}}:

-
- -

${{ final_amount | number: '1.2-2'}}

-
-
-
- - - {{'contracts.coupon' | translate}}: - - {{'contracts.validate' | translate}} - - - - {{'cards.card' | translate}} - - Agregar tarjeta - {{card.brand}} - {{card.card_number}} - - -
- - CVV - - - - MODO DE PRUEBA: El pago será simulado - -

- {{'contracts.hire_confirm' | translate}} -
+ + + +

{{'contracts.fee_amount' | translate}}:

+
+ +

${{ fee | number: '1.2-2'}}

+
+
+ + +

{{'contracts.discount_amount' | translate}}:

+
+ +

${{ discount | number: '1.2-2'}}

+
+
+ + +

{{'contracts.final_amount' | translate}}:

+
+ +

${{ final_amount | number: '1.2-2'}}

+
+

-
- - - - - -

{{'cards.openpay_1' | translate}}

-
-
- - -

{{'cards.openpay_2' | translate}}

- - -

{{'cards.openpay_3' | translate}}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

+ +

Selecciona la fecha en que recibirás el servicio:

+ + + + + {{ date_1_formatted }} + + + + {{ date_2_formatted }} + + + +
+ + + + {{'contracts.coupon' | translate}}: + + {{'contracts.validate' | translate}} + + + + + MODO DE PRUEBA: El pago será simulado + + +
+ + {{'contracts.hire_confirm' | translate}} +
diff --git a/src/app/pages/contracts/hire/hire.page.ts b/src/app/pages/contracts/hire/hire.page.ts index 866844c..3a7f2e6 100755 --- a/src/app/pages/contracts/hire/hire.page.ts +++ b/src/app/pages/contracts/hire/hire.page.ts @@ -1,16 +1,13 @@ import { Component, OnInit } from '@angular/core'; -import { ModalController, MenuController, NavController, LoadingController } from '@ionic/angular'; +import { NavController, LoadingController } from '@ionic/angular'; import { EventService } from '../../../services/event.service'; -import { AddcardPage } from 'src/app/pages/card/addcard/addcard.page'; import { ActivatedRoute } from '@angular/router'; -import { NgForm } from '@angular/forms'; import { EnvService } from 'src/app/services/env.service'; -import { AuthService } from 'src/app/services/auth.service'; import { IchambaService } from 'src/app/services/ichamba.service'; import { TranslateService } from '@ngx-translate/core'; import { AlertService } from 'src/app/services/alert.service'; - -declare var OpenPay: any; +import { Stripe, PaymentSheetEventsEnum } from '@capacitor-community/stripe'; +import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-hire', @@ -21,11 +18,6 @@ declare var OpenPay: any; export class HirePage implements OnInit { private loading: HTMLIonLoadingElement | null = null; - code_check = true; - cards: any[] = []; - code: any = null; - device_id: any = null; - card_id: any = null; supplier: any = null; supplier_id: any = null; postulation_id: any = null; @@ -33,13 +25,14 @@ export class HirePage implements OnInit { discount = 0; final_amount: any = null; coupon: any = null; - cards_menu: any = null; paymentBypass = false; + date_1: string | null = null; + date_2: string | null = null; + selected_date: 'date_1' | 'date_2' | null = null; + date_1_formatted: string = ''; + date_2_formatted: string = ''; constructor( - private modalController: ModalController, - private menu: MenuController, - private authService: AuthService, private alertService: AlertService, private ichambaService: IchambaService, private translateService: TranslateService, @@ -49,122 +42,77 @@ export class HirePage implements OnInit { private events: EventService, private env: EnvService, ) { - this.events.subscribe('refreshcards', (data) => { - this.card_id = null; - this.getcards(); - }); this.supplier = this.activatedRoute.snapshot.paramMap.get('supplier'); this.supplier_id = this.activatedRoute.snapshot.paramMap.get('supplier_id'); this.postulation_id = this.activatedRoute.snapshot.paramMap.get('postulation_id'); this.fee = this.activatedRoute.snapshot.paramMap.get('fee'); this.final_amount = this.fee; - } + this.date_1 = this.activatedRoute.snapshot.paramMap.get('date_1'); + this.date_2 = this.activatedRoute.snapshot.paramMap.get('date_2'); + this.date_1_formatted = this.date_1 ? this.formatDate(this.date_1) : ''; + this.date_2_formatted = this.date_2 ? this.formatDate(this.date_2) : ''; + } ngOnInit() { this.paymentBypass = this.env.PAYMENT_BYPASS; - if (!this.paymentBypass) { - OpenPay.setId(this.env.MERCHANT_ID); - OpenPay.setApiKey(this.env.PUBLIC_API_KEY); - console.log(OpenPay.getSandboxMode()); - var deviceDataId = OpenPay.deviceData.setup("card_form"); - this.getcards(); + } + + private formatDate(iso: string): string { + return new Date((new Date(iso).toLocaleString('en-US') + ' UTC').replace(',', '')) + .toLocaleDateString('es-MX', { weekday: 'long', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }); + } + + checkCoupon() { + if (!this.coupon) return; + if (!this.selected_date) { + this.alertService.presentToast('Selecciona una fecha antes de aplicar el cupón.'); + return; } - } - - newcard() { - this.navCtrl.navigateForward('/addcard'); - } - - getcards() { - this.ichambaService.getCard().subscribe( - data => { - this.cards = data; - }, error => { + this.loadingCtrl.create().then(overlay => { + this.loading = overlay; + this.loading.present(); + }); + this.ichambaService.checkCoupon(this.postulation_id, this.supplier_id, this.selected_date, this.coupon).subscribe( + (data: any) => { + if (this.loading) this.loading.dismiss(); + if (!data) { + this.alertService.presentToast(this.translateService.instant('alerts.nocoupon')); + return; + } + if (data['name'] == 'used') { + this.alertService.presentToast(this.translateService.instant('alerts.coupon_used')); + } else if (data['name'] == 'expired') { + this.alertService.presentToast(this.translateService.instant('alerts.coupon_expired')); + } else if (data['name'] == 'success') { + this.events.publish('refreshpcontracts', 'data'); + this.navCtrl.navigateRoot('/contracts/contracted'); + this.alertService.presentToast(this.translateService.instant('alerts.hire')); + } else if (data['name'] == this.coupon) { + this.discount = data['amount'] + (this.fee * (data['percentage'] / 100)); + if (this.discount > this.fee) this.discount = this.fee; + this.final_amount = this.fee - this.discount; + this.alertService.presentToast(this.translateService.instant('alerts.coupon_valid')); + } + }, (error: any) => { + if (this.loading) this.loading.dismiss(); this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); }); } - code_checker(ev: any) { - if (ev.target.value.length > 4){ - console.log(ev.target.value.length); - ev.target.value = ev.target.value.slice(0, 4); + async createContract() { + if (!this.selected_date) { + this.alertService.presentToast('Selecciona una fecha para continuar.'); + return; } - this.code_check = OpenPay.card.validateCVC(ev.target.value); - console.log(this.code_check); - } - - selected_card(ev: any) { - this.card_id = ev.target.value; - console.log(this.card_id); - if (this.card_id == "Agregar tarjeta") { - console.log(this.card_id); - ev.target.value = null; - this.newcard(); - } - } - - checkCoupon() { - if (this.coupon) { - this.loadingCtrl.create().then((overlay) => { - this.loading = overlay; - this.loading.present(); - }); - this.ichambaService.checkCoupon(this.postulation_id, this.supplier_id, this.coupon).subscribe( - (data: any) => { - if (data) { - if (data['name'] == "used") { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('alerts.coupon_used')); - } else if (data['name'] == "expired") { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('alerts.coupon_expired')); - } else if (data['name'] == this.coupon) { - if (this.loading) this.loading.dismiss(); - this.discount = (data['amount'] + (this.fee * (data['percentage']/100))); - console.log(this.discount); - if (this.discount > this.fee){ - this.discount = this.fee - } - this.final_amount = (this.fee - this.discount); - console.log(this.final_amount); - this.alertService.presentToast(this.translateService.instant('alerts.coupon_valid')); - } else if (data['name'] == "success") { - if (this.loading) this.loading.dismiss(); - this.events.publish('refreshpcontracts', 'data'); - this.navCtrl.navigateRoot('/contracts/contracted'); - this.alertService.presentToast(this.translateService.instant('alerts.hire')); - } - } else { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('alerts.nocoupon')); - } - }, (error: any) => { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); - }); - } - } - - createContract(form: NgForm) { - // Si el bypass está activo, enviar con valores dummy if (this.paymentBypass) { - this.loadingCtrl.create().then((overlay) => { - this.loading = overlay; - this.loading.present(); - }); - this.ichambaService.createContract(this.postulation_id, this.supplier_id, 'BYPASS', this.coupon, '000', 'BYPASS').subscribe( + this.loading = await this.loadingCtrl.create(); + await this.loading.present(); + this.ichambaService.createContract(this.postulation_id, this.supplier_id, 'BYPASS', this.selected_date, this.coupon).subscribe( (data: any) => { - if (data['type'] == "error") { - if (this.loading) this.loading.dismiss(); + if (this.loading) this.loading.dismiss(); + if (data['type'] == 'error') { this.alertService.presentToast(data['message']); - } else if (data['name'] == "used") { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('contracts.coupon_used')); - } else if (data['name'] == "expired") { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('contracts.coupon_expired')); } else { - if (this.loading) this.loading.dismiss(); this.events.publish('refreshpcontracts', 'data'); this.navCtrl.navigateRoot('/contracts/contracted'); this.alertService.presentToast(this.translateService.instant('alerts.hire')); @@ -176,42 +124,53 @@ export class HirePage implements OnInit { return; } - if (this.card_id != "Agregar tarjeta") { - if (this.code_check) { - let code = form.value.code; - let device_id = (document.getElementById('deviceDataId')).value; - console.log(device_id); + this.loading = await this.loadingCtrl.create(); + await this.loading.present(); - this.loadingCtrl.create().then((overlay) => { - this.loading = overlay; - this.loading.present(); - }); - this.ichambaService.createContract(this.postulation_id, this.supplier_id, this.card_id, this.coupon, code, device_id).subscribe( - (data: any) => { - if (data['type'] == "error") { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(data['message']); - } else if (data['name'] == "used") { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('contracts.coupon_used')); - } else if (data['name'] == "expired") { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('contracts.coupon_expired')); - } else { - if (this.loading) this.loading.dismiss(); - this.events.publish('refreshpcontracts', 'data'); - this.navCtrl.navigateRoot('/contracts/contracted'); - this.alertService.presentToast(this.translateService.instant('alerts.hire')); - } - }, (error: any) => { - if (this.loading) this.loading.dismiss(); - this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); - }); - //console.log(onSuccess.data.id); - //console.log(document.getElementById('deviceDataId').value); + try { + const intentData = await firstValueFrom( + this.ichambaService.getPaymentIntent(this.postulation_id, this.supplier_id, this.coupon) + ); + + await Stripe.createPaymentSheet({ + paymentIntentClientSecret: intentData.client_secret, + customerId: intentData.customer_id, + customerEphemeralKeySecret: intentData.ephemeral_key, + merchantDisplayName: 'JobHero', + }); + + await this.loading.dismiss(); + + const result = await Stripe.presentPaymentSheet(); + + if (result.paymentResult !== PaymentSheetEventsEnum.Completed) return; + + this.loading = await this.loadingCtrl.create(); + await this.loading.present(); + + this.ichambaService.createContract( + this.postulation_id, this.supplier_id, intentData.payment_intent_id, this.selected_date!, this.coupon + ).subscribe({ + next: (data: any) => { + if (this.loading) this.loading.dismiss(); + if (data['type'] == 'error') { + this.alertService.presentToast(data['message']); + } else { + this.events.publish('refreshpcontracts', 'data'); + this.navCtrl.navigateRoot('/contracts/contracted'); + this.alertService.presentToast(this.translateService.instant('alerts.hire')); + } + }, + error: (error: any) => { + if (this.loading) this.loading.dismiss(); + this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); + } + }); + } catch (error: any) { + await this.loading?.dismiss(); + if (error?.paymentResult !== PaymentSheetEventsEnum.Canceled) { + this.alertService.presentToast(this.translateService.instant('alerts.error')); } - } else { - this.alertService.presentToast(this.translateService.instant('alerts.select_card')); } } diff --git a/src/app/pages/contracts/pending/pending.module.ts b/src/app/pages/contracts/pending/pending.module.ts index 25d700f..9cafdbe 100755 --- a/src/app/pages/contracts/pending/pending.module.ts +++ b/src/app/pages/contracts/pending/pending.module.ts @@ -6,7 +6,8 @@ import { Routes, RouterModule } from '@angular/router'; import { IonicModule } from '@ionic/angular'; import { PendingPage } from './pending.page'; -import { TranslateModule } from '@ngx-translate/core' +import { TranslateModule } from '@ngx-translate/core'; +import { RepostulatePageModule } from '../../postulations/repostulate/repostulate.module'; const routes: Routes = [ { @@ -21,7 +22,8 @@ const routes: Routes = [ FormsModule, IonicModule, RouterModule.forChild(routes), - TranslateModule + TranslateModule, + RepostulatePageModule, ], declarations: [PendingPage] }) diff --git a/src/app/pages/contracts/pending/pending.page.html b/src/app/pages/contracts/pending/pending.page.html index 75d7df1..daf61b4 100755 --- a/src/app/pages/contracts/pending/pending.page.html +++ b/src/app/pages/contracts/pending/pending.page.html @@ -14,7 +14,7 @@ - + @@ -30,7 +30,7 @@ - +

{{ pcontract.category }}

{{ pcontract.en_category }}

@@ -39,6 +39,16 @@
{{'contracts.viewsuppliers_1.1' | translate}}
{{'contracts.viewsuppliers_1.2' | translate}}
+
+ + + Repostular + + + + Cancelar + +
diff --git a/src/app/pages/contracts/pending/pending.page.ts b/src/app/pages/contracts/pending/pending.page.ts index fcd3e53..819c40f 100755 --- a/src/app/pages/contracts/pending/pending.page.ts +++ b/src/app/pages/contracts/pending/pending.page.ts @@ -1,5 +1,5 @@ import { Component, ChangeDetectorRef } from '@angular/core'; -import { ModalController, MenuController, NavController } from '@ionic/angular'; +import { AlertController, ModalController, MenuController, NavController } from '@ionic/angular'; import { EventService } from '../../../services/event.service'; import { EnvService } from 'src/app/services/env.service'; import { AuthService } from 'src/app/services/auth.service'; @@ -7,6 +7,7 @@ import { IchambaService } from 'src/app/services/ichamba.service'; import { TranslateService } from '@ngx-translate/core'; import { LanguageService } from 'src/app/services/language.service'; import { AlertService } from 'src/app/services/alert.service'; +import { RepostulateePage } from '../../postulations/repostulate/repostulate.page'; @Component({ selector: 'app-pending', @@ -23,6 +24,7 @@ export class PendingPage { constructor( private modalController: ModalController, + private alertController: AlertController, private menu: MenuController, private navCtrl: NavController, private events: EventService, @@ -83,4 +85,45 @@ export class PendingPage { this.navCtrl.navigateForward(['/viewsuppliers/', postulation_id]); } + async confirmCancel(postulation: any) { + const alert = await this.alertController.create({ + header: 'Cancelar postulación', + message: ' ', + buttons: [ + { text: 'No', role: 'cancel' }, + { + text: 'Sí, cancelar', + role: 'destructive', + handler: () => this.doCancel(postulation.id) + } + ] + }); + await alert.present(); + alert.querySelector('.alert-message')!.innerHTML = '¿Estás seguro de que deseas cancelar esta postulación de ' + postulation.category + '?'; + } + + private doCancel(id: any) { + this.ichambaService.cancelPostulation(id).subscribe({ + next: () => { + this.alertService.presentToast('Postulación cancelada.'); + this.getpcontracts(); + }, + error: (error: any) => { + this.alertService.presentToast('Error al cancelar: ' + error['status']); + } + }); + } + + async openRepostulate(postulation: any) { + const modal = await this.modalController.create({ + component: RepostulateePage, + componentProps: { postulation }, + }); + await modal.present(); + const { data } = await modal.onDidDismiss(); + if (data?.reposted) { + this.getpcontracts(); + } + } + } diff --git a/src/app/pages/contracts/viewsuppliers/viewsuppliers.page.html b/src/app/pages/contracts/viewsuppliers/viewsuppliers.page.html index 1a75987..7c31ba6 100755 --- a/src/app/pages/contracts/viewsuppliers/viewsuppliers.page.html +++ b/src/app/pages/contracts/viewsuppliers/viewsuppliers.page.html @@ -6,31 +6,59 @@ {{'contracts.viewsuppliers_header' | translate}} + -

{{ pcontractssuppliers_category[0] }}

-

{{ pcontractssuppliers_en_category[0] }}

-

{{ pcontractssuppliers_address[0] }}

-

{{ pcontractssuppliers_dates[0] }}

+

{{ headerCategory }}

+

{{ headerAddress }}

+ - - - + + +
+ + + Fecha más pronta + + + + Más económico + + + + Certificados + +
+ + + + - + - - + + -

{{ pcontractsupplier.supplier }}

-

{{ pcontractssuppliers_certified[i] }}

-

{{'contracts.viewsuppliers_tags' | translate}}: {{ pcontractsupplier.tags }}

-

{{'contracts.viewsuppliers_fee' | translate}}: ${{ pcontractsupplier.fee }}

-

{{'contracts.viewsuppliers_rate' | translate}}: {{ pcontractsupplier.score }} de 5

+

{{ s.supplier }}

+

+ + {{ s._certified }} +

+

{{'contracts.viewsuppliers_tags' | translate}}: {{ s.tags }}

+

{{'contracts.viewsuppliers_fee' | translate}}: ${{ s.fee }}

+

+ {{ s._dateFormatted }} + · opción 2 disponible +

+

{{'contracts.viewsuppliers_rate' | translate}}: {{ s.score }} de 5

- {{'contracts.viewsuppliers_hire' | translate}} + + {{'contracts.viewsuppliers_hire' | translate}} +
+
diff --git a/src/app/pages/contracts/viewsuppliers/viewsuppliers.page.ts b/src/app/pages/contracts/viewsuppliers/viewsuppliers.page.ts index 5c92072..af68c95 100755 --- a/src/app/pages/contracts/viewsuppliers/viewsuppliers.page.ts +++ b/src/app/pages/contracts/viewsuppliers/viewsuppliers.page.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, ChangeDetectorRef } from '@angular/core'; import { ModalController, MenuController, NavController } from '@ionic/angular'; import { EventService } from '../../../services/event.service'; import { ActivatedRoute, Router } from '@angular/router'; @@ -19,15 +19,13 @@ import { HirePage } from '../hire/hire.page'; export class ViewsuppliersPage implements OnInit { postulation_id: string | null = null; - pcontractssuppliers: any[] = []; - pcontractssuppliers_dates: any[] = []; - pcontractssuppliers_certified: any[] = []; - pcontractssuppliers_category: any[] = []; - pcontractssuppliers_en_category: any[] = []; - pcontractssuppliers_address: any[] = []; - pcontractssuppliers_amount: any[] = []; - pcontractssuppliers_fee: any[] = []; + suppliers: any[] = []; + sortedSuppliers: any[] = []; + headerCategory: string = ''; + headerAddress: string = ''; + headerDate: string = ''; lang: boolean = false; + activeSort: string = ''; constructor( private modalController: ModalController, @@ -42,68 +40,83 @@ export class ViewsuppliersPage implements OnInit { private translateService: TranslateService, private languageService: LanguageService, private env: EnvService, - ) { } + private cdr: ChangeDetectorRef, + ) {} ngOnInit() { this.postulation_id = this.activatedRoute.snapshot.paramMap.get('postulation_id'); - this.getPostulants(this.postulation_id ?? ''); - if (this.languageService.getDefaultLanguage() == 'es') { - this.lang = true; - } else { - this.lang = false - } + this.lang = this.languageService.getDefaultLanguage() === 'es'; + this.loadPostulants(); } refresh(event: any) { - this.ichambaService.getPostulants(this.postulation_id ?? '').subscribe( - (data: any) => { - this.pcontractssuppliers = data; - for (var i of this.pcontractssuppliers) { - if (this.languageService.getDefaultLanguage() == 'es') { - this.pcontractssuppliers_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace("," ,"")).toLocaleDateString('es-US', { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute:'numeric', timeZoneName: 'long'})); - this.pcontractssuppliers_certified.push(i.membership == 1 ? 'Certificado' : ''); - } else { - this.pcontractssuppliers_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace("," ,"")).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute:'numeric', timeZoneName: 'long'})); - this.pcontractssuppliers_certified.push(i.membership == 1 ? 'Certified' : ''); - } - this.pcontractssuppliers_category.push(i.category); - this.pcontractssuppliers_en_category.push(i.en_category); - this.pcontractssuppliers_address.push(i.address); - this.pcontractssuppliers_amount.push(i.amount); - this.pcontractssuppliers_fee.push(i.fee); - } + this.ichambaService.getPostulants(this.postulation_id ?? '').subscribe({ + next: (data: any) => { + this.processSuppliers(data); event.target.complete(); - }, error => { - this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); + }, + error: () => { + this.alertService.presentToast("Por favor contacte a soporte técnico."); event.target.complete(); - }); + } + }); } - getPostulants(postulation_id: String) { - this.ichambaService.getPostulants(postulation_id).subscribe( - (data: any) => { - this.pcontractssuppliers = data; - for (var i of this.pcontractssuppliers) { - if (this.languageService.getDefaultLanguage() == 'es') { - this.pcontractssuppliers_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace("," ,"")).toLocaleDateString('es-US', { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute:'numeric', timeZoneName: 'long'})); - this.pcontractssuppliers_certified.push(i.membership == 1 ? 'Certificado' : ''); - } else { - this.pcontractssuppliers_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace("," ,"")).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute:'numeric', timeZoneName: 'long'})); - this.pcontractssuppliers_certified.push(i.membership == 1 ? 'Certified' : ''); - } - this.pcontractssuppliers_category.push(i.category); - this.pcontractssuppliers_en_category.push(i.en_category); - this.pcontractssuppliers_address.push(i.address); - this.pcontractssuppliers_amount.push(i.amount); - this.pcontractssuppliers_fee.push(i.fee); - } - }, error => { - this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); - }); + loadPostulants() { + this.ichambaService.getPostulants(this.postulation_id ?? '').subscribe({ + next: (data: any) => { + this.processSuppliers(data); + this.cdr.detectChanges(); + }, + error: () => { + this.alertService.presentToast("Por favor contacte a soporte técnico."); + } + }); } - hiresupplier(supplier_id: String, postulation_id: String, supplier: String, fee: Number) { - this.router.navigate(['/hire', {supplier: supplier, supplier_id: supplier_id, postulation_id: postulation_id, fee: fee}]); + processSuppliers(data: any[]) { + const locale = this.lang ? 'es-US' : 'en-US'; + this.suppliers = data.map(s => ({ + ...s, + _certified: this.lang ? (s.membership == 1 ? 'Certificado' : '') : (s.membership == 1 ? 'Certified' : ''), + _dateFormatted: s.date1 + ? new Date((new Date(s.date1).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString(locale, { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }) + : '' + })); + if (this.suppliers.length > 0) { + const first = this.suppliers[0]; + this.headerCategory = this.lang ? first.category : first.en_category; + this.headerAddress = first.address; + } + this.sortedSuppliers = [...this.suppliers]; + if (this.activeSort) this.applySort(this.activeSort); } + sortBy(criteria: string) { + if (this.activeSort === criteria) { + this.activeSort = ''; + this.sortedSuppliers = [...this.suppliers]; + } else { + this.activeSort = criteria; + this.applySort(criteria); + } + this.cdr.detectChanges(); + } + + private applySort(criteria: string) { + this.sortedSuppliers = [...this.suppliers].sort((a, b) => { + if (criteria === 'date') { + const da = a.date1 ? new Date(a.date1).getTime() : Infinity; + const db = b.date1 ? new Date(b.date1).getTime() : Infinity; + return da - db; + } + if (criteria === 'fee') return (a.fee ?? Infinity) - (b.fee ?? Infinity); + if (criteria === 'certified') return (b.membership ?? 0) - (a.membership ?? 0); + return 0; + }); + } + + hiresupplier(supplier_id: String, postulation_id: String, supplier: String, fee: Number, date_1: String, date_2: String) { + this.router.navigate(['/hire', { supplier, supplier_id, postulation_id, fee, date_1, date_2 }]); + } } diff --git a/src/app/pages/postulations/current/current.module.ts b/src/app/pages/postulations/current/current.module.ts index 4f373b0..d681adc 100755 --- a/src/app/pages/postulations/current/current.module.ts +++ b/src/app/pages/postulations/current/current.module.ts @@ -2,16 +2,12 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { Routes, RouterModule } from '@angular/router'; - import { IonicModule } from '@ionic/angular'; - import { CurrentPage } from './current.page'; +import { PostulateOfferPageModule } from '../postulate-offer/postulate-offer.module'; const routes: Routes = [ - { - path: '', - component: CurrentPage - } + { path: '', component: CurrentPage } ]; @NgModule({ @@ -19,7 +15,8 @@ const routes: Routes = [ CommonModule, FormsModule, IonicModule, - RouterModule.forChild(routes) + RouterModule.forChild(routes), + PostulateOfferPageModule, ], declarations: [CurrentPage] }) diff --git a/src/app/pages/postulations/current/current.page.html b/src/app/pages/postulations/current/current.page.html index 33f63d6..651f540 100755 --- a/src/app/pages/postulations/current/current.page.html +++ b/src/app/pages/postulations/current/current.page.html @@ -14,7 +14,7 @@ - + @@ -30,15 +30,20 @@ - - + +

{{ postulation.category }}

{{postulation.address}}

Referencias: {{postulation.references}}

{{postulations_dates[i]}}

Detalles: {{postulation.details}}

-

Tiempo restante: {{postulation.time_limit}} minutos

+

+ + + {{ postulation._timer }} + +

Postularse
diff --git a/src/app/pages/postulations/current/current.page.ts b/src/app/pages/postulations/current/current.page.ts index dcbe3e4..479837b 100755 --- a/src/app/pages/postulations/current/current.page.ts +++ b/src/app/pages/postulations/current/current.page.ts @@ -1,11 +1,12 @@ import { Component, ChangeDetectorRef } from '@angular/core'; -import { MenuController, NavController } from '@ionic/angular'; +import { MenuController, NavController, ModalController } from '@ionic/angular'; import { EventService } from '../../../services/event.service'; import { EnvService } from 'src/app/services/env.service'; import { AuthService } from 'src/app/services/auth.service'; import { IchambaService } from 'src/app/services/ichamba.service'; import { AlertService } from 'src/app/services/alert.service'; import { Browser } from '@capacitor/browser'; +import { PostulateOfferPage } from '../postulate-offer/postulate-offer.page'; @Component({ selector: 'app-current', @@ -18,6 +19,8 @@ export class CurrentPage { postulations: any[] = []; postulations_dates: any[] = []; loading = true; + minFee: number = 0; + private timerInterval: any; constructor( private menu: MenuController, @@ -26,6 +29,7 @@ export class CurrentPage { private authService: AuthService, private alertService: AlertService, private ichambaService: IchambaService, + private modalCtrl: ModalController, private env: EnvService, private cdr: ChangeDetectorRef, ) { @@ -37,6 +41,33 @@ export class CurrentPage { ionViewWillEnter() { this.loading = true; this.getpostulations(); + this.ichambaService.getParameters().subscribe((data: any) => { + this.minFee = data['min_fee'] ?? 0; + }); + this.timerInterval = setInterval(() => { + this.postulations.forEach(p => this.computeTimer(p)); + this.cdr.detectChanges(); + }, 60000); + } + + ionViewWillLeave() { + clearInterval(this.timerInterval); + } + + private computeTimer(p: any) { + const remaining = Math.max(0, Math.floor( + (new Date(p.time_created).getTime() + 1000 * 60 * 1000 - Date.now()) / 60000 + )); + if (remaining <= 0) { + p._timer = 'Expirada'; + } else if (remaining < 60) { + p._timer = `${remaining}m restantes`; + } else { + const h = Math.floor(remaining / 60); + const m = remaining % 60; + p._timer = `${h}h${m > 0 ? ' ' + m + 'm' : ''} restantes`; + } + p._timerCritical = remaining < 60; } refresh(event: any) { @@ -46,6 +77,7 @@ export class CurrentPage { this.postulations_dates = []; for (var i of this.postulations) { this.postulations_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString('es-US', { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'long' })); + this.computeTimer(i); } this.cdr.detectChanges(); event.target.complete(); @@ -62,6 +94,7 @@ export class CurrentPage { this.postulations_dates = []; for (var i of this.postulations) { this.postulations_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString('es-US', { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'long' })); + this.computeTimer(i); } this.loading = false; this.cdr.detectChanges(); @@ -71,18 +104,26 @@ export class CurrentPage { }); } - addpostulation(id: String) { - this.ichambaService.setPostulation(id).subscribe( - (data: any) => { + async addpostulation(id: string) { + const modal = await this.modalCtrl.create({ + component: PostulateOfferPage, + componentProps: { postulationId: id, minFee: this.minFee } + }); + await modal.present(); + const { data } = await modal.onDidDismiss(); + if (!data) return; + this.ichambaService.setPostulation(id, data.date1, data.date2, data.amount).subscribe({ + next: (res: any) => { this.getpostulations(); - this.alertService.presentToast(data['message']); - }, (error: any) => { + this.alertService.presentToast(res['message']); + }, + error: (error: any) => { this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); - }); + } + }); } async openMaps(lat: Number, lng: Number) { await Browser.open({ url: 'http://maps.google.com/maps?q=' + lat + ',' + lng }); } - } diff --git a/src/app/pages/postulations/postulate-offer/postulate-offer.module.ts b/src/app/pages/postulations/postulate-offer/postulate-offer.module.ts new file mode 100644 index 0000000..7071ea9 --- /dev/null +++ b/src/app/pages/postulations/postulate-offer/postulate-offer.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { PostulateOfferPage } from './postulate-offer.page'; + +@NgModule({ + imports: [CommonModule, FormsModule, IonicModule], + declarations: [PostulateOfferPage], + exports: [PostulateOfferPage] +}) +export class PostulateOfferPageModule {} diff --git a/src/app/pages/postulations/postulate-offer/postulate-offer.page.html b/src/app/pages/postulations/postulate-offer/postulate-offer.page.html new file mode 100644 index 0000000..18e4fb6 --- /dev/null +++ b/src/app/pages/postulations/postulate-offer/postulate-offer.page.html @@ -0,0 +1,48 @@ + + + + + + + + Tu oferta + + + + + + + Fecha tentativa 1 * + + + + + + + + + + + Fecha tentativa 2 * + + + + + + + + + + + Monto a cobrar ($) * + + + Cuota mínima del sistema: ${{ minFee }} + + + Enviar oferta + + + diff --git a/src/app/pages/postulations/postulate-offer/postulate-offer.page.scss b/src/app/pages/postulations/postulate-offer/postulate-offer.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/postulations/postulate-offer/postulate-offer.page.ts b/src/app/pages/postulations/postulate-offer/postulate-offer.page.ts new file mode 100644 index 0000000..301c8a5 --- /dev/null +++ b/src/app/pages/postulations/postulate-offer/postulate-offer.page.ts @@ -0,0 +1,48 @@ +import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { AlertService } from 'src/app/services/alert.service'; + +@Component({ + selector: 'app-postulate-offer', + templateUrl: './postulate-offer.page.html', + styleUrls: ['./postulate-offer.page.scss'], + standalone: false +}) +export class PostulateOfferPage implements OnInit { + + @Input() postulationId: any; + @Input() minFee: number = 0; + + date1: string | null = null; + date2: string | null = null; + amount: number | null = null; + minDate: string = new Date().toISOString().slice(0, 10); + + constructor( + private modalCtrl: ModalController, + private alertService: AlertService, + private cdr: ChangeDetectorRef, + ) {} + + ngOnInit() {} + + submit() { + if (!this.date1 || !this.date2) { + this.alertService.presentToast('Debes seleccionar las dos fechas tentativas.'); + return; + } + if (!this.amount || this.amount < this.minFee) { + this.alertService.presentToast(`El monto mínimo es $${this.minFee}.`); + return; + } + this.modalCtrl.dismiss({ + date1: this.date1, + date2: this.date2, + amount: this.amount + }); + } + + dismiss() { + this.modalCtrl.dismiss(null); + } +} diff --git a/src/app/pages/postulations/repostulate/repostulate.module.ts b/src/app/pages/postulations/repostulate/repostulate.module.ts new file mode 100644 index 0000000..e7f0edf --- /dev/null +++ b/src/app/pages/postulations/repostulate/repostulate.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { RepostulateePage } from './repostulate.page'; +import { AddPropertyPageModule } from '../../property/add-property/add-property.module'; + +@NgModule({ + imports: [CommonModule, FormsModule, IonicModule, AddPropertyPageModule], + declarations: [RepostulateePage], + exports: [RepostulateePage], +}) +export class RepostulatePageModule {} diff --git a/src/app/pages/postulations/repostulate/repostulate.page.html b/src/app/pages/postulations/repostulate/repostulate.page.html new file mode 100644 index 0000000..125281f --- /dev/null +++ b/src/app/pages/postulations/repostulate/repostulate.page.html @@ -0,0 +1,70 @@ + + + + + + + + Repostular + + + + + + + Categoría + + + + + Propiedad + + + {{ p.name }}: {{ p.address }} + + + + + + Agregar propiedad + + + + Referencias + + + + + Comentarios + + + +
+ + Fotos (opcional, máx. 3) + +
+
+ + + + +
+ + + +
+
+ + + Se archivará la postulación actual y se creará una nueva. Los proveedores anteriores deberán volver a postularse. + + + + Repostular + + +
diff --git a/src/app/pages/postulations/repostulate/repostulate.page.scss b/src/app/pages/postulations/repostulate/repostulate.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/postulations/repostulate/repostulate.page.ts b/src/app/pages/postulations/repostulate/repostulate.page.ts new file mode 100644 index 0000000..71976bc --- /dev/null +++ b/src/app/pages/postulations/repostulate/repostulate.page.ts @@ -0,0 +1,130 @@ +import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core'; +import { LoadingController, ModalController } from '@ionic/angular'; +import { AlertService } from 'src/app/services/alert.service'; +import { IchambaService } from 'src/app/services/ichamba.service'; +import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'; +import { AddPropertyPage } from '../../property/add-property/add-property.page'; + +@Component({ + selector: 'app-repostulate', + templateUrl: './repostulate.page.html', + styleUrls: ['./repostulate.page.scss'], + standalone: false +}) +export class RepostulateePage implements OnInit { + + @Input() postulation: any; + + properties: any[] = []; + selectedPropertyId: number | null = null; + references: string = ''; + details: string = ''; + photos: string[] = []; + loadingProperties = true; + + constructor( + private modalCtrl: ModalController, + private alertService: AlertService, + private ichambaService: IchambaService, + private loadingCtrl: LoadingController, + private cdr: ChangeDetectorRef, + ) {} + + ngOnInit() { + this.references = this.postulation?.references ?? ''; + this.details = this.postulation?.details ?? ''; + this.loadProperties(); + } + + loadProperties() { + this.ichambaService.getProperties().subscribe({ + next: (data: any[]) => { + this.properties = data; + this.selectedPropertyId = this.postulation?.property_id ?? (data[0]?.id ?? null); + this.loadingProperties = false; + this.cdr.detectChanges(); + if (data.length === 0) this.openAddProperty(); + }, + error: () => { + this.loadingProperties = false; + this.cdr.detectChanges(); + } + }); + } + + async openAddProperty() { + const modal = await this.modalCtrl.create({ component: AddPropertyPage }); + await modal.present(); + const { data } = await modal.onDidDismiss(); + if (data?.property) { + this.properties.push(data.property); + this.selectedPropertyId = data.property.id; + this.cdr.detectChanges(); + } + } + + async addPhoto() { + if (this.photos.length >= 3) { + this.alertService.presentToast('Máximo 3 fotos.'); + return; + } + try { + const image = await Camera.getPhoto({ + quality: 70, + resultType: CameraResultType.Base64, + source: CameraSource.Prompt, + }); + this.photos.push('data:image/jpeg;base64,' + image.base64String); + this.cdr.detectChanges(); + } catch (e) {} + } + + removePhoto(index: number) { + this.photos.splice(index, 1); + this.cdr.detectChanges(); + } + + async submit() { + if (!this.selectedPropertyId) { + this.alertService.presentToast('Selecciona una propiedad.'); + return; + } + const loading = await this.loadingCtrl.create(); + await loading.present(); + + this.ichambaService.cancelPostulation(this.postulation.id).subscribe({ + next: () => { + this.ichambaService.addPostulation( + this.postulation.category, + this.selectedPropertyId!, + this.references, + this.details, + this.photos, + this.postulation.id + ).subscribe({ + next: (data: any) => { + loading.dismiss(); + if (data['message'] === 'No providers') { + this.alertService.presentToast('No hay proveedores disponibles para esta categoría.'); + } else { + this.alertService.presentToast('¡Postulación actualizada con éxito!'); + this.modalCtrl.dismiss({ reposted: true }); + } + }, + error: (error: any) => { + loading.dismiss(); + this.alertService.presentToast('Error al crear la postulación: ' + error['status']); + } + }); + }, + error: (error: any) => { + loading.dismiss(); + this.alertService.presentToast('Error al cancelar la postulación: ' + error['status']); + } + }); + } + + dismiss() { + this.modalCtrl.dismiss(null); + } +} diff --git a/src/app/pages/property/add-property/add-property.module.ts b/src/app/pages/property/add-property/add-property.module.ts new file mode 100644 index 0000000..dca2645 --- /dev/null +++ b/src/app/pages/property/add-property/add-property.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { IonicModule } from '@ionic/angular'; +import { AddPropertyPage } from './add-property.page'; + +@NgModule({ + imports: [CommonModule, FormsModule, IonicModule], + declarations: [AddPropertyPage], + exports: [AddPropertyPage] +}) +export class AddPropertyPageModule {} diff --git a/src/app/pages/property/add-property/add-property.page.html b/src/app/pages/property/add-property/add-property.page.html new file mode 100644 index 0000000..d24e1c6 --- /dev/null +++ b/src/app/pages/property/add-property/add-property.page.html @@ -0,0 +1,63 @@ + + + + + + + + + + Agregar Propiedad + + + + + + + Nombre de la propiedad + + + + + Ícono +
+ + + + + + +
+
+ + + Dirección + + + + + {{ place.description }} + + + + + Número exterior (opcional) + + + + + Guardar propiedad + + +
diff --git a/src/app/pages/property/add-property/add-property.page.scss b/src/app/pages/property/add-property/add-property.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/property/add-property/add-property.page.ts b/src/app/pages/property/add-property/add-property.page.ts new file mode 100644 index 0000000..fd52e27 --- /dev/null +++ b/src/app/pages/property/add-property/add-property.page.ts @@ -0,0 +1,100 @@ +import { Component, NgZone, ChangeDetectorRef } from '@angular/core'; +import { ModalController, LoadingController } from '@ionic/angular'; +import { IchambaService } from 'src/app/services/ichamba.service'; +import { AlertService } from 'src/app/services/alert.service'; + +declare var google: any; + +@Component({ + selector: 'app-add-property', + templateUrl: './add-property.page.html', + styleUrls: ['./add-property.page.scss'], + standalone: false +}) +export class AddPropertyPage { + + name: string = ''; + icon: string = 'home'; + addressAutocomplete: string = ''; + myAddress: string | null = null; + myPosition: any = {}; + myIntnumber: string = ''; + placesSearch: any = null; + showif = true; + saving = false; + + constructor( + private modalCtrl: ModalController, + private loadingCtrl: LoadingController, + private ichambaService: IchambaService, + private alertService: AlertService, + private ngZone: NgZone, + private cdr: ChangeDetectorRef, + ) {} + + autocomplete(ev: any) { + if (!this.addressAutocomplete.trim().length) { + this.placesSearch = null; + return; + } + new google.maps.places.AutocompleteService().getPredictions( + { input: this.addressAutocomplete }, + (predictions: any) => { + this.ngZone.run(() => { + this.placesSearch = predictions; + this.cdr.detectChanges(); + }); + } + ); + } + + geoloc(place_id: string, place_description: string) { + this.myAddress = place_description; + this.addressAutocomplete = place_description; + this.showif = false; + new google.maps.Geocoder().geocode({ placeId: place_id }, (coordinates: any) => { + this.ngZone.run(() => { + this.myPosition = { + latitude: coordinates[0].geometry.location.lat(), + longitude: coordinates[0].geometry.location.lng() + }; + this.cdr.detectChanges(); + }); + }); + } + + async save() { + if (!this.name.trim()) { + this.alertService.presentToast('Ingresa un nombre para la propiedad.'); + return; + } + if (!this.myAddress || !this.myPosition.latitude) { + this.alertService.presentToast('Selecciona una dirección del listado.'); + return; + } + if (!Number.isInteger(parseInt(this.myIntnumber))) { + this.myIntnumber = ''; + } + this.saving = true; + const loading = await this.loadingCtrl.create(); + await loading.present(); + this.ichambaService.addProperty( + this.name, this.myAddress, this.myPosition.latitude, + this.myPosition.longitude, this.myIntnumber, this.icon + ).subscribe({ + next: (property: any) => { + loading.dismiss(); + this.modalCtrl.dismiss({ property }); + }, + error: () => { + loading.dismiss(); + this.saving = false; + this.alertService.presentToast('No se pudo guardar la propiedad.'); + } + }); + } + + dismiss() { + this.modalCtrl.dismiss(); + } +} diff --git a/src/app/pages/team/assign-technician/assign-technician.module.ts b/src/app/pages/team/assign-technician/assign-technician.module.ts new file mode 100644 index 0000000..eedbee4 --- /dev/null +++ b/src/app/pages/team/assign-technician/assign-technician.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { IonicModule } from '@ionic/angular'; +import { AssignTechnicianPage } from './assign-technician.page'; + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + ], + declarations: [AssignTechnicianPage], + exports: [AssignTechnicianPage] +}) +export class AssignTechnicianPageModule {} diff --git a/src/app/pages/team/assign-technician/assign-technician.page.scss b/src/app/pages/team/assign-technician/assign-technician.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/team/assign-technician/assign-technician.page.ts b/src/app/pages/team/assign-technician/assign-technician.page.ts new file mode 100644 index 0000000..3f1e7ae --- /dev/null +++ b/src/app/pages/team/assign-technician/assign-technician.page.ts @@ -0,0 +1,62 @@ +import { Component, Input, OnInit, ChangeDetectorRef } from '@angular/core'; +import { ModalController } from '@ionic/angular'; +import { IchambaService } from 'src/app/services/ichamba.service'; +import { AlertService } from 'src/app/services/alert.service'; + +@Component({ + selector: 'app-assign-technician', + templateUrl: './assign-technician.page.html', + styleUrls: ['./assign-technician.page.scss'], + standalone: false +}) +export class AssignTechnicianPage implements OnInit { + + @Input() contractId: any; + + technicians: any[] = []; + loading = true; + assigning = false; + + constructor( + private modalCtrl: ModalController, + private ichambaService: IchambaService, + private alertService: AlertService, + private cdr: ChangeDetectorRef, + ) {} + + ngOnInit() { + this.ichambaService.getTeamTechnicians().subscribe({ + next: data => { + this.technicians = data; + this.loading = false; + this.cdr.detectChanges(); + }, + error: () => { + this.loading = false; + this.cdr.detectChanges(); + } + }); + } + + assign(technician: any) { + if (this.assigning) return; + this.assigning = true; + this.cdr.detectChanges(); + + this.ichambaService.assignTechnician(this.contractId, technician.id).subscribe({ + next: () => { + this.alertService.presentToast(`${technician.name} asignado al contrato`); + this.modalCtrl.dismiss({ assigned: true, technician }); + }, + error: () => { + this.assigning = false; + this.alertService.presentToast('No se pudo asignar el técnico.'); + this.cdr.detectChanges(); + } + }); + } + + dismiss() { + this.modalCtrl.dismiss(); + } +} diff --git a/src/app/pages/team/team.module.ts b/src/app/pages/team/team.module.ts new file mode 100644 index 0000000..c9e00f7 --- /dev/null +++ b/src/app/pages/team/team.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { Routes, RouterModule } from '@angular/router'; +import { IonicModule } from '@ionic/angular'; +import { TeamPage } from './team.page'; + +const routes: Routes = [ + { path: '', component: TeamPage } +]; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + IonicModule, + RouterModule.forChild(routes) + ], + declarations: [TeamPage] +}) +export class TeamPageModule {} diff --git a/src/app/pages/team/team.page.scss b/src/app/pages/team/team.page.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/services/ichamba.service.ts b/src/app/services/ichamba.service.ts index 3c811fc..9d67c8d 100755 --- a/src/app/services/ichamba.service.ts +++ b/src/app/services/ichamba.service.ts @@ -38,20 +38,42 @@ export class IchambaService { { headers: headers }); } - addCard(token: String, device_id: String) { + getSetupIntent() { const headers = new HttpHeaders({ - 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] }); - return this.http.post(this.env.API_URL + 'payments/addcard', - {token: token, device_id: device_id}, { headers: headers }); + return this.http.post(this.env.API_URL + 'payments/setup-intent', {}, { headers }); } - addPostulation(category: String, address: String, int_number: String, references: String, lat: Number, lng: Number, setdate: String, sethour:String, details: String) { + getPaymentIntent(postulation_id: String, supplier_id: String, coupon: String) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.post(this.env.API_URL + 'payments/intent', + { postulation_id, supplier_id, coupon }, { headers }); + } + + getExtraPaymentIntent(contract_id: Number, amount: Number) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.post(this.env.API_URL + 'payments/extra-intent', + { contract_id, amount }, { headers }); + } + + addPostulation(category: String, property_id: Number, references: String, details: String, photos: string[], related_postulation_id?: Number) { const headers = new HttpHeaders({ 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] }); return this.http.post(this.env.API_URL + 'create-postulation', - {category: category, address: address, int_number: int_number, references: references, lat: lat, lng: lng, setdate: setdate, sethour:sethour, details: details}, { headers: headers }); + { category, property_id, references, details, photos, ...(related_postulation_id ? { related_postulation_id } : {}) }, { headers }); + } + + cancelPostulation(id: any) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.delete(this.env.API_URL + 'postulations/' + id, { headers }); } deleteCard(card_id: String) { @@ -118,12 +140,40 @@ export class IchambaService { { headers: headers }); } - setPostulation(postulation_id: String) { + setPostulation(postulation_id: String, date1: String, date2: String, amount: Number) { const headers = new HttpHeaders({ 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] }); return this.http.post(this.env.API_URL + 'postulate', - {postulation_id: postulation_id}, { headers: headers }); + { postulation_id, date_1: date1, date_2: date2, amount }, { headers }); + } + + getProperties() { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.get(this.env.API_URL + 'properties', { headers }); + } + + addProperty(name: String, address: String, lat: Number, lng: Number, int_number: String, icon: String) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.post(this.env.API_URL + 'properties', { name, address, lat, lng, int_number, icon }, { headers }); + } + + getContractDiscussion(contractId: any) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.get(this.env.API_URL + 'contracts/' + contractId + '/comments', { headers }); + } + + sendContractMessage(contractId: any, comment: string) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.post(this.env.API_URL + 'contracts/' + contractId + '/comments', { comment }, { headers }); } getPostulants(postulation_id: String) { @@ -134,20 +184,20 @@ export class IchambaService { { headers: headers }); } - checkCoupon(postulation_id: String, supplier_id: String, coupon: String) { + checkCoupon(postulation_id: String, supplier_id: String, selected_date: String, coupon: String) { const headers = new HttpHeaders({ 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] }); return this.http.post(this.env.API_URL + 'contracts/coupon', - { postulation_id: postulation_id, supplier_id: supplier_id, coupon: coupon}, { headers: headers }); + { postulation_id, supplier_id, selected_date, coupon }, { headers: headers }); } - createContract(postulation_id: String, supplier_id: String, card_id: String, coupon: String, code: String, device_id: String) { + createContract(postulation_id: String, supplier_id: String, payment_intent_id: String, selected_date: String, coupon: String) { const headers = new HttpHeaders({ 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] }); return this.http.post(this.env.API_URL + 'contracts/create', - { postulation_id: postulation_id, supplier_id: supplier_id, card_id: card_id, coupon: coupon, code: code, device_id: device_id }, { headers: headers }); + { postulation_id, supplier_id, payment_intent_id, selected_date, coupon }, { headers }); } cancelContract(contract_id: String) { @@ -225,12 +275,12 @@ export class IchambaService { { contract_id: contract_id, lat: lat, lng: lng, description: description }, { headers: headers }); } - addExtra(contract_id: Number, amount: Number, card_id: String, code: String, device_id: String) { + addExtra(contract_id: Number, amount: Number, payment_intent_id: String) { const headers = new HttpHeaders({ 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] }); return this.http.post(this.env.API_URL + 'contracts/extra', - { contract_id: contract_id, amount: amount, card_id: card_id, code: code, device_id: device_id }, { headers: headers }); + { contract_id, amount, payment_intent_id }, { headers }); } addHero(name: String, rfc: String, categories: String, tags: String, address: String, lat: Number, lng: Number, bank: Number, bank_account: Number, fee: Number, reference_options: Number, reference: String) { @@ -240,4 +290,43 @@ export class IchambaService { return this.http.post(this.env.API_URL + 'add-hero', {name: name, rfc: rfc, categories: categories, tags: tags, address: address, lat: lat, lng: lng, bank: bank, bank_account: bank_account, fee: fee, reference_options: reference_options, reference: reference}, { headers: headers }); } + + getTeamTechnicians() { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.get(this.env.API_URL + 'technicians', { headers }); + } + + findUserByEmail(email: string) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.get(this.env.API_URL + 'technicians/find?email=' + encodeURIComponent(email), { headers }); + } + + addTechnicianByUserId(user_id: any) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.post(this.env.API_URL + 'technicians/add', { user_id }, { headers }); + } + + removeTechnician(technician_id: any) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.delete(this.env.API_URL + 'technicians/' + technician_id, { headers }); + } + + assignTechnician(contract_id: any, technician_id: any) { + const headers = new HttpHeaders({ + 'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"] + }); + return this.http.post( + this.env.API_URL + 'contracts/assign-technician', + { contract_id, technician_id }, + { headers } + ); + } }