diff --git a/package.json b/package.json index 2103c412c6..7155b82b80 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "planet", "license": "AGPL-3.0", - "version": "0.15.36", + "version": "0.15.37", "myplanet": { "latest": "v0.20.86", "min": "v0.19.86" diff --git a/src/app/community/community.component.ts b/src/app/community/community.component.ts index 5e8fc082bd..479df194d3 100644 --- a/src/app/community/community.component.ts +++ b/src/app/community/community.component.ts @@ -8,6 +8,7 @@ import { DialogsLoadingService } from '../shared/dialogs/dialogs-loading.service import { MatDialog } from '@angular/material/dialog'; import { CommunityLinkDialogComponent } from './community-link-dialog.component'; import { TeamsService } from '../teams/teams.service'; +import { DialogsAnnouncementComponent } from '../shared/dialogs/dialogs-announcement.component'; import { DialogsPromptComponent } from '../shared/dialogs/dialogs-prompt.component'; import { CouchService } from '../shared/couchdb.service'; import { PlanetMessageService } from '../shared/planet-message.service'; @@ -48,6 +49,7 @@ export class CommunityComponent implements OnInit, OnDestroy { resizeCalendar: any = false; deviceType: DeviceType; deviceTypes = DeviceType; + challengeActive: boolean; isLoading: boolean; constructor( @@ -86,6 +88,7 @@ export class CommunityComponent implements OnInit, OnDestroy { this.setCouncillors(users); } }); + this.communityChallenge(); } @HostListener('window:resize') onResize() { @@ -97,6 +100,25 @@ export class CommunityComponent implements OnInit, OnDestroy { this.onDestroy$.complete(); } + communityChallenge() { + const includedCodes = [ 'guatemala', 'san.pablo', 'xela', 'embakasi', 'uriur' ]; + this.challengeActive = includedCodes.includes(this.configuration.code) && + ((new Date() > new Date(2024, 9, 31)) && (new Date() < new Date(2024, 11, 1))); + const popupShown = localStorage.getItem('announcementPopupShown'); + + if (this.challengeActive && !popupShown) { + this.openAnnouncementDialog(); + localStorage.setItem('announcementPopupShown', 'true'); + } + } + + openAnnouncementDialog() { + this.dialog.open(DialogsAnnouncementComponent, { + width: '50vw', + maxHeight: '100vh' + }); + } + getCommunityData() { const setShareTarget = (type) => type === 'center' ? 'nation' : type === 'nation' ? 'community' : undefined; this.route.paramMap.pipe( diff --git a/src/app/dashboard/dashboard.component.html b/src/app/dashboard/dashboard.component.html index 292b296d2b..b385727c58 100644 --- a/src/app/dashboard/dashboard.component.html +++ b/src/app/dashboard/dashboard.component.html @@ -21,6 +21,9 @@

+
+ stars +
this.badgesCourses[group] && this.badgesCourses[group].length); } + openChallengeView() { + this.dialog.open(DialogsAnnouncementComponent, { + width: '50vw', + maxHeight: '100vh' + }); + } + } diff --git a/src/app/shared/database/pouch-auth.service.ts b/src/app/shared/database/pouch-auth.service.ts index 3f3e859b03..363357ca5e 100644 --- a/src/app/shared/database/pouch-auth.service.ts +++ b/src/app/shared/database/pouch-auth.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { from, throwError, Observable, forkJoin } from 'rxjs'; -import { catchError, switchMap } from 'rxjs/operators'; +import { catchError, switchMap, tap } from 'rxjs/operators'; import { PouchService } from './pouch.service'; import { CouchService } from '../couchdb.service'; @@ -32,6 +32,10 @@ export class PouchAuthService { login(username, password) { this.pouchService.configureDBs(); return from(this.authDB.logIn(username, password)).pipe( + tap(() => { + // Reset the popup flag on successful login + localStorage.removeItem('announcementPopupShown'); + }), catchError(this.handleError) ); } diff --git a/src/app/shared/dialogs/dialogs-announcement.component.html b/src/app/shared/dialogs/dialogs-announcement.component.html new file mode 100644 index 0000000000..7cb42a2d72 --- /dev/null +++ b/src/app/shared/dialogs/dialogs-announcement.component.html @@ -0,0 +1,52 @@ +
+ Issues Challenge + +
+
+ + {{ userStatus.joinedCourse ? 'check_circle' : 'radio_button_unchecked' }} + + Únete al curso Reto de noviembre. + + Únete + +
+ +
+ + {{ userStatus.surveyComplete ? 'check_circle' : 'radio_button_unchecked' }} + + ¡Encuesta finalizada! + + Encuesta + +
+ +
+ + {{ userStatus.hasPost ? 'check_circle' : 'radio_button_unchecked' }} + + Comparte tu opinión en Nuestras Voces. + + Voces + +
+
+ +
+
+
+
+ {{ getGoalPercentage() | number:'1.0-2' }}% ({{ '$'+ groupSummary?.length }}) +
+
+
+
+ $0 + $500 +
+
diff --git a/src/app/shared/dialogs/dialogs-announcement.component.scss b/src/app/shared/dialogs/dialogs-announcement.component.scss new file mode 100644 index 0000000000..aec859b3d2 --- /dev/null +++ b/src/app/shared/dialogs/dialogs-announcement.component.scss @@ -0,0 +1,79 @@ +@import '../../variables'; + +.announcement-container { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px; +} + +.announcement-banner { + max-width: 100%; + margin-bottom: 20px; +} + +.steps-container { + width: 100%; + max-width: 600px; + margin-bottom: 20px; +} + +.step { + display: flex; + align-items: center; + margin-bottom: 10px; +} + +.step mat-icon { + margin-right: 10px; +} + +.step span { + flex-grow: 1; +} + +.thermometer-container { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + margin-top: 10px; +} + +.thermometer { + width: 100%; + height: 30px; + background-color: #e0e0e0; + border-radius: 15px; + overflow: hidden; + position: relative; + margin-bottom: 10px; +} + +.thermometer-bar { + height: 100%; + background-color: $primary; + transition: width 0.5s; + display: flex; + justify-content: center; + align-items: center; +} + +.thermometer-label { + color: white; + font-size: 12px; + padding: 2px; + border-radius: 3px; +} + +.thermometer-label.outside { + position: absolute; + left: 0.5rem; +} + +.thermometer-goal { + display: flex; + justify-content: space-between; + width: 100%; + font-size: 14px; +} diff --git a/src/app/shared/dialogs/dialogs-announcement.component.ts b/src/app/shared/dialogs/dialogs-announcement.component.ts new file mode 100644 index 0000000000..f1b141548c --- /dev/null +++ b/src/app/shared/dialogs/dialogs-announcement.component.ts @@ -0,0 +1,179 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { findDocuments } from '../../shared/mangoQueries'; +import { CouchService } from '../couchdb.service'; +import { CoursesService } from '../../courses/courses.service'; +import { NewsService } from '../../news/news.service'; +import { StateService } from '../state.service'; +import { SubmissionsService } from '../../submissions/submissions.service'; +import { UserService } from '../user.service'; +import { planetAndParentId } from '../../manager-dashboard/reports/reports.utils'; + +@Component({ + templateUrl: './dialogs-announcement.component.html', + styleUrls: [ './dialogs-announcement.component.scss' ] +}) +export class DialogsAnnouncementComponent implements OnInit, OnDestroy { + + private onDestroy$ = new Subject(); + currentUserName = this.userService.get().name; + configuration = this.stateService.configuration; + teamId = planetAndParentId(this.stateService.configuration); + submissionsSet = new Set(); + groupSummary = []; + enrolledMembers: any; + courseId = '9517e3b45a5bb63e69bb8f269216974d'; + startDate = new Date(2024, 9, 31); + endDate = new Date(2024, 11, 1); + userStatus = { + joinedCourse: false, + surveyComplete: false, + hasPost: false, + }; + + constructor( + public dialogRef: MatDialogRef, + private router: Router, + private couchService: CouchService, + private coursesService: CoursesService, + private newsService: NewsService, + private stateService: StateService, + private submissionsService: SubmissionsService, + private userService: UserService + ) {} + + ngOnInit() { + const includedCodes = [ 'guatemala', 'san.pablo', 'xela', 'embakasi', 'uriur' ]; + + if (includedCodes.includes(this.configuration.code)) { + this.configuration = this.stateService.configuration; + this.initializeData(); + } + } + + ngOnDestroy() { + this.onDestroy$.next(); + this.onDestroy$.complete(); + } + + onClose(): void { + this.dialogRef.close(); + } + + initializeData() { + this.coursesService.requestCourses(); + this.newsService.requestNews({ + selectors: { + '$or': [ + { messagePlanetCode: this.configuration.code, viewableBy: 'community' }, + { viewIn: { '$elemMatch': { '_id': this.teamId, section: 'community' } } } + ] + }, + viewId: this.teamId + }); + this.fetchCourseAndNews(); + this.fetchEnrolled(); + } + + joinCourse() { + const courseTitle = this.coursesService.getCourseNameFromId(this.courseId); + this.coursesService.courseResignAdmission(this.courseId, 'admission', courseTitle).subscribe((res) => { + this.router.navigate([ '/courses/view', this.courseId ]); + }, (error) => ((error))); + this.dialogRef.close(); + } + + doSurvey() { + this.router.navigate([ `/courses/view/${this.courseId}/step/3` ]); + this.dialogRef.close(); + } + + postVoice() { + this.router.navigate([ '/' ]); + this.dialogRef.close(); + } + + fetchEnrolled() { + this.couchService.findAll('shelf', { + selector: { courseIds: { $elemMatch: { $eq: this.courseId } } }, + }).subscribe((members) => { + this.enrolledMembers = members.map((member: any) => { + const [ , memberName ] = member?._id.split(':'); + return { + ...member, + name: memberName, + }; + }); + }); + } + + hasCompletedSurvey(userName: string) { + return this.submissionsSet.has(userName); + } + + hasSubmittedVoice(news: any[], userName: string) { + return news.some(post => { + return ( + post.doc.user.name === userName && + post.doc.time > this.startDate && + post.doc.time < this.endDate + ); + }); + } + + hasEnrolledCourse(member) { + return member.courseIds.includes(this.courseId); + } + + fetchCourseAndNews() { + this.newsService.newsUpdated$.pipe( + takeUntil(this.onDestroy$) + ).subscribe(news => { + news.map(post => ({ + ...post, + public: ( + (post.doc.viewIn || []).find( + (view) => + view._id === + `${this.configuration.code}@${this.configuration.parentCode}` + ) || {} + ).public, + })); + + this.submissionsService.getSubmissions(findDocuments({ type: 'survey' })) + .subscribe((submissions: any[]) => { + const filteredSubmissions = submissions.filter(submission => submission.parentId.includes(this.courseId)); + this.submissionsSet = new Set(filteredSubmissions.map(submission => submission.user.name)); + + // Group Summary + this.enrolledMembers.forEach((member) => { + const hasCompletedSurvey = this.hasCompletedSurvey(member.name); + const hasPosted = this.hasSubmittedVoice(news, member.name); + const hasJoinedCourse = this.hasEnrolledCourse(member); + + if (hasCompletedSurvey && hasPosted && hasJoinedCourse) { + this.groupSummary.push(member); + } + }); + + // Individual stats + this.userStatus.surveyComplete = this.hasCompletedSurvey(this.currentUserName); + this.userStatus.hasPost = this.hasSubmittedVoice(news, this.currentUserName); + this.enrolledMembers.some(member => { + if (member.name === this.currentUserName) { + this.userStatus.joinedCourse = this.hasEnrolledCourse(member); + } + }); + }); + }); + } + + getGoalPercentage(): number { + const goal = 500; + return (this.groupSummary?.length / goal) * 100; + } +} diff --git a/src/app/shared/dialogs/planet-dialogs.module.ts b/src/app/shared/dialogs/planet-dialogs.module.ts index 879ce81aa5..0a7f384566 100644 --- a/src/app/shared/dialogs/planet-dialogs.module.ts +++ b/src/app/shared/dialogs/planet-dialogs.module.ts @@ -16,6 +16,7 @@ import { SharedComponentsModule } from '../shared-components.module'; import { SyncDirective } from '../../manager-dashboard/sync.directive'; import { DialogsImagesComponent } from './dialogs-images.component'; import { DialogsVideoComponent } from './dialogs-video.component'; +import { DialogsAnnouncementComponent } from './dialogs-announcement.component'; import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-ratings.component'; @@ -40,7 +41,8 @@ import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-rati DialogsRatingsComponent, DialogsRatingsDirective, ChangePasswordDirective, - SyncDirective + SyncDirective, + DialogsAnnouncementComponent, ], declarations: [ DialogsFormComponent, @@ -54,7 +56,8 @@ import { DialogsRatingsComponent, DialogsRatingsDirective } from './dialogs-rati DialogsRatingsComponent, DialogsRatingsDirective, ChangePasswordDirective, - SyncDirective + SyncDirective, + DialogsAnnouncementComponent ], providers: [ DialogsFormService,