diff --git a/.github/workflows/app-test.yml b/.github/workflows/app-test.yml index cd55bb8e..ac764a59 100644 --- a/.github/workflows/app-test.yml +++ b/.github/workflows/app-test.yml @@ -4,6 +4,9 @@ on: branches: - main - dev + push: + branches: + - dev jobs: test: diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml deleted file mode 100644 index 5fca6e2d..00000000 --- a/.github/workflows/firebase-hosting-pull-request.yml +++ /dev/null @@ -1,37 +0,0 @@ -# This file was auto-generated by the Firebase CLI -# https://github.com/firebase/firebase-tools - -name: Deploy to Firebase Hosting on PR -'on': pull_request -jobs: - build_and_preview: - if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: "18" - - name: Install Dependencies - working-directory: ./app/WhereIsThePower - run: npm install --frozen-lock - - name: Update env - uses: cschleiden/replace-tokens@v1.2 - with: - tokenPrefix: '{' - tokenSuffix: '}' - files: ./app/WhereIsThePower/src/environments/environment.prod.ts - env: - MapboxApiKey: ${{ secrets.TEST_SECRET }} - - name: Build app - working-directory: ./app/WhereIsThePower - run: npm run build - - uses: FirebaseExtended/action-hosting-deploy@v0 - with: - working-directory: ./app/WhereIsThePower - repoToken: '${{ secrets.GITHUB_TOKEN }}' - firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_WHEREISTHEPOWER_33A66 }}' - projectId: whereisthepower-33a66 diff --git a/.github/workflows/firebase_dev.yml b/.github/workflows/firebase_dev.yml new file mode 100644 index 00000000..76328a0f --- /dev/null +++ b/.github/workflows/firebase_dev.yml @@ -0,0 +1,42 @@ +name: Build and Deploy app to DEV site +'on': + push: + branches: + - dev + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Install Dependencies + working-directory: ./app/WhereIsThePower + run: npm install --frozen-lock + + - name: Inject Developer tags + run: | + sed -i 's||DEVELOPER SITE|g' app/WhereIsThePower/src/app/app.component.html + sed -i 's|Where Is The Power|<title>WITP-DEV|g' app/WhereIsThePower/src/index.html + sed -i 's|href="assets/icon/favicon.ico"|href="assets/Ramp.svg"|g' app/WhereIsThePower/src/index.html + + - name: update Prod ENV file + run: | + sed -i 's/HelloAPIKey/${{ secrets.MAPBOX_API_KEY }}/g' app/WhereIsThePower/src/environments/environment.prod.ts + + - name: Build app + working-directory: ./app/WhereIsThePower + run: npm run build + + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_WHEREISTHEPOWER_33A66 }}' + channelId: live + projectId: whereisthepower-33a66 + target: dev + entrypoint: ./app/WhereIsThePower \ No newline at end of file diff --git a/.github/workflows/firebase_prod.yml b/.github/workflows/firebase_prod.yml new file mode 100644 index 00000000..e5f2af18 --- /dev/null +++ b/.github/workflows/firebase_prod.yml @@ -0,0 +1,39 @@ +name: Build and Deploy app to PROD site +'on': + push: + branches: + - main + +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Install Dependencies + working-directory: ./app/WhereIsThePower + run: npm install --frozen-lock + + - name: Remove consolelogs from production site + run: echo "if(window) { window.console.log = function() {}; }" >> app/WhereIsThePower/src/main.ts + + - name: update Prod ENV file + run: | + sed -i 's/HelloAPIKey/${{ secrets.MAPBOX_API_KEY }}/g' app/WhereIsThePower/src/environments/environment.prod.ts + + - name: Build app + working-directory: ./app/WhereIsThePower + run: npm run build + + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_WHEREISTHEPOWER_33A66 }}' + channelId: live + projectId: whereisthepower-33a66 + target: prod + entrypoint: ./app/WhereIsThePower \ No newline at end of file diff --git a/api/src/ai.rs b/api/src/ai.rs index bfcf37e5..eaba8701 100644 --- a/api/src/ai.rs +++ b/api/src/ai.rs @@ -9,7 +9,7 @@ use utoipa::ToSchema; #[utoipa::path(post, tag = "AI", path = "/api/ai/info", request_body = AiInfoRequest)] #[post("/ai/info", format = "application/json", data = "<request>")] pub async fn get_ai_info<'a>(request: Json<AiInfoRequest>) -> ApiResponse<'a, AiInfoResponse> { - let mapbox_api_key = if let Ok(key) = env::var("MAPBOX_API_KEY") { + let mapbox_api_key = if let Ok(key) = dbg!(env::var("MAPBOX_API_KEY")) { key } else { log::error!("We couldn't get the mapbox api key. The environment variable was not set!"); @@ -21,11 +21,12 @@ pub async fn get_ai_info<'a>(request: Json<AiInfoRequest>) -> ApiResponse<'a, Ai match Command::new("python3") .args([ - "src/avoid_cords.py", + "route.py", serde_json::to_string(&request.into_inner()) .unwrap() .as_ref(), ]) + .current_dir("src") .stdout(Stdio::piped()) .env("MAPBOX_API_KEY", mapbox_api_key) .output() @@ -56,19 +57,21 @@ pub async fn get_ai_info<'a>(request: Json<AiInfoRequest>) -> ApiResponse<'a, Ai #[derive(Clone, Deserialize, Serialize, ToSchema)] #[schema(example = json! { AiInfoRequest { - polygon: vec![ - [0.0, 0.0], - [90.0, -90.0], - [-90.0, 90.0], - ] + origin: Box::new([28.3, -27.73]), + destination: Box::new([28.2651, -25.7597]) } })] pub struct AiInfoRequest { - pub polygon: Vec<[f64; 2]>, + pub origin: Box<[f64; 2]>, + pub destination: Box<[f64; 2]>, } #[derive(Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct AiInfoResponse { - pub coords_to_avoid: Vec<[f64; 2]>, + pub duration: f32, + pub distance: f32, + pub traffic_lights_avoided: Vec<[f32; 2]>, + pub instructions: Vec<String>, + pub coordinates: Vec<[f32; 2]>, } diff --git a/api/src/loadshedding.rs b/api/src/loadshedding.rs index de111bf1..2f69b3c9 100644 --- a/api/src/loadshedding.rs +++ b/api/src/loadshedding.rs @@ -16,6 +16,7 @@ use rocket::{ fairing::{self, Fairing, Info, Kind}, futures::{future::try_join_all, TryStreamExt}, post, + get, serde::json::Json, Orbit, Rocket, State, }; @@ -193,6 +194,22 @@ pub async fn fetch_time_for_polygon<'a>( } } +#[utoipa::path(get, path = "/api/fetchCurrentStage")] +#[get("/fetchCurrentStage")] +pub async fn get_current_stage<'a>( + loadshedding_stage: &State<Option<Arc<RwLock<LoadSheddingStage>>>>, +) -> ApiResponse<'a, i32> { + // query end + ApiResponse::Ok(loadshedding_stage + .inner() + .as_ref() + .clone() + .unwrap() + .read() + .await + .stage) +} + #[derive(Debug, Serialize, Deserialize, Clone, Entity)] #[serde(rename_all = "camelCase")] #[collection_name = "stage_log"] diff --git a/api/src/main.rs b/api/src/main.rs index 0c61baa1..5a9135cb 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -40,6 +40,7 @@ const DB_NAME: &'static str = "wip"; #[openapi( paths( user::create_user, + loadshedding::get_current_stage, loadshedding::fetch_map_data, loadshedding::fetch_schedule, loadshedding::fetch_suburb_stats, @@ -182,6 +183,10 @@ async fn get_config() -> Figment { } async fn build_rocket() -> Rocket<Build> { + if let Err(err) = dotenvy::dotenv() { + warn!("Couldn't read .env file! {err:?}"); + } + let figment = get_config().await; let db_uri = env::var("DATABASE_URI").unwrap_or(String::from("")); // Cors Options, we should modify to our needs but leave as default for now. @@ -218,6 +223,7 @@ async fn build_rocket() -> Rocket<Build> { routes!( auth::authenticate, user::create_user, + loadshedding::get_current_stage, loadshedding::fetch_map_data, loadshedding::fetch_suburb_stats, loadshedding::fetch_schedule, @@ -253,6 +259,7 @@ async fn build_rocket() -> Rocket<Build> { routes!( auth::authenticate, user::create_user, + loadshedding::get_current_stage, loadshedding::fetch_map_data, loadshedding::fetch_suburb_stats, loadshedding::fetch_schedule, @@ -285,9 +292,6 @@ async fn build_rocket() -> Rocket<Build> { async fn main() -> Result<(), rocket::Error> { setup_logger().expect("Couldn't setup logger!"); - if let Err(err) = dotenvy::dotenv() { - warn!("Couldn't read .env file! {err:?}"); - } if let Err(err) = dns::update_dns().await { warn!("Couldn't setup DNS: {err:?}"); } diff --git a/api/src/route.py b/api/src/route.py index 32a07a57..aa2f63c3 100644 --- a/api/src/route.py +++ b/api/src/route.py @@ -1,7 +1,7 @@ import sys import requests import json - +import os congestion_levels = { "low": 1, diff --git a/api/src/tests.rs b/api/src/tests.rs index 7ba28574..24da71cf 100644 --- a/api/src/tests.rs +++ b/api/src/tests.rs @@ -190,7 +190,10 @@ async fn test_ai_endpoint() { let response = client .post(format!("/api{}", uri!(super::ai::get_ai_info))) .header(ContentType::JSON) - .json(&AiInfoRequest { polygon: vec![] }) + .json(&AiInfoRequest { + origin: Box::new([28.3, -27.73]), + destination: Box::new([28.2651, -25.7597]), + }) .dispatch() .await; diff --git a/app/WhereIsThePower/.firebaserc b/app/WhereIsThePower/.firebaserc index fec127cf..4b3d3c98 100644 --- a/app/WhereIsThePower/.firebaserc +++ b/app/WhereIsThePower/.firebaserc @@ -1,5 +1,18 @@ { "projects": { "default": "whereisthepower-33a66" - } -} + }, + "targets": { + "whereisthepower-33a66": { + "hosting": { + "prod": [ + "whereisthepower-33a66" + ], + "dev": [ + "whereisthepowerdev" + ] + } + } + }, + "etags": {} +} \ No newline at end of file diff --git a/app/WhereIsThePower/android/app/src/main/AndroidManifest.xml b/app/WhereIsThePower/android/app/src/main/AndroidManifest.xml index e8b35355..e869a45a 100644 --- a/app/WhereIsThePower/android/app/src/main/AndroidManifest.xml +++ b/app/WhereIsThePower/android/app/src/main/AndroidManifest.xml @@ -38,5 +38,6 @@ <!-- Permissions --> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-feature android:name="android.hardware.location.gps" /> <uses-permission android:name="android.permission.INTERNET" /> </manifest> diff --git a/app/WhereIsThePower/firebase.json b/app/WhereIsThePower/firebase.json index 2aa9fa3f..34b769ae 100644 --- a/app/WhereIsThePower/firebase.json +++ b/app/WhereIsThePower/firebase.json @@ -1,5 +1,51 @@ { - "hosting": { + "hosting":[ + { + "target": "prod", + "public": "www", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ], + "headers": [ + { + "source": "**", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache, no-store, must-revalidate" + } + ] + }, + { + "source": "**/*.@(jpg|jpeg|gif|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|font.css)", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache" + } + ] + }, + { + "source": "ngsw-worker.js", + "headers": [ + { + "key": "Cache-Control", + "value": "no-cache" + } + ] + } + ] + }, + { + "target": "dev", "public": "www", "ignore": [ "firebase.json", @@ -42,4 +88,5 @@ } ] } +] } diff --git a/app/WhereIsThePower/package.json b/app/WhereIsThePower/package.json index 5be267f2..10d11925 100644 --- a/app/WhereIsThePower/package.json +++ b/app/WhereIsThePower/package.json @@ -57,7 +57,7 @@ "@angular/compiler": "^16.0.0", "@angular/compiler-cli": "^16.0.0", "@angular/language-service": "^16.0.0", - "@capacitor/assets": "^2.0.4", + "@capacitor/assets": "^3.0.0", "@capacitor/cli": "5.2.2", "@cypress/schematic": "^2.5.0", "@ionic/angular-toolkit": "^9.0.0", diff --git a/app/WhereIsThePower/src/app/app.component.html b/app/WhereIsThePower/src/app/app.component.html index bb471727..f173ab2c 100644 --- a/app/WhereIsThePower/src/app/app.component.html +++ b/app/WhereIsThePower/src/app/app.component.html @@ -32,6 +32,8 @@ <h2>Where is the Power?</h2> <ion-icon slot="start" name="person-circle"></ion-icon> <ion-label>Profile</ion-label> </ion-item> + + <!-- DEVELOPER SITE INJECTION --> </ion-menu-toggle> </ion-list> </ion-content> diff --git a/app/WhereIsThePower/src/app/report/report.page.html b/app/WhereIsThePower/src/app/report/report.page.html index e7fe158e..c7d8e12b 100644 --- a/app/WhereIsThePower/src/app/report/report.page.html +++ b/app/WhereIsThePower/src/app/report/report.page.html @@ -42,7 +42,7 @@ <h3>Reporting is only available to registered users. </h3> </ion-icon> </ion-card-content> </ion-card> - <ion-text>Substation Blew</ion-text> + <ion-label><h5>Substation Blew</h5></ion-label> </ion-col> <ion-col size="6" size-xl="3" class="ion-text-center ion-margin-bottom"> <ion-card (click)="report('CablesStolen')"> @@ -52,7 +52,7 @@ <h3>Reporting is only available to registered users. </h3> </ion-icon> </ion-card-content> </ion-card> - <ion-text>Cables Stolen</ion-text> + <ion-label>Cables Stolen</ion-label> </ion-col> <ion-col size="6" size-xl="3" class="ion-text-center ion-margin-bottom"> <ion-card (click)="report('PowerOutage')"> @@ -62,7 +62,7 @@ <h3>Reporting is only available to registered users. </h3> </ion-icon> </ion-card-content> </ion-card> - <ion-text>Power Outage</ion-text> + <ion-label>Power Outage</ion-label> </ion-col> <ion-col size="6" size-xl="3" offset-xl="1.5" class="ion-text-center ion-margin-bottom"> <ion-card (click)="report('CablesDamaged')"> @@ -72,7 +72,7 @@ <h3>Reporting is only available to registered users. </h3> </ion-icon> </ion-card-content> </ion-card> - <ion-text>Cables Damaged</ion-text> + <ion-label>Cables Damaged</ion-label> </ion-col> <ion-col size="6" size-xl="3" class="ion-text-center ion-margin-bottom"> <ion-card (click)="report('Traffic')"> @@ -82,7 +82,7 @@ <h3>Reporting is only available to registered users. </h3> </ion-icon> </ion-card-content> </ion-card> - <ion-text>Traffic</ion-text> + <ion-label>Traffic</ion-label> </ion-col> </ion-row> </ion-grid> diff --git a/app/WhereIsThePower/src/app/report/report.page.spec.ts b/app/WhereIsThePower/src/app/report/report.page.spec.ts deleted file mode 100644 index ff5d1b3f..00000000 --- a/app/WhereIsThePower/src/app/report/report.page.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReportPage } from './report.page'; - -describe('ReportPage', () => { - let component: ReportPage; - let fixture: ComponentFixture<ReportPage>; - - beforeEach(async(() => { - fixture = TestBed.createComponent(ReportPage); - component = fixture.componentInstance; - fixture.detectChanges(); - })); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/app/WhereIsThePower/src/app/report/report.page.ts b/app/WhereIsThePower/src/app/report/report.page.ts index c7ec63fc..17e98812 100644 --- a/app/WhereIsThePower/src/app/report/report.page.ts +++ b/app/WhereIsThePower/src/app/report/report.page.ts @@ -20,9 +20,7 @@ export class ReportPage implements OnInit { ) { } ngOnInit() { - this.reportService.getReports().subscribe((data) => { - console.log(data); - }); + } async ionViewWillEnter() { diff --git a/app/WhereIsThePower/src/app/report/report.service.spec.ts b/app/WhereIsThePower/src/app/report/report.service.spec.ts deleted file mode 100644 index ea0ae5ad..00000000 --- a/app/WhereIsThePower/src/app/report/report.service.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { ReportService } from './report.service'; - -describe('ReportService', () => { - let service: ReportService; - - beforeEach(() => { - TestBed.configureTestingModule({}); - service = TestBed.inject(ReportService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); -}); diff --git a/app/WhereIsThePower/src/app/report/report.service.ts b/app/WhereIsThePower/src/app/report/report.service.ts index e0ba2bfd..b0dfbcbd 100644 --- a/app/WhereIsThePower/src/app/report/report.service.ts +++ b/app/WhereIsThePower/src/app/report/report.service.ts @@ -13,7 +13,7 @@ export class ReportService { private headers: HttpHeaders = new HttpHeaders(); latitude: any; longitude: any; - reports: BehaviorSubject<any> = new BehaviorSubject<any>(false); + reports: BehaviorSubject<any[]> = new BehaviorSubject<any[]>([]); constructor( private http: HttpClient, @@ -25,23 +25,36 @@ export class ReportService { getReports() { this.headers = this.authService.getAuthHeaders(); // get the auth headers return this.http.get(this.apiUrl, { headers: this.headers }).pipe(tap((res: any) => { - this.reports.next(res); - console.log(this.reports); + this.reports.next(res.result); + console.log("getReports (service file)", res.result); })); } reportIssue(type: string) { + this.headers = this.authService.getAuthHeaders(); // get the auth headers + // Get the current user location this.latitude = this.userLocationService.getLatitude(); this.longitude = this.userLocationService.getLongitude(); - - let body = + let report = { "report_type": type, "timestamp": Date.now(), "latitude": this.latitude, "longitude": this.longitude } - return this.http.post(this.apiUrl, body, { headers: this.headers }); + return this.http.post(this.apiUrl, report, { headers: this.headers}).pipe(tap((res: any) => { + if(res) { + let currentReports = this.reports.getValue(); + console.log("===================================="); + console.log(" this.reports", this.reports); + console.log("currentReports", currentReports); + console.log("report", report); + + console.log("===================================="); + + this.reports.next([...currentReports, report]); + } + })); } } diff --git a/app/WhereIsThePower/src/app/shared/components/login/login.component.html b/app/WhereIsThePower/src/app/shared/components/login/login.component.html index 1ef0da83..3ef8b44b 100644 --- a/app/WhereIsThePower/src/app/shared/components/login/login.component.html +++ b/app/WhereIsThePower/src/app/shared/components/login/login.component.html @@ -14,19 +14,43 @@ <ion-content [fullscreen]="true"> - <div class="ion-padding"> - <form class="form" [formGroup]="loginForm" (ngSubmit)="login()"> - <ion-item data-cy="login-email-input"> - <ion-label position="floating">Email</ion-label> - <ion-input type="email" formControlName="email" aria-label="Input for email"></ion-input> - </ion-item> + <ion-grid> + <ion-row class="ion-text-center ion-padding-top"> + <ion-col size = "3" offset = "4.5" size-md = "2" offset-md = "5" class="ion-text-center"> + <ion-avatar class="ion-no-margin" class="ion-text-center" style="display:inline"> + <ion-img src="assets/WhereIsThePower.png"></ion-img> + </ion-avatar> + </ion-col> + </ion-row> + <ion-row> + <ion-col class="ion-text-center"> + <form class="form" [formGroup]="loginForm" (ngSubmit)="login()" class="ion-text-center"> + <h3 class="ion-padding-start"><ion-text color="primary">Where Is the Power?</ion-text></h3> - <ion-item data-cy="login-password-input"> - <ion-label position="floating" >Password</ion-label> - <ion-input type="password" formControlName="password" aria-label="Input for password"></ion-input> - </ion-item> + <ion-item> + <ion-input + label = "Email" + labelPlacement="floating" + placeholder="Email" + formControlName = "email" + ></ion-input> + </ion-item> - <ion-button expand="full" type="submit" data-cy="btn-login-confirm">Login</ion-button> - </form> - </div> + <ion-item class="ion-padding-top"> + <ion-input + type = "password" + label = "Password" + labelPlacement="floating" + placeholder="Password" + formControlName = "password" + autocomplete="off" + ></ion-input> + </ion-item> + <div class="ion-padding-top"> + <ion-button expand="block" type="submit" class="ion-margin-top ion-margin-start" data-cy="btn-login-confirm">Login</ion-button> + </div> + </form> + </ion-col> + </ion-row> + </ion-grid> </ion-content> diff --git a/app/WhereIsThePower/src/app/shared/components/login/login.component.ts b/app/WhereIsThePower/src/app/shared/components/login/login.component.ts index 221c98bb..03c5a71b 100644 --- a/app/WhereIsThePower/src/app/shared/components/login/login.component.ts +++ b/app/WhereIsThePower/src/app/shared/components/login/login.component.ts @@ -5,6 +5,7 @@ import { ToastController } from '@ionic/angular'; import { User } from '../../models/user'; import { AuthService } from '../../../authentication/auth.service'; import { ModalController } from '@ionic/angular'; +import { LoadingController } from '@ionic/angular'; @Component({ selector: 'app-login', @@ -24,7 +25,7 @@ export class LoginComponent implements OnInit { loginForm: FormGroup = this.formBuilder.group({ email: ['', [Validators.required, Validators.email]], - password: ['', [Validators.required, Validators.minLength(8)]], + password: ['', [Validators.required]], }); constructor( @@ -32,7 +33,8 @@ export class LoginComponent implements OnInit { private formBuilder: FormBuilder, private toastController: ToastController, private authService: AuthService, - public modalController: ModalController + public modalController: ModalController, + private loadingController: LoadingController ) { } @@ -42,11 +44,12 @@ export class LoginComponent implements OnInit { this.modalController.dismiss(); } - login() { + async login() { if (this.loginForm.valid) { this.User.authType = "User"; this.User.email = this.loginForm.value.email; this.User.password = this.loginForm.value.password; + const loading = await this.presentLoading(); // Show loading spinner console.log(this.User) this.authService.loginUser(this.User).subscribe(async (response: any) => { @@ -57,13 +60,14 @@ export class LoginComponent implements OnInit { this.User.lastName = response.lastName; this.authService.user.next(this.User); await this.authService.saveUserData('Token', JSON.stringify(this.User.token)); - // this.sucessToast('Welcome back ' + this.User.firstName) + //this.sucessToast('Welcome back ' + this.User.firstName) //const userData = await this.authService.getUserData(); //console.log("TOKEN " + userData); } else { this.failToast('Please ensure all details are correct'); } + loading.dismiss(); // Dismiss loading spinner when response is received }); } else { this.failToast('Please ensure all details are correct'); @@ -84,9 +88,19 @@ export class LoginComponent implements OnInit { const toast = await this.toastController.create({ message: message, color: 'success', - duration: 3000, - position: 'bottom', + duration: 2000, + position: 'top', }); toast.present(); } + + private async presentLoading() { + const loading = await this.loadingController.create({ + message: 'Logging in...', + spinner: 'crescent', // spinner style + duration: 20000, + }); + await loading.present(); + return loading; + } } diff --git a/app/WhereIsThePower/src/app/shared/components/signup/signup.component.html b/app/WhereIsThePower/src/app/shared/components/signup/signup.component.html index d04790b7..fc9d921e 100644 --- a/app/WhereIsThePower/src/app/shared/components/signup/signup.component.html +++ b/app/WhereIsThePower/src/app/shared/components/signup/signup.component.html @@ -15,7 +15,16 @@ <ion-content [fullscreen]="true"> <div class="ion-padding"> - <form class="form" [formGroup]="signupForm" (ngSubmit)="signup()"> + <ion-row class="ion-text-center ion-padding-top"> + <ion-col size = "3" offset = "4.5" size-md = "2" offset-md = "5" class="ion-text-center"> + <ion-avatar class="ion-no-margin" class="ion-text-center" style="display:inline"> + <ion-img src="assets/WhereIsThePower.png"></ion-img> + </ion-avatar> + </ion-col> + </ion-row> + <form class="form" [formGroup]="signupForm" (ngSubmit)="signup()" class="ion-text-center"> + <h3 class="ion-padding-start"><ion-text color="primary">Where Is the Power?</ion-text></h3> + <ion-item data-cy="input-fn"> <ion-label position="floating">First Name</ion-label> <ion-input type="firstName" formControlName="firstName" aria-label="Input for First Name"></ion-input> @@ -30,13 +39,20 @@ <ion-label position="floating">Email</ion-label> <ion-input type="email" formControlName="email" aria-label="Input for email" ></ion-input> </ion-item> + <ion-text color="danger" *ngIf="signupForm.get('email')!.hasError('email')" class="ion-padding-start"> + Invalid email. + </ion-text> <ion-item data-cy="input-password"> <ion-label position="floating">Password</ion-label> <ion-input type="password" formControlName="password" aria-label="Input for password" ></ion-input> </ion-item> - - <ion-button expand="full" type="submit" data-cy="btn-confirm-signup">Signup</ion-button> + <ion-text color="danger" *ngIf="signupForm.get('password')!.hasError('minlength')" class="ion-padding-start"> + Password must be at least 8 characters + </ion-text> + <div class="ion-padding-top"> + <ion-button expand="block" type="submit" data-cy="btn-confirm-signup" class="ion-margin-top ion-margin-start">Signup</ion-button> + </div> </form> </div> </ion-content> diff --git a/app/WhereIsThePower/src/app/shared/components/signup/signup.component.ts b/app/WhereIsThePower/src/app/shared/components/signup/signup.component.ts index d2633263..4c2757f0 100644 --- a/app/WhereIsThePower/src/app/shared/components/signup/signup.component.ts +++ b/app/WhereIsThePower/src/app/shared/components/signup/signup.component.ts @@ -6,6 +6,7 @@ import { RegisterUser } from '../../models/register-user'; import { AuthService } from '../../../authentication/auth.service'; import { ModalController } from '@ionic/angular'; import { User } from '../../models/user'; +import { LoadingController } from '@ionic/angular'; @Component({ selector: 'app-signup', @@ -33,7 +34,8 @@ export class SignupComponent implements OnInit { private formBuilder: FormBuilder, private toastController: ToastController, private authService: AuthService, - public modalController: ModalController + public modalController: ModalController, + private loadingController: LoadingController ) { } @@ -54,13 +56,15 @@ export class SignupComponent implements OnInit { this.authService.signupUser(this.newUser).subscribe(async (response: any) => { console.log(response); let createNewUser = new User("User", this.newUser.email, this.newUser.password, this.newUser.firstName, this.newUser.lastName); + const loading = await this.presentLoading(); // Show loading spinner console.log(createNewUser); this.authService.loginUser(createNewUser).subscribe(async (response: any) => { createNewUser.token = response.token; await this.authService.saveUserData('Token', JSON.stringify(createNewUser.token)); + loading.dismiss(); // Dismiss loading spinner when response is received - //console.log("RES" + response); + //console.log("RES" + response); this.authService.user.next(createNewUser); this.dismissModal(); }); @@ -90,4 +94,15 @@ export class SignupComponent implements OnInit { }); toast.present(); } + + private async presentLoading() { + const loading = await this.loadingController.create({ + message: 'Signing up...', + spinner: 'crescent', // spinner style + duration: 20000, + }); + await loading.present(); + return loading; + } } + diff --git a/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.html b/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.html index 70285e43..4af9ec22 100644 --- a/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.html +++ b/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.html @@ -8,6 +8,7 @@ (keyup.enter)="onSearchInput($event)" (ionClear)="onSearchBarClear()" (ionFocus)="onSearchBarFocus()" + (ionBlur)="onBlur()" [disabled]="!mapLoaded" #searchBar> </ion-searchbar> @@ -136,7 +137,7 @@ <h3 style="margin-top: 12px;"> </h3> <ion-text>{{ tripDuration}} min</ion-text> <ion-text style="padding-left: 8px; padding-right: 8px;">•</ion-text> - <ion-text>13:00</ion-text> + <ion-text>{{tripETAH}}:{{tripETAM}}</ion-text> </ion-col> <ion-col size="4" class="ion-text-end ion-align-items-center" style=" display: flex; diff --git a/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.scss b/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.scss index cd471de2..0e0143ab 100644 --- a/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.scss +++ b/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.scss @@ -99,4 +99,10 @@ ion-list { :host ::ng-deep .mapboxgl-popup-content h4 ion-icon { padding-right: 8px; /* Adjust the margin as needed */ -} \ No newline at end of file +} + +.report-icon { + position: absolute; + z-index: 9999; /* Set a high z-index value */ + /* Add any other styling as needed */ +} diff --git a/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.ts b/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.ts index 22c2ce27..f2937322 100644 --- a/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.ts +++ b/app/WhereIsThePower/src/app/shared/map-modal/map-modal.component.ts @@ -20,6 +20,7 @@ import { Subscribable } from 'rxjs'; import { Place } from '../../tab-saved/place'; import { Router } from '@angular/router'; import { ReportService } from '../../report/report.service'; +import { LoadingController } from '@ionic/angular'; declare let MapboxDirections: any; declare let mapboxgl: any; declare let MapboxGeocoder: any; @@ -39,7 +40,8 @@ export class MapModalComponent implements OnInit, AfterViewInit { private changeDetectorRef: ChangeDetectorRef, private savedPlacesService: SavedPlacesService, private router: Router, - private reportService: ReportService + private reportService: ReportService, + private loadingController: LoadingController ) { } map: any; dat: any; @@ -68,6 +70,8 @@ export class MapModalComponent implements OnInit, AfterViewInit { currentSuburbSchedule: any; modifiedAddress: string = ""; isPlaceSaved: boolean = false; + currentPopup: string | null = null; + isReportMarker: boolean = false; @ViewChild('myModal') myModal: any; // Reference to the ion-modal element modalResult: any; // To store the selected result data @@ -108,9 +112,11 @@ export class MapModalComponent implements OnInit, AfterViewInit { console.log("navigateToSavedPlace: ", isNavigate); this.isPlaceSaved = isNavigate; }); + + } - ngAfterViewInit() { + async ngAfterViewInit() { this.MapSubscription = this.mapSuburbsService.getSuburbData().subscribe(async (data: any) => { console.log(data.result.mapPolygons[0]); console.log("Data: ", data); @@ -150,6 +156,21 @@ export class MapModalComponent implements OnInit, AfterViewInit { this.map.on('load', () => { this.map.resize(); // Trigger map resize after the initial rendering + // Reporting + this.reportService.getReports().subscribe((data) => { + console.log("getReports: ", data); + }); + + this.reportService.reports.subscribe((reports: any) => { + if (reports) { + console.log("Reports (Map Page)", reports); + + // Add marker on map for each report + reports.forEach((report: any) => { + this.addMarker(report.longitude, report.latitude, report.report_type); + }); + } + }); }); // Populate Map(suburbs) with Polygons @@ -160,36 +181,25 @@ export class MapModalComponent implements OnInit, AfterViewInit { console.log(error); } ); - - // Reporting - this.reportService.reports.subscribe((reports: any) => { - console.log("reports", reports.result); - // console.log("reports.length", reports.size); - - if (reports) { - console.log("reports??"); - reports.result.forEach((report: any) => { - this.addMarker(report.longitude, report.latitude, report.report_type); - }); - } - }); } addMarker(lon: number, lat: number, reportType: string) { - console.log("addMarkeraddMarker"); + console.log("Add Marker"); const customIcon = document.createElement('ion-icon'); customIcon.style.width = '30px'; // Set the width of your custom icon customIcon.style.height = '30px'; // Set the height of your custom icon - customIcon.style.backgroundColor = 'var(--ion-color-primary)'; // Use Ionic primary color variable + customIcon.style.backgroundColor = '#00a165'; // Use Ionic primary color variable customIcon.style.backgroundImage = `url('assets/${reportType}.svg')`; // Replace with your icon path customIcon.style.backgroundSize = 'cover'; customIcon.style.backgroundPosition = 'center'; customIcon.style.borderRadius = '50%'; customIcon.style.padding = '8px'; + customIcon.style.pointerEvents = 'none'; + customIcon.setAttribute('class', 'report-icon'); const formattedReportType = reportType.replace(/([A-Z])/g, ' $1'); - const marker = new mapboxgl.Marker({ + new mapboxgl.Marker({ element: customIcon, }) .setLngLat([lon, lat]) @@ -201,15 +211,30 @@ export class MapModalComponent implements OnInit, AfterViewInit { <ion-card-title color="primary">${formattedReportType}</ion-card-title> </ion-card-header> <ion-card-content> - <h4><ion-icon src="assets/schedule.svg"></ion-icon><ion-text>Reported at 14:00</ion-text></h4> + <h4><ion-icon src="assets/schedule.svg"></ion-icon><ion-text>Reported at ${this.formatTime(new Date())}</ion-text></h4> </ion-card-content> </ion-card>` ) ) .addTo(this.map); + customIcon.addEventListener('click', this.handleReportIconClick.bind(this)); - // this.markers.push(marker); // Add the marker to the markers array + console.log("this.popup", this.popup); + // Close the other popup if it's open } + + formatTime(date: any) { + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + return `${hours}:${minutes}`; + } + + // Define a separate function to handle the click event + handleReportIconClick() { + console.log("Report Icon Clicked"); + this.isReportMarker = true; + } + populatePolygons() { this.map.on('load', () => { // Add a data source containing GeoJSON data. @@ -253,10 +278,14 @@ export class MapModalComponent implements OnInit, AfterViewInit { // Listen for the click event on the map this.map.on('click', 'polygons-layer', (e: any) => { + setTimeout(() => { + this.isReportMarker = false; + }, 20); + const clickedFeature = e.features[0]; //console.log(e); - if (clickedFeature) { + if (clickedFeature && this.isReportMarker == false) { let suburbId = clickedFeature.id; console.log("Suburb ID =" + suburbId) // Get the properties of the clicked feature (suburb information) @@ -268,30 +297,36 @@ export class MapModalComponent implements OnInit, AfterViewInit { this.mapSuburbsService.fetchTimeForPolygon(suburbId).subscribe( (response: any) => { // Handle the response here + + console.log('Time response:', response); - const timesOff = response.result.timesOff; // Assuming "response" holds your API response + console.log("success", response.success); - if (timesOff && timesOff.length > 0) { - const formattedTimes = timesOff.map((time: any) => { - const start = new Date(time.start * 1000); // Convert seconds to milliseconds - const end = new Date(time.end * 1000); // Convert seconds to milliseconds + if (response.success === true) { + const timesOff = response.result.timesOff; // Assuming "response" holds your API response + if (timesOff && timesOff.length > 0) { + const formattedTimes = timesOff.map((time: any) => { + const start = new Date(time.start * 1000); // Convert seconds to milliseconds + const end = new Date(time.end * 1000); // Convert seconds to milliseconds - const startHours = start.getHours().toString().padStart(2, '0'); - const startMinutes = start.getMinutes().toString().padStart(2, '0'); + const startHours = start.getHours().toString().padStart(2, '0'); + const startMinutes = start.getMinutes().toString().padStart(2, '0'); - const endHours = end.getHours().toString().padStart(2, '0'); - const endMinutes = end.getMinutes().toString().padStart(2, '0'); + const endHours = end.getHours().toString().padStart(2, '0'); + const endMinutes = end.getMinutes().toString().padStart(2, '0'); - this.currentSuburbSchedule = `${startHours}:${startMinutes} - ${endHours}:${endMinutes}`; - }); + this.currentSuburbSchedule = `${startHours}:${startMinutes} - ${endHours}:${endMinutes}`; - console.log('Formatted Time Ranges:', formattedTimes); - } else { - console.log('No time ranges available.'); - this.currentSuburbSchedule = "unavailable"; - } - const showSchedule = suburbInfo?.PowerStatus !== 'on'; - const popupContent = ` + }); + + console.log('Formatted Time Ranges:', formattedTimes); + } else { + console.log('No time ranges available.'); + this.currentSuburbSchedule = "unavailable"; + } + + const showSchedule = suburbInfo?.PowerStatus !== 'on'; + const popupContent = ` <ion-card class="popup-ion-card"> <ion-card-header class="popup-ion-card-header"> <ion-card-title color="primary">${suburbInfo?.SP_NAME}</ion-card-title> @@ -302,11 +337,15 @@ export class MapModalComponent implements OnInit, AfterViewInit { </ion-card-content> </ion-card> `; - // Create a new popup and set its HTML content - this.popup = new mapboxgl.Popup() - .setLngLat(e.lngLat) - .setHTML(popupContent) - .addTo(this.map); + // Create a new popup and set its HTML content + this.popup = new mapboxgl.Popup() + .setLngLat(e.lngLat) + .setHTML(popupContent) + .addTo(this.map); + + this.currentPopup = popupContent; + + } }, (error) => { // Handle errors here @@ -362,8 +401,16 @@ export class MapModalComponent implements OnInit, AfterViewInit { } } + onBlur() { + console.log("Search Bar Blurred"); + setTimeout(() => { + this.showResultsList = false; + }, 200); // 200ms delay + } + async getRoute(selectedResult: Place | any) { + this.instructions = []; this.updateBreakpoint(); this.emitGetDirections(); this.gettingRoute = true; @@ -376,25 +423,46 @@ export class MapModalComponent implements OnInit, AfterViewInit { console.log("selected Result for directions", selectedResult); let query: any; + let fallback = false; + const loading = await this.presentLoading(); // Show loading spinner if (!selectedResult.hasOwnProperty('center')) { - console.log("SELECTED DIRECTION", selectedResult); - + try { + console.log("Selected directions (saved places) ", selectedResult); + query = await this.mapSuburbsService.fetchOptimalRoute(this.longitude, this.latitude, selectedResult.longitude, selectedResult.latitude).toPromise(); + coords = query.result.coordinates; + } + catch (error) { + console.error("Error in the first query:", error); + fallback = true; + query = await fetch(`https://api.mapbox.com/directions/v5/mapbox/driving/${this.longitude},${this.latitude};${selectedResult.longitude},${selectedResult.latitude}?alternatives=true&geometries=geojson&language=en&overview=full&steps=true&access_token=${environment.MapboxApiKey}`); + coords = [selectedResult.longitude, selectedResult.latitude]; + } this.searchBar.value = `${selectedResult.address}`; - query = await fetch(`https://api.mapbox.com/directions/v5/mapbox/driving/${this.longitude},${this.latitude};${selectedResult.longitude},${selectedResult.latitude}?alternatives=true&geometries=geojson&language=en&overview=full&steps=true&access_token=${environment.MapboxApiKey}`) - coords = [selectedResult[0], selectedResult[1]]; + } else { - console.log("SEARCH DIRECTION", selectedResult); - + try { + console.log("Searched directions (searchbar) ", selectedResult); + + query = await this.mapSuburbsService.fetchOptimalRoute(this.longitude, this.latitude, selectedResult.center[0], selectedResult.center[1]).toPromise(); + coords = query.result.coordinates; + + } catch (error) { + // Handle the error from the first query + console.error("Error in the first query:", error); + fallback = true; + query = await fetch(`https://api.mapbox.com/directions/v5/mapbox/driving-traffic/${this.longitude},${this.latitude};${selectedResult.center[0]},${selectedResult.center[1]}?alternatives=true&geometries=geojson&language=en&steps=true&access_token=${environment.MapboxApiKey}`); + coords = [selectedResult.center[0], selectedResult.center[1]]; + } this.searchBar.value = `${selectedResult.place_name}`; - query = await fetch(`https://api.mapbox.com/directions/v5/mapbox/driving/${this.longitude},${this.latitude};${selectedResult.center[0]},${selectedResult.center[1]}?alternatives=true&geometries=geojson&language=en&steps=true&access_token=${environment.MapboxApiKey}`) - coords = [selectedResult.center[0], selectedResult.center[1]]; } - console.log("Directions query: ", query); - console.log(coords); - // Add a marker for the start point + console.log("_________________________"); + console.log("Directions query", query); + console.log("_________________________"); + loading.dismiss(); // Dismiss loading spinner when response is received + // Add a marker for the start point const start = { type: 'FeatureCollection', features: [ @@ -446,7 +514,7 @@ export class MapModalComponent implements OnInit, AfterViewInit { properties: {}, geometry: { type: 'Point', - coordinates: coords + coordinates: [coords[coords.length - 1][0], coords[coords.length - 1][1]] } } ] @@ -467,7 +535,7 @@ export class MapModalComponent implements OnInit, AfterViewInit { properties: {}, geometry: { type: 'Point', - coordinates: coords + coordinates: [coords[coords.length - 1][0], coords[coords.length - 1][1]] } } ] @@ -480,30 +548,62 @@ export class MapModalComponent implements OnInit, AfterViewInit { }); } - const json = await query.json(); + let geojson: any; + let route: any; + + if (!fallback) // Optimised route + { + const data = query.result; // Pick 1st route in list of route recommendations + route = coords; // list of coordinates forming route + geojson = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: route + } + }; + // get the sidebar and add the instructions - const data = json.routes[0]; // Pick 1st route in list of route recommendations - const route = data.geometry.coordinates; // list of coordinates forming route - const geojson = { - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: route + const steps = data.instructions; + for (const step of steps) { + this.instructions.push(step); } - }; - // get the sidebar and add the instructions - const steps = data.legs[0].steps; - for (const step of steps) { - this.instructions.push(step.maneuver.instruction); + + this.tripDuration = Math.floor(data.duration / 60); + this.tripDistance = Math.floor(data.distance / 1000); + + //CALCULATE ETA + this.tripETA = new Date(); + this.calculateETA(); } + else // Mapbox + { + const json = await query.json(); + + const data = json.routes[0]; // Pick 1st route in list of route recommendations + route = data.geometry.coordinates; // list of coordinates forming route + geojson = { + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: route + } + }; + // get the sidebar and add the instructions + const steps = data.legs[0].steps; + for (const step of steps) { + this.instructions.push(step.maneuver.instruction); + } - this.tripDuration = Math.floor(data.duration / 60); - this.tripDistance = Math.floor(data.distance / 1000); + this.tripDuration = Math.floor(data.duration / 60); + this.tripDistance = Math.floor(data.distance / 1000); - //CALCULATE ETA - this.tripETA = new Date(); - this.calculateETA(); + //CALCULATE ETA + this.tripETA = new Date(); + this.calculateETA(); + } // if the route already exists on the map, we'll reset it using setData @@ -555,6 +655,16 @@ export class MapModalComponent implements OnInit, AfterViewInit { }); } + private async presentLoading() { + const loading = await this.loadingController.create({ + message: 'Calculating Route...', + spinner: 'crescent', // spinner style + duration: 20000, + }); + await loading.present(); + return loading; + } + delay(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } diff --git a/app/WhereIsThePower/src/app/shared/map-modal/map-suburbs.service.ts b/app/WhereIsThePower/src/app/shared/map-modal/map-suburbs.service.ts index 801bcf08..79727644 100644 --- a/app/WhereIsThePower/src/app/shared/map-modal/map-suburbs.service.ts +++ b/app/WhereIsThePower/src/app/shared/map-modal/map-suburbs.service.ts @@ -27,4 +27,23 @@ export class MapSuburbsService { return this.httpClient.post(url, requestBody); } + + fetchOptimalRoute(originLon: number, originLat: number, destinationLon: number, destinationLat: number) { + const url = `https://witpa.codelog.co.za/api/ai/info`; +console.log(originLon, originLat, destinationLon, destinationLat); + + const requestBody = + { + "origin": [ + originLon, + originLat + ], + "destination": [ + destinationLon, + destinationLat + ] + }; + + return this.httpClient.post(url, requestBody); + } } diff --git a/app/WhereIsThePower/src/app/tab-navigate/tab-navigate.page.html b/app/WhereIsThePower/src/app/tab-navigate/tab-navigate.page.html index f24dab09..78557f04 100644 --- a/app/WhereIsThePower/src/app/tab-navigate/tab-navigate.page.html +++ b/app/WhereIsThePower/src/app/tab-navigate/tab-navigate.page.html @@ -20,7 +20,7 @@ <h3><strong>Enable Device location</strong></h3> </ion-row> </div> <ng-template #noLocationProvided> - <app-map-modal></app-map-modal> + <app-map-modal #mapModalComponent></app-map-modal> </ng-template> </ion-content> diff --git a/app/WhereIsThePower/src/app/tab-navigate/tab-navigate.page.ts b/app/WhereIsThePower/src/app/tab-navigate/tab-navigate.page.ts index 3d7f6731..551bc6c6 100644 --- a/app/WhereIsThePower/src/app/tab-navigate/tab-navigate.page.ts +++ b/app/WhereIsThePower/src/app/tab-navigate/tab-navigate.page.ts @@ -1,6 +1,8 @@ import { Component } from '@angular/core'; import { UserLocationService } from '../user-location.service'; import { SavedPlacesService } from '../tab-saved/saved-places.service'; +import { ViewChild } from '@angular/core'; +import { MapModalComponent } from '../shared/map-modal/map-modal.component'; @Component({ selector: 'app-tab-navigate', @@ -8,6 +10,7 @@ import { SavedPlacesService } from '../tab-saved/saved-places.service'; styleUrls: ['tab-navigate.page.scss'] }) export class TabNavigatePage { + @ViewChild('mapModalComponent', { static: false }) mapModalComponent!: MapModalComponent; constructor(private UserLocationService: UserLocationService, private savedPlacesService: SavedPlacesService) { } isLocationProvide = false; @@ -21,6 +24,9 @@ export class TabNavigatePage { console.log('isLocationAvailable', isLocationAvailable); this.isLocationProvide = isLocationAvailable; }); + + if(this.mapModalComponent && this.mapModalComponent.map) + this.mapModalComponent.map.resize(); } onLocateUser() { @@ -28,6 +34,9 @@ export class TabNavigatePage { } ionViewDidLeave() { + if (this.mapModalComponent && this.mapModalComponent.searchBar) { + this.mapModalComponent.searchBar.value = ""; + } this.savedPlacesService.navigateToPlace.next(false); this.savedPlacesService.navigateToSavedPlace.next(false); } diff --git a/app/WhereIsThePower/src/app/tab-profile/tab-profile.page.html b/app/WhereIsThePower/src/app/tab-profile/tab-profile.page.html index 84a9de19..a6df0c94 100644 --- a/app/WhereIsThePower/src/app/tab-profile/tab-profile.page.html +++ b/app/WhereIsThePower/src/app/tab-profile/tab-profile.page.html @@ -17,31 +17,47 @@ alt="Traffic Jam Illustration" class="ion-padding" > - </ion-img> + </ion-img> </ion-col> </ion-row> + <ion-row class="ion-padding-start"> + <h5 style="margin: 0;"> + <ion-label>Account</ion-label> + </h5> + </ion-row> + <ion-item button detail="true" (click)="showLoginComponent()"> + <ion-icon name="log-in-outline" class="ion-padding-end"></ion-icon> + <ion-label>Login to my account</ion-label> + </ion-item> + <ion-item button detail="true" (click)="showSignupComponent()"> + <ion-icon name="person-add-outline" class="ion-padding-end"></ion-icon> + <ion-label>Sign up for an account</ion-label> + </ion-item> + + <ion-row class="ion-padding-start ion-padding-top ion-margin-top"> + <h5> + <ion-label>User Manual</ion-label> + </h5> + </ion-row> + + <!-- Download links for "Mobile" and "Desktop" user manuals --> + <ion-item button (click)="downloadUserManual('mobile')"> + <ion-icon name="phone-portrait-outline" class="ion-padding-end"></ion-icon> + <ion-label>Mobile</ion-label> + </ion-item> + + <ion-item button (click)="downloadUserManual('desktop')"> + <ion-icon name="desktop-outline" class="ion-padding-end"></ion-icon> + <ion-label>Desktop</ion-label> + </ion-item> <ion-row> - <ion-col size="12" size-md="11" offset-md="0.5" size-xl="10" offset-xl="1"> - <ion-toolbar> - <ion-list> - <ion-item button detail="true" (click)="showLoginComponent()"> - <ion-icon name="log-in-outline" class="ion-padding-end"></ion-icon> - <ion-label>Login to my account</ion-label> - </ion-item> - <ion-item button detail="true" (click)="showSignupComponent()"> - <ion-icon name="person-add-outline" class="ion-padding-end"></ion-icon> - <ion-label>Sign up for an account</ion-label> - </ion-item> - </ion-list> - </ion-toolbar> - </ion-col> - <ion-col size="12" size-md="11" offset-md="0.5" size-xl="10" offset-xl="1"> + <ion-col size="12" size-md="11" offset-md="0.5" size-xl="10" offset-xl="1" class="ion-padding-top"> <ion-item> <ion-label color="primary" style="text-align: center;">Terms of Service</ion-label> </ion-item> </ion-col> </ion-row> - </ion-grid> + </ion-grid> </div> <ng-template #UserLoggedIn> @@ -53,25 +69,44 @@ <ion-text class="ion-text-center"> <h1 data-cy="Welcome-text"> Welcome {{user?.firstName}} !</h1> </ion-text> - <ion-list> + <ion-row class="ion-padding-start"> + <h5> + <ion-label>Settings</ion-label> + </h5> + </ion-row> <ion-item button> <ion-icon name="moon-outline" class="ion-padding-end"></ion-icon> <ion-select #systemTheme - label="Theme" + label="Theme" interface="popover" toggleIcon="add" expandedIcon="remove" aria-label="Theme" - placeholder="Select Theme" + placeholder="Light" (ionChange)="toggleTheme(systemTheme.value)"> <ion-select-option value="light">Light</ion-select-option> <ion-select-option value="dark" >Dark</ion-select-option> </ion-select> </ion-item> <ion-item button (click)="logout()" data-cy="logout-button"> - <ion-icon name="log-out-outline" class="ion-padding-end"></ion-icon> - <ion-label>Logout</ion-label> + <ion-icon name="log-out-outline" class="ion-padding-end" color="danger"></ion-icon> + <ion-label color="danger">Logout</ion-label> </ion-item> - </ion-list> + <ion-row class="ion-padding-start ion-padding-top ion-margin-top"> + <h5 style="margin-bottom: 4px;"> + <ion-label>User Manual</ion-label> + </h5> + </ion-row> + + <!-- Download links for "Mobile" and "Desktop" user manuals --> + <ion-item button (click)="downloadUserManual('mobile')"> + <ion-icon name="phone-portrait-outline" class="ion-padding-end"></ion-icon> + <ion-label>Mobile</ion-label> + </ion-item> + + <ion-item button (click)="downloadUserManual('desktop')"> + <ion-icon name="desktop-outline" class="ion-padding-end"></ion-icon> + <ion-label>Desktop</ion-label> + </ion-item> </ng-template> </ion-content> diff --git a/app/WhereIsThePower/src/app/tab-profile/tab-profile.page.ts b/app/WhereIsThePower/src/app/tab-profile/tab-profile.page.ts index 272ec394..a214d888 100644 --- a/app/WhereIsThePower/src/app/tab-profile/tab-profile.page.ts +++ b/app/WhereIsThePower/src/app/tab-profile/tab-profile.page.ts @@ -21,7 +21,7 @@ export class TabProfilePage implements OnInit { isLoggedIn: boolean = false; constructor(private authService: AuthService, - private modalController: ModalController) { } + private modalController: ModalController) { } ngOnInit() { //this.isLoggedIn = this.authService.isLoggedin; @@ -65,6 +65,7 @@ export class TabProfilePage implements OnInit { async logout() { this.isLoggedIn = false; await this.authService.signOutUser(); + this.toggleTheme('light'); } ngOnDestroy() { @@ -95,4 +96,23 @@ export class TabProfilePage implements OnInit { toggleTheme(systemTheme: string) { document.body.setAttribute('witp-color-theme', systemTheme); } + + downloadUserManual(type: string) { + // Determine the URL of the user manual based on the 'type' + let userManualURL = ''; + + if (type === 'mobile') { + userManualURL = 'assets/pdf/user-manual-mobile.pdf'; + } else if (type === 'desktop') { + userManualURL = 'assets/pdf/user-manual-desktop.pdf'; + } + // Add more 'if' conditions for other user manual types if needed + + // Trigger the download + const link = document.createElement('a'); + link.href = userManualURL; + link.target = '_blank'; // Open the link in a new tab (optional) + link.download = `user-manual-${type}.pdf`; // Specify the filename + link.click(); + } } diff --git a/app/WhereIsThePower/src/app/tab-saved/saved-places.service.ts b/app/WhereIsThePower/src/app/tab-saved/saved-places.service.ts index 34c910b9..7995fc30 100644 --- a/app/WhereIsThePower/src/app/tab-saved/saved-places.service.ts +++ b/app/WhereIsThePower/src/app/tab-saved/saved-places.service.ts @@ -2,7 +2,7 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { AuthService } from '../authentication/auth.service'; import { BehaviorSubject, Subscription } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { filter, tap } from 'rxjs/operators'; import { Router } from '@angular/router'; import { Place } from './place'; @@ -50,6 +50,21 @@ export class SavedPlacesService { return this.httpClient.put(`${this.apiUrl}user/savedPlaces`, newPlace, { headers: this.headers }) } + deleteSavedPlace(placeID: string) { + let currentPlaces: any = this.place.value; + console.log("currentPlaces: ", currentPlaces) + + if (currentPlaces && currentPlaces.result) { + const updatedPlaces = currentPlaces.result.filter((place: Place) => { + return place.mapboxId !== placeID; + }); + console.log("updatedPlaces: ", updatedPlaces) + this.place.next(updatedPlaces); + } + + return this.httpClient.delete(`${this.apiUrl}user/savedPlaces/${placeID}`, { headers: this.headers }) + } + goToPlace(place: Place) { this.selectedPlace = place; this.navigateToPlace.next(true); diff --git a/app/WhereIsThePower/src/app/tab-saved/tab-saved.page.html b/app/WhereIsThePower/src/app/tab-saved/tab-saved.page.html index ea9dd88d..61dbe9bf 100644 --- a/app/WhereIsThePower/src/app/tab-saved/tab-saved.page.html +++ b/app/WhereIsThePower/src/app/tab-saved/tab-saved.page.html @@ -32,7 +32,14 @@ <h3>Saved places is only available to registered users. </h3> <!-- SEARCH BAR --> <ion-row class="justify-content-center"> <ion-col class="searchbar-container"> - <ion-searchbar animated="true" placeholder="Search for a place..." (ionInput)="onSearchInput($event)" (keyup.enter)="onSearchInput($event)" (ionClear)="onSearchBarClear()" (ionFocus)="onSearchBarFocus()" #searchBar></ion-searchbar> + <ion-searchbar animated="true" placeholder="Search for a place..." + (ionInput)="onSearchInput($event)" + (keyup.enter)="onSearchInput($event)" + (ionClear)="onSearchBarClear()" + (ionFocus)="onSearchBarFocus()" + (ionBlur)="onBlur()" + #searchBar> + </ion-searchbar> <ion-list *ngIf="showResultsList; else elseBlock"> <ion-item *ngFor="let result of searchResults" (click)="savePlace(result)" button> <ion-icon slot="start" name="{{ getFeatureType(result.place_type[0]) }}"></ion-icon> @@ -42,17 +49,6 @@ <h3><strong>{{ result.text }}</strong></h3> </ion-label> <!-- TEMP ADD PLACE --> <ion-icon name="add-circle" color="primary" size="large"></ion-icon> - <!-- BOOKMARK --> - <!-- <div *ngIf="isPlaceSaved(result) === false; else elseBlock"> --> - <!-- ADD --> - <!-- <ion-icon color="success" style="font-size: 24px;" slot="icon-only" name="bookmark-outline"></ion-icon> --> - <!-- </div> --> - <!-- <ng-template #elseBlock> --> - <!-- <div> --> - <!-- DELETE --> - <!-- <ion-icon slot="icon-only" name="bookmark"></ion-icon> --> - <!-- </div> --> - <!-- </ng-template> --> </ion-item> </ion-list> </ion-col> @@ -62,33 +58,27 @@ <h3><strong>{{ result.text }}</strong></h3> <ion-col> <ion-row> <ion-toolbar class="ion-text-wrap"> - <!-- Saved Places is empty --> <ion-title *ngIf="places?.length === 0"> You have no saved places </ion-title> + <!-- Show Saved places --> <ion-list *ngFor="let savedPlace of places"> - <ion-item (click)="goToSavedPlace(savedPlace)"> - <!-- <ion-icon slot="start" name="{{ savedPlace.type }}"></ion-icon> --> - <ion-label class="ion-text-wrap"> - <h2>{{ savedPlace.address }}</h2> - </ion-label> - <div> - <!-- DELETE --> - <ion-button - color="success" - fill="clear" - (click)="removeSavedPlace(savedPlace)"> - <ion-icon slot="icon-only" name="bookmark"></ion-icon> - </ion-button> - </div> - </ion-item> + <ion-item-sliding> + <ion-item (click)="goToSavedPlace(savedPlace)"> + <ion-label class="ion-text-wrap"> + <h2>{{ savedPlace.address }}</h2> + </ion-label> + </ion-item> + <ion-item-options side="end"> + <ion-item-option color="danger" (click)="deleteSavedPlace(savedPlace)"> + <ion-icon slot="icon-only" name="trash"></ion-icon> + </ion-item-option> + </ion-item-options> + </ion-item-sliding> </ion-list> </ion-toolbar> </ion-row> - <!-- <ion-row class="ion-hide-lg-down"> - <app-map-modal></app-map-modal> - </ion-row> --> </ion-col> </ng-template> </ion-grid> diff --git a/app/WhereIsThePower/src/app/tab-saved/tab-saved.page.ts b/app/WhereIsThePower/src/app/tab-saved/tab-saved.page.ts index 6c2e17ba..cdd88648 100644 --- a/app/WhereIsThePower/src/app/tab-saved/tab-saved.page.ts +++ b/app/WhereIsThePower/src/app/tab-saved/tab-saved.page.ts @@ -38,7 +38,7 @@ export class TabSavedPage { this.savePlaceSubscription = new Subscription(); } - ngOnInit() { } + ngOnInit() {} gotoProfileRoute() { this.router.navigate(['tabs/tab-profile']); @@ -118,11 +118,12 @@ export class TabSavedPage { } } - removeSavedPlace(place: any) { - this.savedPlaces = this.savedPlaces.filter((sPlace: any) => { - if (sPlace.id !== place.id) return sPlace; + deleteSavedPlace(savedPlace: any) { + console.log("deleteSavedPlace", savedPlace); + this.savedPlaceService.deleteSavedPlace(savedPlace.mapboxId).subscribe(data => { + console.log("deleteSavedPlace: ", data); + this.places = this.places.filter((place: Place) => place.mapboxId !== savedPlace.mapboxId); }); - console.log("removeSavedPlace: ", this.savedPlaces); } isPlaceSaved(place: any) { @@ -212,6 +213,12 @@ export class TabSavedPage { this.showResultsList = false; } + onBlur() { + console.log("Search Bar Blurred"); + setTimeout(() => { + this.showResultsList = false; + }, 200); // 200ms delay + } async sucessToast(message: string) { const toast = await this.toastController.create({ diff --git a/app/WhereIsThePower/src/app/tab-schedule/schedule-time.ts b/app/WhereIsThePower/src/app/tab-schedule/schedule-time.ts new file mode 100644 index 00000000..398863b5 --- /dev/null +++ b/app/WhereIsThePower/src/app/tab-schedule/schedule-time.ts @@ -0,0 +1,4 @@ +export interface IScheduleTime { + startTime: Date; + endTime: Date; +} diff --git a/app/WhereIsThePower/src/app/tab-schedule/schedule.service.ts b/app/WhereIsThePower/src/app/tab-schedule/schedule.service.ts new file mode 100644 index 00000000..a2b0b297 --- /dev/null +++ b/app/WhereIsThePower/src/app/tab-schedule/schedule.service.ts @@ -0,0 +1,23 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class ScheduleService { + apiUrl = 'https://witpa.codelog.co.za/api/fetchScheduleData' + + constructor(private httpClient: HttpClient) { } + + getScheduleData(suburb: number) { + let body = { + "suburbId": suburb + } + + return this.httpClient.post<number>(this.apiUrl, body); + } + + getLoadSheddingStage() { + return this.httpClient.get('https://witpa.codelog.co.za/api/fetchCurrentStage'); + } +} diff --git a/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.html b/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.html index c15c3d43..995cfc76 100644 --- a/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.html +++ b/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.html @@ -7,48 +7,71 @@ </ion-header> <ion-content [fullscreen]="true"> - <h1 class="ion-text-center">Area</h1> - <h1 class="ion-padding-start">Sat, 06 May</h1> - <ion-list> - <ion-item> - <ion-label> - <p>Stage 5</p> - <h2>00:00 - 02:30</h2> - </ion-label> - </ion-item> - <ion-item> - <ion-label> - <p>Stage 4</p> - <h2>08:00 - 10:30</h2> - </ion-label> - </ion-item> - <ion-item> - <ion-label> - <p>Stage 5</p> - <h2>16:00 - 18:30</h2> - </ion-label> - </ion-item> - </ion-list> - - <h1 class="ion-padding-start ion-padding-top">Sun, 07 May</h1> - <ion-list> - <ion-item> - <ion-label> - <p>Stage 5</p> - <h2>00:00 - 02:30</h2> - </ion-label> - </ion-item> - <ion-item> - <ion-label> - <p>Stage 4</p> - <h2>08:00 - 10:30</h2> - </ion-label> - </ion-item> - <ion-item> - <ion-label> - <p>Stage 5</p> - <h2>16:00 - 18:30</h2> - </ion-label> - </ion-item> - </ion-list> + <ion-grid> + <ion-row> + <ion-col class="searchbar-container" size-xs="12" size-md="4" offset-md="7.8"> + <ion-searchbar + [(ngModel)]="searchTerm" + animated="true" + placeholder="Search for a place..." + (ionInput)="onSearch($event)" + (keyup.enter)="onSearch($event)" + (ionFocus)="onSearch($event)" + (ionBlur)="onBlur()" + > + </ion-searchbar> + <ion-list *ngIf="filteredItems.length > 0 && showResultsList" class="search-results"> + <ion-item *ngFor="let result of filteredItems" (click)="selectSuburb(result)" button> + <ion-label> + <h3><strong>{{ result.name }}</strong></h3> + </ion-label> + </ion-item> + </ion-list> + </ion-col> + </ion-row> + <ion-row *ngIf="isLocationProvided && isAreaFound; else areaNotAvailable"> + <ion-col size="12" class="ion-text-center"><h2>{{ suburbName }}</h2></ion-col> + <ion-col size="12" class="ion-text-center"> + <ion-chip [color]="chipColor"> + Current Stage: {{ loadsheddingStage }} + </ion-chip> + </ion-col> + <ion-col> + <ion-card *ngFor="let time of loadshedTimes"> + <ion-card-header> + <ion-card-title> + {{ days[time.startTime.getDay()] }} + </ion-card-title> + <ion-card-subtitle> + {{ time.startTime.getDate() }} {{ months[time.startTime.getMonth()] }} + </ion-card-subtitle> + </ion-card-header> + <ion-card-content> + <ion-title> + {{ formatTime(time.startTime.getHours()) }}:{{ formatTime(time.startTime.getMinutes()) }} - {{ formatTime(time.endTime.getHours()) }}:{{ formatTime(time.endTime.getMinutes()) }} + </ion-title> + </ion-card-content> + </ion-card> + </ion-col> + </ion-row> + <ng-template #areaNotAvailable> + <ion-row> + <ion-col size="8" offset="2" size-md="6" offset-md="3" size-xl="4" offset-xl="4"> + <ion-img + src="assets/phoneStats.svg" + alt="Address on phone Illustration" + class="ion-padding" + > + </ion-img> + </ion-col> + </ion-row> + <ion-row> + <ion-col class="ion-text-center ion-text-md-end"> + </ion-col> + <ion-col size="12" class="ion-text-center"> + <h3>Search for a suburb inside City of Tshwane.</h3> + </ion-col> + </ion-row> + </ng-template> + </ion-grid> </ion-content> diff --git a/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.scss b/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.scss index 81a69207..0a44da5e 100644 --- a/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.scss +++ b/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.scss @@ -1,3 +1,21 @@ .map { width: 100%; -} \ No newline at end of file +} + +.loadsheddingBanner{ + background-color: yellow; +} + +.search-container { + position: relative; +} + +.search-results { + position: absolute; + top: 100%; /* Display results directly below the search bar */ + left: 0; + right: 0; + z-index: 999; /* Ensure results are displayed above other elements */ + max-height: 300px; + overflow-y: auto; /* Allow scrolling for long lists */ +} diff --git a/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.ts b/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.ts index 68c58c8f..edd867f9 100644 --- a/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.ts +++ b/app/WhereIsThePower/src/app/tab-schedule/tab-schedule.page.ts @@ -1,4 +1,9 @@ import { Component } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { UserLocationService } from '../user-location.service'; +import { ScheduleService } from './schedule.service'; +import { HttpClient } from '@angular/common/http'; +import { IScheduleTime } from './schedule-time'; @Component({ selector: 'app-tab-schedule', @@ -6,7 +11,161 @@ import { Component } from '@angular/core'; styleUrls: ['tab-schedule.page.scss'] }) export class TabSchedulePage { + searchItems: any[] = []; + filteredItems: any[] = []; + geojsonData: any; + showResultsList = false; + isLocationProvided = false; + isAreaFound = false; + suburbName = ""; + searchTerm: string = ""; + loadsheddingStage: number = 0; + chipColor: string = "success"; + months: string[] = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; + days: string[] = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; - constructor() {} + loadshedTimes: IScheduleTime[] = []; + // Subscriptions + suburbDataSubscription: Subscription = new Subscription(); + listSuburbsSubscription: Subscription = new Subscription(); + loadsheddingStageSubscription: Subscription = new Subscription(); + isLocationAvailableSubscription: Subscription = new Subscription(); + + constructor(private userLocationService: UserLocationService, + private scheduleService: ScheduleService, + private http: HttpClient, + ) {} + + async ngOnInit() { + this.listSuburbsSubscription = this.http.get('assets/suburbs.json').subscribe(data => { + this.geojsonData = data; + this.searchItems = this.geojsonData.features.map((feature: any) => ({ + name: feature.properties.SP_NAME, + id: feature.id + })); + this.filteredItems = [...this.searchItems]; + console.log("Search Items:", this.filteredItems); + }); + + this.loadsheddingStageSubscription = this.scheduleService.getLoadSheddingStage().subscribe((stage: any) => { + console.log(stage); + this.loadsheddingStage = stage.result; + this.chipColor = this.setChipColor(this.loadsheddingStage); + }); + } + + async ionViewWillEnter(){ + // Attempt to get location + await this.userLocationService.getUserLocation(); + + this.isLocationAvailableSubscription = this.userLocationService.isLocationAvailable.subscribe((isLocationAvailable) => { + this.isLocationProvided = isLocationAvailable; + console.log("isLocationAvailable (Schedule page): ", this.isLocationProvided); + }); + + // Default Schedule: Area schedule on user location + let area = await this.userLocationService.getArea(); + console.log("Area: ", area); + if (area != null) { + console.log("Area Name: ", area.properties.SP_NAME); + console.log("Area ID: ", area.id); + this.selectSuburb( + { + "id": area.id, + "name": area.properties.SP_NAME + } + ); + } + else { + console.log("Area is not available outside of City of Tshwane."); + } + } + + onSearch(event: any) { + if (this.searchTerm.length > 0) { + this.showResultsList = true; + } + else { + this.showResultsList = false; + } + console.log(this.searchTerm); + // Reset items back to all of the items + this.filteredItems = [...this.searchItems]; + + // if the value is an empty string, don't filter the items + if (!this.searchTerm) return; + + this.filteredItems = this.searchItems.filter(item => { + if (item.name && this.searchTerm) { + return item.name.toLowerCase().includes(this.searchTerm.toLowerCase()); + } + return false; + }); + console.log("Filtered Items: ", this.filteredItems); + } + + onBlur() { + console.log("Search Bar Blurred"); + setTimeout(() => { + this.showResultsList = false; + }, 200); // 200ms delay + } + + selectSuburb(selectedSuburb: any) { + //this.clearAllCharts(); + console.log(selectedSuburb.name); // Logs the suburb name + console.log(selectedSuburb.id); // Logs the suburb id + this.showResultsList = false; + this.isAreaFound = true; + + this.suburbDataSubscription = this.scheduleService.getScheduleData(selectedSuburb.id).subscribe((data: any) => { + console.log("ScheduleService: ", data); + if (data.result != null) { + this.suburbName = selectedSuburb.name; + this.searchTerm = selectedSuburb.name; + + this.loadshedTimes = []; + + data.result.timesOff.forEach((timeOff: any) => { + let tempScheduleTimes: IScheduleTime = { + startTime: this.convertToDateTime(timeOff.start), + endTime: this.convertToDateTime(timeOff.end) + } + + this.loadshedTimes.push(tempScheduleTimes); + }); + + console.log(this.loadshedTimes); + + } + else { + this.isAreaFound = false; + } + }, + (error) => { + console.error(error); + this.isAreaFound = false; + }); + } + + convertToDateTime(utcTime: number) { + return new Date(1000 * utcTime); + } + + formatTime(unformattedTime: number) { + if(unformattedTime < 10) return '0' + unformattedTime; + return unformattedTime; + } + + setChipColor(loadshedStage: number) { + if(loadshedStage > 0 && loadshedStage < 4) return "warning"; + if(loadshedStage >= 4) return "danger"; + return "success"; + } + + ngOnDestroy() { + this.suburbDataSubscription.unsubscribe(); + this.listSuburbsSubscription.unsubscribe(); + } } diff --git a/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.html b/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.html index 3da360d5..e08e2ff1 100644 --- a/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.html +++ b/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.html @@ -30,25 +30,44 @@ <h3><strong>{{ result.name }}</strong></h3> </ion-list> </ion-col> </ion-row> - <ion-row - *ngIf="isLocationProvided && isAreaFound; else areaNotAvailable" - class="ion-justify-content-center ion-padding-bottom"> - <ion-col size="12" class="ion-text-start"> - <h2>{{ suburbName }}</h2> - </ion-col> - <ion-col size="10" offset="1" size-md="4" offset-md="0" class="ion-text-center"> - <ion-text><h3>Loadshedding for Today</h3></ion-text> - <div class="doughnutChart-container ion-justify-content-center"> - <canvas #doughnutChartRef id="doughnutChart"></canvas> - </div> - </ion-col> - <ion-col size="12" size-md="7" offset-md="1" class="ion-text-center ion-padding-vertical ion-justify-content-center ion-padding-vertical"> - <ion-text><h3>Loadshedding for this Week</h3></ion-text> - <div class="chart-container"> - <canvas #barChart id="barChart"></canvas> - </div> - </ion-col> - </ion-row> + <div *ngIf="isLocationProvided && isAreaFound; else areaNotAvailable"> + <ion-row + class="ion-justify-content-center"> + <ion-col size="12" class="ion-text-start ion-padding-start"> + <h2>{{ suburbName }}</h2> + </ion-col> + </ion-row> + <ion-row> + <ion-col size="12" size-md="4" class="ion-text-center"> + <ion-card class="graph ion-padding-bottom ion-padding-start ion-padding-end"> + <ion-card-header> + <ion-card-title> + <ion-text><h3>Loadshedding for Today</h3></ion-text> + </ion-card-title> + </ion-card-header> + <ion-card-content> + <div class="doughnutChart-container ion-justify-content-center"> + <canvas #doughnutChartRef id="doughnutChart"></canvas> + </div> + </ion-card-content> + </ion-card> + </ion-col> + <ion-col size="12" size-md="8" class="ion-text-center ion-justify-content-center"> + <ion-card class="graph"> + <ion-card-header> + <ion-card-title> + <ion-text><h3>Loadshedding for this Week</h3></ion-text> + </ion-card-title> + </ion-card-header> + <ion-card-content> + <div class="chart-container"> + <canvas #barChart id="barChart"></canvas> + </div> + </ion-card-content> + </ion-card> + </ion-col> + </ion-row> + </div> <ng-template #areaNotAvailable> <ion-row> <ion-col size="8" offset="2" size-md="6" offset-md="3" size-xl="4" offset-xl="4"> diff --git a/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.scss b/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.scss index 150cc913..1cb6222b 100644 --- a/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.scss +++ b/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.scss @@ -11,3 +11,7 @@ max-height: 300px; overflow-y: auto; /* Allow scrolling for long lists */ } + +.graph{ + height: 95%; +} diff --git a/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.ts b/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.ts index a1c7a17f..5eb689f8 100644 --- a/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.ts +++ b/app/WhereIsThePower/src/app/tab-statistics/tab-statistics.page.ts @@ -49,7 +49,6 @@ export class TabStatisticsPage implements OnInit { })); this.filteredItems = [...this.searchItems]; console.log("Search Items:", this.filteredItems); - }); } @@ -276,9 +275,5 @@ export class TabStatisticsPage implements OnInit { this.suburbDataSubscription.unsubscribe(); this.listSuburbsSubscription.unsubscribe(); } - - ionViewDidLeave() { - this.searchTerm = ''; - } } diff --git a/app/WhereIsThePower/src/assets/CablesDamaged.svg b/app/WhereIsThePower/src/assets/CablesDamaged.svg index 3ab98ec5..d11ebf00 100644 --- a/app/WhereIsThePower/src/assets/CablesDamaged.svg +++ b/app/WhereIsThePower/src/assets/CablesDamaged.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M412-120q-12.75 0-21.375-8.625T382-150v-88L256-377q-7.652-7.857-11.826-18.214T240-417v-192.318Q240-634 257-651.5q17-17.5 41-17.5l60 60h-58v191l142 155.701V-180h76v-82l49-54L88-795q-9-9-9-21t9-21q9-9 21-9t21 9l708 708q9 9 9 21t-9 21q-9 9-21 9t-21-9L610-273l-32 35v88q0 12.75-8.625 21.375T548-120H412Zm308-489v195q0 10.667-3.5 20.333Q713-384 706-376l-14 16-32-32v-217H443L342-710v-100q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T402-810v141h156v-141q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T618-810v171l-30-30h72q24.75 0 42.375 17.625T720-609ZM553-499Zm-114 55Z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" stroke="white" fill="white" height="48" viewBox="0 -960 960 960" width="48"><path d="M412-120q-12.75 0-21.375-8.625T382-150v-88L256-377q-7.652-7.857-11.826-18.214T240-417v-192.318Q240-634 257-651.5q17-17.5 41-17.5l60 60h-58v191l142 155.701V-180h76v-82l49-54L88-795q-9-9-9-21t9-21q9-9 21-9t21 9l708 708q9 9 9 21t-9 21q-9 9-21 9t-21-9L610-273l-32 35v88q0 12.75-8.625 21.375T548-120H412Zm308-489v195q0 10.667-3.5 20.333Q713-384 706-376l-14 16-32-32v-217H443L342-710v-100q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T402-810v141h156v-141q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T618-810v171l-30-30h72q24.75 0 42.375 17.625T720-609ZM553-499Zm-114 55Z"/></svg> \ No newline at end of file diff --git a/app/WhereIsThePower/src/assets/PowerOutage.svg b/app/WhereIsThePower/src/assets/PowerOutage.svg index ea2740fd..5fa5096d 100644 --- a/app/WhereIsThePower/src/assets/PowerOutage.svg +++ b/app/WhereIsThePower/src/assets/PowerOutage.svg @@ -1 +1 @@ -<svg xmlns="http://www.w3.org/2000/svg" class="ionicon" viewBox="0 0 512 512"><path d="M432 448a15.92 15.92 0 01-11.31-4.69l-352-352a16 16 0 0122.62-22.62l352 352A16 16 0 01432 448zM294.34 84.28l-22.08 120.84a16 16 0 006.17 15.71 16.49 16.49 0 009.93 3.17h94.12l-38.37 47.42a4 4 0 00.28 5.34l17.07 17.07a4 4 0 005.94-.31l60.8-75.16a16.37 16.37 0 003.3-14.36 16 16 0 00-15.5-12H307.19L335.4 37.63c.05-.3.1-.59.13-.89A18.45 18.45 0 00302.73 23l-92.58 114.46a4 4 0 00.28 5.35l17.07 17.06a4 4 0 005.94-.31zM217.78 427.57l22-120.71a16 16 0 00-6.19-15.7 16.54 16.54 0 00-9.92-3.16h-94.1l38.36-47.42a4 4 0 00-.28-5.34l-17.07-17.07a4 4 0 00-5.93.31L83.8 293.64A16.37 16.37 0 0080.5 308 16 16 0 0096 320h108.83l-28.09 154.36v.11a18.37 18.37 0 0032.5 14.53l92.61-114.46a4 4 0 00-.28-5.35l-17.07-17.06a4 4 0 00-5.94.31z"/></svg> \ No newline at end of file +<svg xmlns="http://www.w3.org/2000/svg" stroke="white" fill="white" class="ionicon" viewBox="0 0 512 512"><path d="M432 448a15.92 15.92 0 01-11.31-4.69l-352-352a16 16 0 0122.62-22.62l352 352A16 16 0 01432 448zM294.34 84.28l-22.08 120.84a16 16 0 006.17 15.71 16.49 16.49 0 009.93 3.17h94.12l-38.37 47.42a4 4 0 00.28 5.34l17.07 17.07a4 4 0 005.94-.31l60.8-75.16a16.37 16.37 0 003.3-14.36 16 16 0 00-15.5-12H307.19L335.4 37.63c.05-.3.1-.59.13-.89A18.45 18.45 0 00302.73 23l-92.58 114.46a4 4 0 00.28 5.35l17.07 17.06a4 4 0 005.94-.31zM217.78 427.57l22-120.71a16 16 0 00-6.19-15.7 16.54 16.54 0 00-9.92-3.16h-94.1l38.36-47.42a4 4 0 00-.28-5.34l-17.07-17.07a4 4 0 00-5.93.31L83.8 293.64A16.37 16.37 0 0080.5 308 16 16 0 0096 320h108.83l-28.09 154.36v.11a18.37 18.37 0 0032.5 14.53l92.61-114.46a4 4 0 00-.28-5.35l-17.07-17.06a4 4 0 00-5.94.31z"/></svg> \ No newline at end of file diff --git a/app/WhereIsThePower/src/assets/Traffic.svg b/app/WhereIsThePower/src/assets/Traffic.svg index b251e4b9..0951d785 100644 --- a/app/WhereIsThePower/src/assets/Traffic.svg +++ b/app/WhereIsThePower/src/assets/Traffic.svg @@ -1,4 +1 @@ -<svg width="27" height="26" viewBox="0 0 27 26" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M15.92 1.01C15.72 0.42 15.16 0 14.5 0H3.5C2.84 0 2.29 0.42 2.08 1.01L0.11 6.68C0.0399999 6.89 0 7.11 0 7.34V14.5C0 15.33 0.67 16 1.5 16C2.33 16 3 15.33 3 14.5V14H10L12.5 11.5H18C17.1124 11.5 18.82 11.5 18 11.5V7.34C18 7.12 17.96 6.89 17.89 6.68L15.92 1.01ZM3.5 11C2.67 11 2 10.33 2 9.5C2 8.67 2.67 8 3.5 8C4.33 8 5 8.67 5 9.5C5 10.33 4.33 11 3.5 11ZM14.5 11C13.67 11 13 10.33 13 9.5C13 8.67 13.67 8 14.5 8C15.33 8 16 8.67 16 9.5C16 10.33 15.33 11 14.5 11ZM2 6L3.27 2.18C3.41 1.78 3.79 1.5 4.22 1.5H13.78C14.21 1.5 14.59 1.78 14.73 2.18L16 6H2Z" fill="white"/> -<path d="M24.92 11.01C24.72 10.42 24.16 10 23.5 10H12.5C11.84 10 11.29 10.42 11.08 11.01L9.11 16.68C9.04 16.89 9 17.11 9 17.34V24.5C9 25.33 9.67 26 10.5 26C11.33 26 12 25.33 12 24.5V24H24V24.5C24 25.32 24.67 26 25.5 26C26.32 26 27 25.33 27 24.5V17.34C27 17.12 26.96 16.89 26.89 16.68L24.92 11.01ZM12.5 21C11.67 21 11 20.33 11 19.5C11 18.67 11.67 18 12.5 18C13.33 18 14 18.67 14 19.5C14 20.33 13.33 21 12.5 21ZM23.5 21C22.67 21 22 20.33 22 19.5C22 18.67 22.67 18 23.5 18C24.33 18 25 18.67 25 19.5C25 20.33 24.33 21 23.5 21ZM11 16L12.27 12.18C12.41 11.78 12.79 11.5 13.22 11.5H22.78C23.21 11.5 23.59 11.78 23.73 12.18L25 16H11Z" fill="white"/> -</svg> +<svg xmlns="http://www.w3.org/2000/svg" stroke="white" fill="white" height="24" viewBox="0 -960 960 960" width="24"><path d="M224.614-220.001v37.693q0 17.628-12.352 29.967-12.353 12.34-29.999 12.34-17.647 0-29.954-12.34-12.308-12.339-12.308-29.967v-282.153q0-6.231.808-12.462t2.718-11.935l71.628-202.526q7.19-21.606 26.065-35.11 18.874-13.505 42.242-13.505h393.076q23.368 0 42.242 13.505 18.875 13.504 26.065 35.11l71.628 202.526q1.91 5.704 2.718 11.935.808 6.231.808 12.462v282.153q0 17.628-12.353 29.967-12.353 12.34-29.999 12.34-17.647 0-29.954-12.34-12.307-12.339-12.307-29.967v-37.693H224.614Zm-.307-316.92h511.386l-47.385-135.001q-1.539-3.847-4.616-5.962-3.077-2.116-7.308-2.116H283.616q-4.231 0-7.308 2.116-3.077 2.115-4.616 5.962l-47.385 135.001ZM200-476.923V-280v-196.923Zm98.552 150.769q21.832 0 37.024-15.283 15.193-15.283 15.193-37.115t-15.283-37.024q-15.283-15.193-37.115-15.193t-37.025 15.283q-15.192 15.283-15.192 37.115t15.283 37.025q15.283 15.192 37.115 15.192Zm363.077 0q21.832 0 37.025-15.283 15.192-15.283 15.192-37.115t-15.283-37.024q-15.283-15.193-37.115-15.193t-37.024 15.283q-15.193 15.283-15.193 37.115t15.283 37.025q15.283 15.192 37.115 15.192ZM200-280h560v-196.923H200V-280Z"/></svg> \ No newline at end of file diff --git a/app/WhereIsThePower/src/assets/WhereIsThePower.png b/app/WhereIsThePower/src/assets/WhereIsThePower.png new file mode 100644 index 00000000..3ac1f648 Binary files /dev/null and b/app/WhereIsThePower/src/assets/WhereIsThePower.png differ diff --git a/app/WhereIsThePower/src/assets/pdf/user-manual-desktop.pdf b/app/WhereIsThePower/src/assets/pdf/user-manual-desktop.pdf new file mode 100644 index 00000000..274c0424 Binary files /dev/null and b/app/WhereIsThePower/src/assets/pdf/user-manual-desktop.pdf differ diff --git a/app/WhereIsThePower/src/assets/pdf/user-manual-mobile.pdf b/app/WhereIsThePower/src/assets/pdf/user-manual-mobile.pdf new file mode 100644 index 00000000..85a1d50d Binary files /dev/null and b/app/WhereIsThePower/src/assets/pdf/user-manual-mobile.pdf differ diff --git a/app/WhereIsThePower/src/environments/environment.prod.ts b/app/WhereIsThePower/src/environments/environment.prod.ts index 99ca2d53..aadbcd1d 100644 --- a/app/WhereIsThePower/src/environments/environment.prod.ts +++ b/app/WhereIsThePower/src/environments/environment.prod.ts @@ -1,4 +1,4 @@ export const environment = { production: true, - MapboxApiKey: 'MapboxApiKey' + MapboxApiKey: 'HelloAPIKey' }; diff --git a/app/WhereIsThePower/src/environments/environment.ts b/app/WhereIsThePower/src/environments/environment.ts index 8559209c..d23824c7 100644 --- a/app/WhereIsThePower/src/environments/environment.ts +++ b/app/WhereIsThePower/src/environments/environment.ts @@ -1,4 +1,4 @@ export const environment = { production: false, - MapboxApiKey: 'replace' + MapboxApiKey: 'pk.eyJ1IjoidTE4MDA0ODc0IiwiYSI6ImNsajMzdWh5ZzAwcHAzZXMxc3lveXJmNDgifQ.7P_tuuiC4M_Q1_H5ZF1rTA' };