feat: Reportes, chat de discusión y mejoras de navegación

- Tab "Reportados" en Contratos (cliente) y tab "Reportadas" en Postulaciones (proveedor) con skeleton loading e ionViewWillEnter
- Modal de chat para discusión de reportes: burbujas por rol (propio/otro/moderador), skeleton, input con cámara y envío
- Endpoints GET/POST contracts/reports/{id}/comments en ichamba.service
- userId guardado en AuthService desde auth/user para identificar mensajes propios
- Botón "Postularse" corregido con slot=end en ion-item
- Todas las secciones de tabs migradas de ngOnInit a ionViewWillEnter + ChangeDetectorRef.detectChanges()
- Android navigation bar reactiva al tema del sistema vía values/values-night styles.xml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 21:05:24 -06:00
parent aa8b0061c9
commit c677fdcb59
34 changed files with 901 additions and 102 deletions

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
<item name="android:navigationBarColor">#000000</item>
<item name="android:windowLightNavigationBar">false</item>
</style>
</resources>

View File

@@ -13,6 +13,8 @@
<item name="windowActionBar">false</item> <item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
<item name="android:background">@null</item> <item name="android:background">@null</item>
<item name="android:navigationBarColor">#FFFFFF</item>
<item name="android:windowLightNavigationBar">true</item>
</style> </style>

View File

@@ -27,7 +27,6 @@ const routes: Routes = [
{ path: 'start', loadChildren: () => import('./pages/contracts/start/start.module').then(m => m.StartPageModule), canActivate: [AuthGuard]}, { path: 'start', loadChildren: () => import('./pages/contracts/start/start.module').then(m => m.StartPageModule), canActivate: [AuthGuard]},
{ path: 'review/:contract_id', loadChildren: () => import('./pages/contracts/review/review.module').then(m => m.ReviewPageModule), canActivate: [AuthGuard] }, { path: 'review/:contract_id', loadChildren: () => import('./pages/contracts/review/review.module').then(m => m.ReviewPageModule), canActivate: [AuthGuard] },
{ path: 'report/:contract_id', loadChildren: () => import('./pages/contracts/report/report.module').then(m => m.ReportPageModule), canActivate: [AuthGuard] }, { path: 'report/:contract_id', loadChildren: () => import('./pages/contracts/report/report.module').then(m => m.ReportPageModule), canActivate: [AuthGuard] },
{ path: 'reports', loadChildren: () => import('./pages/reports/reports.module').then(m => m.ReportsPageModule), canActivate: [AuthGuard]},
{ path: 'nohome/:contract_id', loadChildren: () => import('./pages/contracts/nohome/nohome.module').then(m => m.NohomePageModule), canActivate: [AuthGuard] }, { path: 'nohome/:contract_id', loadChildren: () => import('./pages/contracts/nohome/nohome.module').then(m => m.NohomePageModule), canActivate: [AuthGuard] },
{ path: 'extra', loadChildren: () => import('./pages/contracts/extra/extra.module').then(m => m.ExtraPageModule), canActivate: [AuthGuard] }, { path: 'extra', loadChildren: () => import('./pages/contracts/extra/extra.module').then(m => m.ExtraPageModule), canActivate: [AuthGuard] },
{ path: 'ended', loadChildren: () => import('./pages/postulations/ended/ended.module').then(m => m.EndedPageModule), canActivate: [AuthGuard] }, { path: 'ended', loadChildren: () => import('./pages/postulations/ended/ended.module').then(m => m.EndedPageModule), canActivate: [AuthGuard] },

View File

@@ -11,6 +11,24 @@
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)"> <ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content> <ion-refresher-content></ion-refresher-content>
</ion-refresher> </ion-refresher>
<ng-container *ngIf="loading">
<ion-card *ngFor="let _ of [1,2]">
<ion-item style="--border-color: #fff">
<ion-label>
<ion-skeleton-text [animated]="true" style="width: 55%; height: 18px; margin-bottom: 8px"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 85%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 70%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 50%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 40%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 35%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 65%; height: 16px; margin-top: 6px"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-card>
</ng-container>
<ng-container *ngIf="!loading">
<ng-container *ngFor="let ccontract of ccontracts; let i = index"> <ng-container *ngFor="let ccontract of ccontracts; let i = index">
<ion-card *ngIf="ccontract.status==1"> <ion-card *ngIf="ccontract.status==1">
<ion-item style="--border-color: #fff"> <ion-item style="--border-color: #fff">
@@ -29,4 +47,5 @@
<ion-button *ngIf="ccontract.past_due < 0" style="height: 4.0em; padding-right: 1.0em; padding-bottom:1.0em; float: right" color="danger" (click)="cancelContract(ccontract.id, ccontract.date)">{{'contracts.cancel_1.1' | translate}}<br>{{'contracts.cancel_1.2' | translate}}</ion-button> <ion-button *ngIf="ccontract.past_due < 0" style="height: 4.0em; padding-right: 1.0em; padding-bottom:1.0em; float: right" color="danger" (click)="cancelContract(ccontract.id, ccontract.date)">{{'contracts.cancel_1.1' | translate}}<br>{{'contracts.cancel_1.2' | translate}}</ion-button>
</ion-card> </ion-card>
</ng-container> </ng-container>
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, ChangeDetectorRef } from '@angular/core';
import { ModalController, MenuController, NavController } from '@ionic/angular'; import { ModalController, MenuController, NavController } from '@ionic/angular';
import { EventService } from '../../../services/event.service'; import { EventService } from '../../../services/event.service';
import { EnvService } from 'src/app/services/env.service'; import { EnvService } from 'src/app/services/env.service';
@@ -15,11 +15,12 @@ import { AlertController } from '@ionic/angular';
styleUrls: ['./contracted.page.scss'], styleUrls: ['./contracted.page.scss'],
standalone: false standalone: false
}) })
export class ContractedPage implements OnInit { export class ContractedPage {
ccontracts: any[] = []; ccontracts: any[] = [];
ccontracts_dates: any[] = []; ccontracts_dates: any[] = [];
lang: boolean = false; lang: boolean = false;
loading = true;
constructor( constructor(
private modalController: ModalController, private modalController: ModalController,
@@ -33,32 +34,29 @@ export class ContractedPage implements OnInit {
private translateService: TranslateService, private translateService: TranslateService,
private languageService: LanguageService, private languageService: LanguageService,
private env: EnvService, private env: EnvService,
private cdr: ChangeDetectorRef,
) { ) {
this.events.subscribe('refreshccontracts', (data) => { this.events.subscribe('refreshccontracts', (data) => {
this.getccontracts(); this.getccontracts();
}); });
} }
ngOnInit() { ionViewWillEnter() {
this.lang = this.languageService.getDefaultLanguage() === 'es';
this.loading = true;
this.getccontracts(); this.getccontracts();
if (this.languageService.getDefaultLanguage() == 'es') {
this.lang = true;
} else {
this.lang = false
}
} }
refresh(event: any) { refresh(event: any) {
this.ichambaService.getCurrentcontracts().subscribe( this.ichambaService.getCurrentcontracts().subscribe(
data => { data => {
this.ccontracts = data; this.ccontracts = data;
this.ccontracts_dates = [];
for (var i of this.ccontracts) { for (var i of this.ccontracts) {
if (this.languageService.getDefaultLanguage() == 'es') { const locale = this.lang ? 'es-US' : 'en-US';
this.ccontracts_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.ccontracts_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'long' }));
} else {
this.ccontracts_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.cdr.detectChanges();
event.target.complete(); event.target.complete();
}, error => { }, error => {
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
@@ -70,14 +68,15 @@ export class ContractedPage implements OnInit {
this.ichambaService.getCurrentcontracts().subscribe( this.ichambaService.getCurrentcontracts().subscribe(
data => { data => {
this.ccontracts = data; this.ccontracts = data;
this.ccontracts_dates = [];
for (var i of this.ccontracts) { for (var i of this.ccontracts) {
if (this.languageService.getDefaultLanguage() == 'es') { const locale = this.lang ? 'es-US' : 'en-US';
this.ccontracts_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.ccontracts_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'long' }));
} else {
this.ccontracts_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.loading = false;
this.cdr.detectChanges();
}, error => { }, error => {
this.loading = false;
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
}); });
} }

View File

@@ -33,6 +33,10 @@ const routes: Routes = [
{ {
path: 'finished', path: 'finished',
loadChildren: () => import('./finished/finished.module').then(m => m.FinishedPageModule) loadChildren: () => import('./finished/finished.module').then(m => m.FinishedPageModule)
},
{
path: 'reported',
loadChildren: () => import('../reports/reports.module').then(m => m.ReportsPageModule)
} }
] ]
} }

View File

@@ -24,5 +24,10 @@
<ion-label>{{'contracts.header_3' | translate}}</ion-label> <ion-label>{{'contracts.header_3' | translate}}</ion-label>
</ion-tab-button> </ion-tab-button>
<ion-tab-button tab="reported">
<ion-icon name="flag"></ion-icon>
<ion-label>{{'contracts.header_4' | translate}}</ion-label>
</ion-tab-button>
</ion-tab-bar> </ion-tab-bar>
</ion-tabs> </ion-tabs>

View File

@@ -3,7 +3,7 @@
<ion-buttons slot="start"> <ion-buttons slot="start">
<ion-menu-button></ion-menu-button> <ion-menu-button></ion-menu-button>
</ion-buttons> </ion-buttons>
<ion-title>{{'contract.header' | translate}}</ion-title> <ion-title>{{'contracts.header' | translate}}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding"> <ion-content class="ion-padding">
@@ -11,6 +11,24 @@
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)"> <ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content> <ion-refresher-content></ion-refresher-content>
</ion-refresher> </ion-refresher>
<ng-container *ngIf="loading">
<ion-card *ngFor="let _ of [1,2]">
<ion-item style="--border-color: #fff">
<ion-label>
<ion-skeleton-text [animated]="true" style="width: 55%; height: 18px; margin-bottom: 8px"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 85%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 70%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 50%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 40%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 35%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 65%; height: 16px; margin-top: 6px"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-card>
</ng-container>
<ng-container *ngIf="!loading">
<ng-container *ngFor="let fcontract of fcontracts; let i = index"> <ng-container *ngFor="let fcontract of fcontracts; let i = index">
<ion-card> <ion-card>
<ion-item style="--border-color: #fff"> <ion-item style="--border-color: #fff">
@@ -35,4 +53,5 @@
</ion-row> </ion-row>
</ion-card> </ion-card>
</ng-container> </ng-container>
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, ChangeDetectorRef } from '@angular/core';
import { ReviewPage } from '../review/review.page'; import { ReviewPage } from '../review/review.page';
import { ModalController, MenuController, NavController } from '@ionic/angular'; import { ModalController, MenuController, NavController } from '@ionic/angular';
import { EventService } from '../../../services/event.service'; import { EventService } from '../../../services/event.service';
@@ -17,11 +17,12 @@ import { AlertController } from '@ionic/angular';
styleUrls: ['./finished.page.scss'], styleUrls: ['./finished.page.scss'],
standalone: false standalone: false
}) })
export class FinishedPage implements OnInit { export class FinishedPage {
fcontracts: any[] = []; fcontracts: any[] = [];
fcontracts_dates: any[] = []; fcontracts_dates: any[] = [];
lang: boolean = false; lang: boolean = false;
loading = true;
constructor( constructor(
private modalController: ModalController, private modalController: ModalController,
@@ -37,32 +38,29 @@ export class FinishedPage implements OnInit {
private translateService: TranslateService, private translateService: TranslateService,
private languageService: LanguageService, private languageService: LanguageService,
private env: EnvService, private env: EnvService,
private cdr: ChangeDetectorRef,
) { ) {
this.events.subscribe('refreshccontracts', (data) => { this.events.subscribe('refreshccontracts', (data) => {
this.getfcontracts(); this.getfcontracts();
}); });
} }
ngOnInit() { ionViewWillEnter() {
this.lang = this.languageService.getDefaultLanguage() === 'es';
this.loading = true;
this.getfcontracts(); this.getfcontracts();
if (this.languageService.getDefaultLanguage() == 'es') {
this.lang = true;
} else {
this.lang = false
}
} }
refresh(event: any) { refresh(event: any) {
this.ichambaService.getFinishedcontracts().subscribe( this.ichambaService.getFinishedcontracts().subscribe(
data => { data => {
this.fcontracts = data; this.fcontracts = data;
this.fcontracts_dates = [];
for (var i of this.fcontracts) { for (var i of this.fcontracts) {
if (this.languageService.getDefaultLanguage() == 'es') { const locale = this.lang ? 'es-US' : 'en-US';
this.fcontracts_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.fcontracts_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'long' }));
} else {
this.fcontracts_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.cdr.detectChanges();
event.target.complete(); event.target.complete();
}, error => { }, error => {
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
@@ -74,14 +72,15 @@ export class FinishedPage implements OnInit {
this.ichambaService.getFinishedcontracts().subscribe( this.ichambaService.getFinishedcontracts().subscribe(
data => { data => {
this.fcontracts = data; this.fcontracts = data;
this.fcontracts_dates = [];
for (var i of this.fcontracts) { for (var i of this.fcontracts) {
if (this.languageService.getDefaultLanguage() == 'es') { const locale = this.lang ? 'es-US' : 'en-US';
this.fcontracts_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.fcontracts_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'long' }));
} else {
this.fcontracts_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.loading = false;
this.cdr.detectChanges();
}, error => { }, error => {
this.loading = false;
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
}); });
} }

View File

@@ -11,6 +11,24 @@
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)"> <ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content> <ion-refresher-content></ion-refresher-content>
</ion-refresher> </ion-refresher>
<ng-container *ngIf="loading">
<ion-card *ngFor="let _ of [1,2]">
<ion-item style="--border-color: #fff">
<ion-label>
<ion-skeleton-text [animated]="true" style="width: 55%; height: 18px; margin-bottom: 8px"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 85%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 70%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 50%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 40%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 35%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 65%; height: 16px; margin-top: 6px"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-card>
</ng-container>
<ng-container *ngIf="!loading">
<ion-card *ngFor="let pcontract of pcontracts; let i = index"> <ion-card *ngFor="let pcontract of pcontracts; let i = index">
<ion-item> <ion-item>
<ion-label> <ion-label>
@@ -22,4 +40,5 @@
<ion-button style="height: 3em; padding-left: 0.5em;" color="secondary" (click)="viewsuppliers(pcontract.id)">{{'contracts.viewsuppliers_1.1' | translate}}<br>{{'contracts.viewsuppliers_1.2' | translate}}</ion-button> <ion-button style="height: 3em; padding-left: 0.5em;" color="secondary" (click)="viewsuppliers(pcontract.id)">{{'contracts.viewsuppliers_1.1' | translate}}<br>{{'contracts.viewsuppliers_1.2' | translate}}</ion-button>
</ion-item> </ion-item>
</ion-card> </ion-card>
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, ChangeDetectorRef } from '@angular/core';
import { ModalController, MenuController, NavController } from '@ionic/angular'; import { ModalController, MenuController, NavController } from '@ionic/angular';
import { EventService } from '../../../services/event.service'; import { EventService } from '../../../services/event.service';
import { EnvService } from 'src/app/services/env.service'; import { EnvService } from 'src/app/services/env.service';
@@ -14,11 +14,12 @@ import { AlertService } from 'src/app/services/alert.service';
styleUrls: ['./pending.page.scss'], styleUrls: ['./pending.page.scss'],
standalone: false standalone: false
}) })
export class PendingPage implements OnInit { export class PendingPage {
pcontracts: any[] = []; pcontracts: any[] = [];
pcontracts_dates: any[] = []; pcontracts_dates: any[] = [];
lang: boolean = false; lang: boolean = false;
loading = true;
constructor( constructor(
private modalController: ModalController, private modalController: ModalController,
@@ -31,32 +32,29 @@ export class PendingPage implements OnInit {
private translateService: TranslateService, private translateService: TranslateService,
private languageService: LanguageService, private languageService: LanguageService,
private env: EnvService, private env: EnvService,
private cdr: ChangeDetectorRef,
) { ) {
this.events.subscribe('refreshpcontracts', (data) => { this.events.subscribe('refreshpcontracts', (data) => {
this.getpcontracts(); this.getpcontracts();
}); });
} }
ngOnInit() { ionViewWillEnter() {
this.lang = this.languageService.getDefaultLanguage() === 'es';
this.loading = true;
this.getpcontracts(); this.getpcontracts();
if (this.languageService.getDefaultLanguage() == 'es') {
this.lang = true;
} else {
this.lang = false
}
} }
refresh(event: any) { refresh(event: any) {
this.ichambaService.getPendingcontracts().subscribe( this.ichambaService.getPendingcontracts().subscribe(
data => { data => {
this.pcontracts = data; this.pcontracts = data;
this.pcontracts_dates = [];
for (var i of this.pcontracts) { for (var i of this.pcontracts) {
if (this.languageService.getDefaultLanguage() == 'es') { const locale = this.lang ? 'es-US' : 'en-US';
this.pcontracts_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.pcontracts_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'long' }));
} else {
this.pcontracts_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.cdr.detectChanges();
event.target.complete(); event.target.complete();
}, error => { }, error => {
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
@@ -68,14 +66,15 @@ export class PendingPage implements OnInit {
this.ichambaService.getPendingcontracts().subscribe( this.ichambaService.getPendingcontracts().subscribe(
data => { data => {
this.pcontracts = data; this.pcontracts = data;
this.pcontracts_dates = [];
for (var i of this.pcontracts) { for (var i of this.pcontracts) {
if (this.languageService.getDefaultLanguage() == 'es') { const locale = this.lang ? 'es-US' : 'en-US';
this.pcontracts_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.pcontracts_dates.push(new Date((new Date(i.date).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', timeZoneName: 'long' }));
} else {
this.pcontracts_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.loading = false;
this.cdr.detectChanges();
}, error => { }, error => {
this.loading = false;
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']); this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
}); });
} }

View File

@@ -11,6 +11,24 @@
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)"> <ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content> <ion-refresher-content></ion-refresher-content>
</ion-refresher> </ion-refresher>
<ng-container *ngIf="loading">
<ion-card *ngFor="let _ of [1,2]">
<ion-item style="--border-color: #fff">
<ion-label>
<ion-skeleton-text [animated]="true" style="width: 55%; height: 18px; margin-bottom: 8px"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 85%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 40%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 70%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 75%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 35%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 80%"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-card>
</ng-container>
<ng-container *ngIf="!loading">
<ng-container *ngFor="let postulation of postulations; let i = index"> <ng-container *ngFor="let postulation of postulations; let i = index">
<ion-card> <ion-card>
<ion-item> <ion-item>
@@ -26,4 +44,5 @@
</ion-item> </ion-item>
</ion-card> </ion-card>
</ng-container> </ng-container>
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, ChangeDetectorRef } from '@angular/core';
import { MenuController, NavController } from '@ionic/angular'; import { MenuController, NavController } from '@ionic/angular';
import { EventService } from '../../../services/event.service'; import { EventService } from '../../../services/event.service';
import { EnvService } from 'src/app/services/env.service'; import { EnvService } from 'src/app/services/env.service';
@@ -13,10 +13,11 @@ import { Browser } from '@capacitor/browser';
styleUrls: ['./already.page.scss'], styleUrls: ['./already.page.scss'],
standalone: false standalone: false
}) })
export class AlreadyPage implements OnInit { export class AlreadyPage {
postulations: any[] = []; postulations: any[] = [];
postulations_dates: any[] = []; postulations_dates: any[] = [];
loading = true;
constructor( constructor(
private menu: MenuController, private menu: MenuController,
@@ -26,13 +27,15 @@ export class AlreadyPage implements OnInit {
private alertService: AlertService, private alertService: AlertService,
private ichambaService: IchambaService, private ichambaService: IchambaService,
private env: EnvService, private env: EnvService,
private cdr: ChangeDetectorRef,
) { ) {
this.events.subscribe('refreshpostulations', (data) => { this.events.subscribe('refreshpostulations', (data) => {
this.getpostulations(); this.getpostulations();
}); });
} }
ngOnInit() { ionViewWillEnter() {
this.loading = true;
this.getpostulations(); this.getpostulations();
} }
@@ -40,9 +43,11 @@ export class AlreadyPage implements OnInit {
this.ichambaService.getContractedPostulation().subscribe( this.ichambaService.getContractedPostulation().subscribe(
data => { data => {
this.postulations = data; this.postulations = data;
this.postulations_dates = [];
for (var i of this.postulations) { 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.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.cdr.detectChanges();
event.target.complete(); event.target.complete();
}, error => { }, error => {
this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']);
@@ -54,10 +59,14 @@ export class AlreadyPage implements OnInit {
this.ichambaService.getContractedPostulation().subscribe( this.ichambaService.getContractedPostulation().subscribe(
data => { data => {
this.postulations = data; this.postulations = data;
this.postulations_dates = [];
for (var i of this.postulations) { 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.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.loading = false;
this.cdr.detectChanges();
}, error => { }, error => {
this.loading = false;
this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']);
}); });
} }

View File

@@ -11,6 +11,24 @@
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)"> <ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content> <ion-refresher-content></ion-refresher-content>
</ion-refresher> </ion-refresher>
<ng-container *ngIf="loading">
<ion-card *ngFor="let _ of [1,2]">
<ion-item style="--border-color: #fff">
<ion-label>
<ion-skeleton-text [animated]="true" style="width: 55%; height: 18px; margin-bottom: 8px"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 85%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 70%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 75%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 80%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 45%"></ion-skeleton-text>
</ion-label>
<ion-skeleton-text slot="end" [animated]="true" style="width: 72px; height: 36px; border-radius: 4px"></ion-skeleton-text>
</ion-item>
</ion-card>
</ng-container>
<ng-container *ngIf="!loading">
<ng-container *ngFor="let postulation of postulations; let i = index"> <ng-container *ngFor="let postulation of postulations; let i = index">
<ion-card *ngIf="!postulation.already_post"> <ion-card *ngIf="!postulation.already_post">
<ion-item> <ion-item>
@@ -22,8 +40,9 @@
<p class="ion-text-wrap">Detalles: {{postulation.details}}</p> <p class="ion-text-wrap">Detalles: {{postulation.details}}</p>
<p class="ion-text-wrap">Tiempo restante: {{postulation.time_limit}} minutos</p> <p class="ion-text-wrap">Tiempo restante: {{postulation.time_limit}} minutos</p>
</ion-label> </ion-label>
<ion-button style="height: 3em; padding-left: 0.5em;" color="secondary" (click)="addpostulation(postulation.id)">Postularse</ion-button> <ion-button slot="end" style="height: 3em;" color="secondary" (click)="addpostulation(postulation.id)">Postularse</ion-button>
</ion-item> </ion-item>
</ion-card> </ion-card>
</ng-container> </ng-container>
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, ChangeDetectorRef } from '@angular/core';
import { MenuController, NavController } from '@ionic/angular'; import { MenuController, NavController } from '@ionic/angular';
import { EventService } from '../../../services/event.service'; import { EventService } from '../../../services/event.service';
import { EnvService } from 'src/app/services/env.service'; import { EnvService } from 'src/app/services/env.service';
@@ -13,10 +13,11 @@ import { Browser } from '@capacitor/browser';
styleUrls: ['./current.page.scss'], styleUrls: ['./current.page.scss'],
standalone: false standalone: false
}) })
export class CurrentPage implements OnInit { export class CurrentPage {
postulations: any[] = []; postulations: any[] = [];
postulations_dates: any[] = []; postulations_dates: any[] = [];
loading = true;
constructor( constructor(
private menu: MenuController, private menu: MenuController,
@@ -26,13 +27,15 @@ export class CurrentPage implements OnInit {
private alertService: AlertService, private alertService: AlertService,
private ichambaService: IchambaService, private ichambaService: IchambaService,
private env: EnvService, private env: EnvService,
private cdr: ChangeDetectorRef,
) { ) {
this.events.subscribe('refreshpostulations', (data) => { this.events.subscribe('refreshpostulations', (data) => {
this.getpostulations(); this.getpostulations();
}); });
} }
ngOnInit() { ionViewWillEnter() {
this.loading = true;
this.getpostulations(); this.getpostulations();
} }
@@ -40,9 +43,11 @@ export class CurrentPage implements OnInit {
this.ichambaService.getPostulation().subscribe( this.ichambaService.getPostulation().subscribe(
data => { data => {
this.postulations = data; this.postulations = data;
this.postulations_dates = [];
for (var i of this.postulations) { 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.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.cdr.detectChanges();
event.target.complete(); event.target.complete();
}, error => { }, error => {
this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']);
@@ -54,10 +59,14 @@ export class CurrentPage implements OnInit {
this.ichambaService.getPostulation().subscribe( this.ichambaService.getPostulation().subscribe(
data => { data => {
this.postulations = data; this.postulations = data;
this.postulations_dates = [];
for (var i of this.postulations) { 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.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.loading = false;
this.cdr.detectChanges();
}, error => { }, error => {
this.loading = false;
this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']);
}); });
} }

View File

@@ -11,6 +11,24 @@
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)"> <ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content> <ion-refresher-content></ion-refresher-content>
</ion-refresher> </ion-refresher>
<ng-container *ngIf="loading">
<ion-card *ngFor="let _ of [1,2]">
<ion-item style="--border-color: #fff">
<ion-label>
<ion-skeleton-text [animated]="true" style="width: 55%; height: 18px; margin-bottom: 8px"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 85%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 40%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 70%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 75%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 35%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 80%"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-card>
</ng-container>
<ng-container *ngIf="!loading">
<ng-container *ngFor="let postulation of postulations; let i = index"> <ng-container *ngFor="let postulation of postulations; let i = index">
<ion-card> <ion-card>
<ion-item> <ion-item>
@@ -24,4 +42,5 @@
</ion-item> </ion-item>
</ion-card> </ion-card>
</ng-container> </ng-container>
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, ChangeDetectorRef } from '@angular/core';
import { ModalController, MenuController, NavController } from '@ionic/angular'; import { ModalController, MenuController, NavController } from '@ionic/angular';
import { EventService } from '../../../services/event.service'; import { EventService } from '../../../services/event.service';
import { EnvService } from 'src/app/services/env.service'; import { EnvService } from 'src/app/services/env.service';
@@ -12,10 +12,11 @@ import { AlertService } from 'src/app/services/alert.service';
styleUrls: ['./ended.page.scss'], styleUrls: ['./ended.page.scss'],
standalone: false standalone: false
}) })
export class EndedPage implements OnInit { export class EndedPage {
postulations: any[] = []; postulations: any[] = [];
postulations_dates: any[] = []; postulations_dates: any[] = [];
loading = true;
constructor( constructor(
private modalController: ModalController, private modalController: ModalController,
@@ -26,13 +27,15 @@ export class EndedPage implements OnInit {
private alertService: AlertService, private alertService: AlertService,
private ichambaService: IchambaService, private ichambaService: IchambaService,
private env: EnvService, private env: EnvService,
private cdr: ChangeDetectorRef,
) { ) {
this.events.subscribe('refreshpostulations', (data) => { this.events.subscribe('refreshpostulations', (data) => {
this.getfinishedpostulations(); this.getfinishedpostulations();
}); });
} }
ngOnInit() { ionViewWillEnter() {
this.loading = true;
this.getfinishedpostulations(); this.getfinishedpostulations();
} }
@@ -40,9 +43,11 @@ export class EndedPage implements OnInit {
this.ichambaService.getFinishedPostulation().subscribe( this.ichambaService.getFinishedPostulation().subscribe(
data => { data => {
this.postulations = data; this.postulations = data;
this.postulations_dates = [];
for (var i of this.postulations) { 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.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.cdr.detectChanges();
event.target.complete(); event.target.complete();
}, error => { }, error => {
this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']);
@@ -54,10 +59,14 @@ export class EndedPage implements OnInit {
this.ichambaService.getFinishedPostulation().subscribe( this.ichambaService.getFinishedPostulation().subscribe(
data => { data => {
this.postulations = data; this.postulations = data;
this.postulations_dates = [];
for (var i of this.postulations) { 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.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.loading = false;
this.cdr.detectChanges();
}, error => { }, error => {
this.loading = false;
this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']); this.alertService.presentToast("Por favor contacte a soporte técnico, Estatus:" + error['status']);
}); });
} }

View File

@@ -28,6 +28,10 @@ const routes: Routes = [
{ {
path: 'ended', path: 'ended',
loadChildren: () => import('./ended/ended.module').then(m => m.EndedPageModule) loadChildren: () => import('./ended/ended.module').then(m => m.EndedPageModule)
},
{
path: 'reported',
loadChildren: () => import('./reported/reported.module').then(m => m.PostulationReportedPageModule)
} }
] ]
} }

View File

@@ -24,5 +24,10 @@
<ion-label>Finalizadas</ion-label> <ion-label>Finalizadas</ion-label>
</ion-tab-button> </ion-tab-button>
<ion-tab-button tab="reported">
<ion-icon name="flag"></ion-icon>
<ion-label>Reportadas</ion-label>
</ion-tab-button>
</ion-tab-bar> </ion-tab-bar>
</ion-tabs> </ion-tabs>

View File

@@ -0,0 +1,26 @@
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 { PostulationReportedPage } from './reported.page';
import { ReportDiscussionPageModule } from '../../report-discussion/report-discussion.module';
const routes: Routes = [
{
path: '',
component: PostulationReportedPage
}
];
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
RouterModule.forChild(routes),
ReportDiscussionPageModule
],
declarations: [PostulationReportedPage]
})
export class PostulationReportedPageModule {}

View File

@@ -0,0 +1,53 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>Postulaciones</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h2 class="ion-text-capitalize" style="padding-bottom: 0.5em">Reportadas</h2>
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>
<ng-container *ngIf="loading">
<ion-card *ngFor="let _ of [1,2]">
<ion-item style="--border-color: #fff">
<ion-label>
<ion-skeleton-text [animated]="true" style="width: 55%; height: 18px; margin-bottom: 8px"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 50%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 85%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 70%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 35%"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-card>
</ng-container>
<ng-container *ngIf="!loading">
<p *ngIf="reports.length === 0" class="ion-text-center ion-padding-top">
No tienes postulaciones reportadas
</p>
<ng-container *ngFor="let report of reports; let i = index">
<ion-card>
<ion-item style="--border-color: #fff">
<ion-label>
<h2 class="ion-text-capitalize">{{ report.category }}</h2>
<p class="ion-text-wrap ion-text-capitalize">Cliente: {{ report.client }}</p>
<p class="ion-text-wrap">{{ report.address }}</p>
<p class="ion-text-wrap ion-text-capitalize">{{ reports_dates[i] }}</p>
<p>Monto: ${{ report.amount }}</p>
</ion-label>
<ion-button color="secondary" fill="outline" slot="end" (click)="viewDiscussion(report.id)">
<ion-icon slot="icon-only" name="chatbubbles-outline"></ion-icon>
</ion-button>
</ion-item>
</ion-card>
</ng-container>
</ng-container>
</ion-content>

View File

@@ -0,0 +1,77 @@
import { Component, 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';
import { ReportDiscussionPage } from '../../report-discussion/report-discussion.page';
@Component({
selector: 'app-postulation-reported',
templateUrl: './reported.page.html',
styleUrls: ['./reported.page.scss'],
standalone: false
})
export class PostulationReportedPage {
reports: any[] = [];
reports_dates: string[] = [];
loading = true;
constructor(
private alertService: AlertService,
private ichambaService: IchambaService,
private cdr: ChangeDetectorRef,
private modalCtrl: ModalController,
) { }
ionViewWillEnter() {
this.loading = true;
this.loadReports();
}
refresh(event: any) {
this.ichambaService.getPostulationReports().subscribe({
next: data => {
this.reports = data;
this.buildDates();
this.cdr.detectChanges();
event.target.complete();
},
error: error => {
this.alertService.presentToast('Por favor contacte a soporte técnico, Estatus:' + error['status']);
event.target.complete();
}
});
}
loadReports() {
this.ichambaService.getPostulationReports().subscribe({
next: data => {
this.reports = data;
this.buildDates();
this.loading = false;
this.cdr.detectChanges();
},
error: error => {
this.loading = false;
this.alertService.presentToast('Por favor contacte a soporte técnico, Estatus:' + error['status']);
}
});
}
async viewDiscussion(reportId: any) {
const modal = await this.modalCtrl.create({
component: ReportDiscussionPage,
componentProps: { reportId },
});
await modal.present();
}
private buildDates() {
this.reports_dates = this.reports.map(r =>
new Date((new Date(r.appointment).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString('es-US', {
weekday: 'long', year: 'numeric', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric', timeZoneName: 'long'
})
);
}
}

View File

@@ -0,0 +1,16 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { ReportDiscussionPage } from './report-discussion.page';
@NgModule({
imports: [
CommonModule,
FormsModule,
IonicModule,
],
declarations: [ReportDiscussionPage],
exports: [ReportDiscussionPage]
})
export class ReportDiscussionPageModule {}

View File

@@ -0,0 +1,94 @@
<ion-header>
<ion-toolbar color="primary">
<ion-buttons slot="start">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="arrow-back"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title>Discusión del Reporte</ion-title>
</ion-toolbar>
</ion-header>
<ion-content #content>
<div class="messages-container">
<ng-container *ngIf="loading">
<div class="message-wrapper left">
<ion-skeleton-text [animated]="true" style="width: 55%; height: 54px; border-radius: 18px;"></ion-skeleton-text>
</div>
<div class="message-wrapper right">
<ion-skeleton-text [animated]="true" style="width: 42%; height: 40px; border-radius: 18px;"></ion-skeleton-text>
</div>
<div class="message-wrapper center">
<ion-skeleton-text [animated]="true" style="width: 64%; height: 48px; border-radius: 10px;"></ion-skeleton-text>
</div>
<div class="message-wrapper left">
<ion-skeleton-text [animated]="true" style="width: 50%; height: 36px; border-radius: 18px;"></ion-skeleton-text>
</div>
<div class="message-wrapper right">
<ion-skeleton-text [animated]="true" style="width: 60%; height: 60px; border-radius: 18px;"></ion-skeleton-text>
</div>
</ng-container>
<div class="empty-state" *ngIf="!loading && messages.length === 0">
<ion-icon name="chatbubbles-outline" style="font-size: 48px; margin-bottom: 12px;"></ion-icon>
<p>No hay mensajes aún.</p>
</div>
<ng-container *ngIf="!loading">
<ng-container *ngFor="let msg of messages">
<!-- Moderador — centrado -->
<div class="message-wrapper center" *ngIf="msg.sender_type === 'moderator'">
<div class="message-bubble moderator">
<span class="sender-name">Moderador</span>
<p>{{ msg.comment }}</p>
<span class="timestamp">{{ msg.created_at | date:'d MMM, HH:mm' }}</span>
</div>
</div>
<!-- Mensaje propio — derecha -->
<div class="message-wrapper right"
*ngIf="msg.sender_type !== 'moderator' && msg.sender_id === currentUserId">
<div class="message-bubble mine">
<p>{{ msg.comment }}</p>
<span class="timestamp">{{ msg.created_at | date:'d MMM, HH:mm' }}</span>
</div>
</div>
<!-- Mensaje del otro — izquierda -->
<div class="message-wrapper left"
*ngIf="msg.sender_type !== 'moderator' && msg.sender_id !== currentUserId">
<div class="message-bubble other">
<span class="sender-name">{{ msg.sender_name }}</span>
<p>{{ msg.comment }}</p>
<span class="timestamp">{{ msg.created_at | date:'d MMM, HH:mm' }}</span>
</div>
</div>
</ng-container>
</ng-container>
</div>
</ion-content>
<ion-footer>
<div class="input-bar">
<ion-button fill="clear" color="medium" (click)="attachPhoto()">
<ion-icon slot="icon-only" name="camera-outline"></ion-icon>
</ion-button>
<ion-textarea
[(ngModel)]="newMessage"
placeholder="Escribe un comentario..."
rows="1"
autoGrow="true"
></ion-textarea>
<ion-button
fill="clear"
color="primary"
[disabled]="!newMessage.trim() || sending"
(click)="sendMessage()">
<ion-icon slot="icon-only" name="send"></ion-icon>
</ion-button>
</div>
</ion-footer>

View File

@@ -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 6px;
gap: 2px;
background: var(--ion-background-color, #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-light);
border-radius: 20px;
margin: 0;
max-height: 120px;
}
ion-button {
--padding-start: 6px;
--padding-end: 6px;
min-height: 40px;
}
}

View File

@@ -0,0 +1,80 @@
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-report-discussion',
templateUrl: './report-discussion.page.html',
styleUrls: ['./report-discussion.page.scss'],
standalone: false
})
export class ReportDiscussionPage implements OnInit {
@Input() reportId: 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.getReportDiscussion(this.reportId).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.sendReportMessage(this.reportId, 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.');
}
});
}
attachPhoto() {
// TODO: implementar captura y envío de foto
}
dismiss() {
this.modalCtrl.dismiss();
}
}

View File

@@ -4,8 +4,10 @@ import { FormsModule } from '@angular/forms';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { IonicModule } from '@ionic/angular'; import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { ReportsPage } from './reports.page'; import { ReportsPage } from './reports.page';
import { ReportDiscussionPageModule } from '../report-discussion/report-discussion.module';
const routes: Routes = [ const routes: Routes = [
{ {
@@ -19,7 +21,9 @@ const routes: Routes = [
CommonModule, CommonModule,
FormsModule, FormsModule,
IonicModule, IonicModule,
RouterModule.forChild(routes) RouterModule.forChild(routes),
TranslateModule,
ReportDiscussionPageModule
], ],
declarations: [ReportsPage] declarations: [ReportsPage]
}) })

View File

@@ -1,9 +1,53 @@
<ion-header> <ion-header>
<ion-toolbar> <ion-toolbar color="primary">
<ion-title>reports</ion-title> <ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>{{'contracts.header' | translate}}</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content> <ion-content class="ion-padding">
<h2 class="ion-text-capitalize" style="padding-bottom: 0.5em">{{'contracts.header_4' | translate}}</h2>
<ion-refresher slot="fixed" (ionRefresh)="refresh($event)">
<ion-refresher-content></ion-refresher-content>
</ion-refresher>
<ng-container *ngIf="loading">
<ion-card *ngFor="let _ of [1,2]">
<ion-item style="--border-color: #fff">
<ion-label>
<ion-skeleton-text [animated]="true" style="width: 55%; height: 18px; margin-bottom: 8px"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 85%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 70%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 50%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 40%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 35%"></ion-skeleton-text>
<ion-skeleton-text [animated]="true" style="width: 65%; height: 16px; margin-top: 6px"></ion-skeleton-text>
</ion-label>
</ion-item>
</ion-card>
</ng-container>
<ng-container *ngIf="!loading">
<ng-container *ngFor="let report of reports; let i = index">
<ion-card>
<ion-item style="--border-color: #fff">
<ion-label>
<h2 class="ion-text-capitalize" *ngIf="lang">{{ report.category }}</h2>
<h2 class="ion-text-capitalize" *ngIf="!lang">{{ report.en_category }}</h2>
<p class="ion-text-wrap ion-text-capitalize">{{'contracts.supplier' | translate}}: {{ report.supplier }}</p>
<p class="ion-text-wrap ion-text-capitalize" *ngIf="report.company">{{'reports.company' | translate}}: {{ report.company }}</p>
<p class="ion-text-wrap">{{ report.address }}</p>
<p class="ion-text-wrap ion-text-capitalize">{{ reports_dates[i] }}</p>
<p>{{'contracts.amount' | translate}}: ${{ report.amount }}</p>
</ion-label>
<ion-button color="secondary" fill="outline" slot="end" size="large" (click)="viewDiscussion(report.id)">
<ion-icon slot="icon-only" name="chatbubbles-outline"></ion-icon>
</ion-button>
</ion-item>
</ion-card>
</ng-container>
</ng-container>
</ion-content> </ion-content>

View File

@@ -1,4 +1,10 @@
import { Component, OnInit } from '@angular/core'; import { Component, ChangeDetectorRef } from '@angular/core';
import { ModalController } from '@ionic/angular';
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 { ReportDiscussionPage } from '../report-discussion/report-discussion.page';
@Component({ @Component({
selector: 'app-reports', selector: 'app-reports',
@@ -6,11 +12,80 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./reports.page.scss'], styleUrls: ['./reports.page.scss'],
standalone: false standalone: false
}) })
export class ReportsPage implements OnInit { export class ReportsPage {
constructor() { } reports: any[] = [];
reports_dates: string[] = [];
lang: boolean = false;
loading = true;
ngOnInit() { constructor(
private alertService: AlertService,
private ichambaService: IchambaService,
private translateService: TranslateService,
private languageService: LanguageService,
private cdr: ChangeDetectorRef,
private modalCtrl: ModalController,
) { }
ionViewWillEnter() {
this.lang = this.languageService.getDefaultLanguage() === 'es';
this.loading = true;
this.loadReports();
} }
refresh(event: any) {
this.reports = [];
this.reports_dates = [];
this.ichambaService.getReports().subscribe({
next: data => {
this.reports = data;
this.buildDates();
event.target.complete();
},
error: error => {
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
event.target.complete();
}
});
}
loadReports() {
this.ichambaService.getReports().subscribe({
next: data => {
this.reports = data;
this.buildDates();
this.loading = false;
this.cdr.detectChanges();
},
error: error => {
this.loading = false;
this.alertService.presentToast(this.translateService.instant('alerts.error') + error['status']);
}
});
}
async viewDiscussion(reportId: any) {
const modal = await this.modalCtrl.create({
component: ReportDiscussionPage,
componentProps: { reportId },
});
await modal.present();
}
getStatusColor(status: string): string {
if (status === 'resuelto') return 'success';
if (status === 'en revision' || status === 'en revisión') return 'primary';
return 'warning';
}
private buildDates() {
const locale = this.lang ? 'es-US' : 'en-US';
this.reports_dates = this.reports.map(r =>
new Date((new Date(r.appointment).toLocaleString('en-US') + ' UTC').replace(',', '')).toLocaleDateString(locale, {
weekday: 'long', year: 'numeric', month: 'short', day: 'numeric',
hour: 'numeric', minute: 'numeric', timeZoneName: 'long'
})
);
}
} }

View File

@@ -20,6 +20,7 @@ export class AuthService {
isReported = false; isReported = false;
userName: string = ''; userName: string = '';
userRole: number = 0; userRole: number = 0;
userId: any = null;
role: any; role: any;
token: any; token: any;
userInfo: any; userInfo: any;
@@ -140,6 +141,7 @@ export class AuthService {
} }
this.userName = user['name'] ?? ''; this.userName = user['name'] ?? '';
this.userRole = user['role_id'] ?? 0; this.userRole = user['role_id'] ?? 0;
this.userId = user['id'] ?? null;
this.events.publish('set_role', user['role_id']); this.events.publish('set_role', user['role_id']);
console.log("after login:", user); console.log("after login:", user);
}) })

View File

@@ -181,6 +181,34 @@ export class IchambaService {
{ contract_id: contract_id, comment: comment }, { headers: headers }); { contract_id: contract_id, comment: comment }, { headers: headers });
} }
getReports() {
const headers = new HttpHeaders({
'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"]
});
return this.http.get<any[]>(this.env.API_URL + 'contracts/reports', { headers: headers });
}
getPostulationReports() {
const headers = new HttpHeaders({
'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"]
});
return this.http.get<any[]>(this.env.API_URL + 'postulations/reports', { headers: headers });
}
getReportDiscussion(reportId: any) {
const headers = new HttpHeaders({
'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"]
});
return this.http.get<any[]>(this.env.API_URL + 'contracts/reports/' + reportId + '/comments', { headers });
}
sendReportMessage(reportId: any, comment: string) {
const headers = new HttpHeaders({
'Authorization': this.authService.token["token_type"]+" "+this.authService.token["access_token"]
});
return this.http.post<any>(this.env.API_URL + 'contracts/reports/' + reportId + '/comments', { comment: comment }, { headers });
}
noHomeCheck() { noHomeCheck() {
const headers = new HttpHeaders({ 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"]

View File

@@ -44,7 +44,7 @@
"dashboard": { "dashboard": {
"header": "Find services", "header": "Find services",
"searchbox_placeholder": "Enter the service you are looking for", "searchbox_placeholder": "Enter the service you are looking for",
"slogan": "Everything begins with a search", "slogan": "It all starts with a search",
"welcome": "Welcome" "welcome": "Welcome"
}, },
@@ -62,6 +62,7 @@
"header_1": "Postulated", "header_1": "Postulated",
"header_2": "Confirmed", "header_2": "Confirmed",
"header_3": "Finished", "header_3": "Finished",
"header_4": "Reported",
"parent": "Extra charge from service: ", "parent": "Extra charge from service: ",
"status": "Status", "status": "Status",
"rate": "Rate", "rate": "Rate",
@@ -73,7 +74,7 @@
"validate": "Validate", "validate": "Validate",
"hire_info_1": "You are about to hire", "hire_info_1": "You are about to hire",
"hire_info_2": "to provide the service you have requested.", "hire_info_2": "to provide the service you have requested.",
"hire_pay": "Please select with which card do you want to pay the service and enter the CVV of the card to continue.", "hire_pay": "Please select which card do you want to pay the service with, and enter the CVV of the card to continue.",
"hire_confirm": "Hire service", "hire_confirm": "Hire service",
"coupon": "Coupon", "coupon": "Coupon",
"no_home": "Not at home", "no_home": "Not at home",
@@ -178,6 +179,12 @@
"faq_9.1": "Phone numbers so you can contact each other and the user names." "faq_9.1": "Phone numbers so you can contact each other and the user names."
}, },
"reports": {
"header": "My Reports",
"company": "Company",
"empty": "You have no reports registered"
},
"alerts": { "alerts": {
"login": "Logged In", "login": "Logged In",
"login_error": "Wrong email or password", "login_error": "Wrong email or password",

View File

@@ -62,6 +62,7 @@
"header_1": "Postulados", "header_1": "Postulados",
"header_2": "Confirmados", "header_2": "Confirmados",
"header_3": "Finalizados", "header_3": "Finalizados",
"header_4": "Reportados",
"parent": "Fondo extra del servicio: ", "parent": "Fondo extra del servicio: ",
"status": "Estado", "status": "Estado",
"rate": "Calificar", "rate": "Calificar",
@@ -178,6 +179,12 @@
"faq_9.1": "El celular para que puedan contactarse y el nombre de usuario." "faq_9.1": "El celular para que puedan contactarse y el nombre de usuario."
}, },
"reports": {
"header": "Mis Reportes",
"company": "Empresa",
"empty": "No tienes reportes registrados"
},
"alerts": { "alerts": {
"login": "Sesión iniciada", "login": "Sesión iniciada",
"login_error": "Email o contraseña incorrectos", "login_error": "Email o contraseña incorrectos",