diff --git a/agrilink_vocpro/src/app/app.config.ts b/agrilink_vocpro/src/app/app.config.ts index 7e40e44..0ceb76d 100644 --- a/agrilink_vocpro/src/app/app.config.ts +++ b/agrilink_vocpro/src/app/app.config.ts @@ -1,9 +1,10 @@ import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core'; import { provideRouter } from '@angular/router'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideAnimations } from '@angular/platform-browser/animations'; import { ToastrModule } from 'ngx-toastr'; import { routes } from './app.routes'; +import { httpErrorInterceptor } from './cores/interceptors/http-error-interceptor.service'; export const appConfig: ApplicationConfig = { providers: [ @@ -12,5 +13,6 @@ export const appConfig: ApplicationConfig = { provideHttpClient(), provideAnimations(), importProvidersFrom(ToastrModule.forRoot()), + provideHttpClient(withInterceptors([httpErrorInterceptor])) // Register your HttpErrorInterceptor ] }; diff --git a/agrilink_vocpro/src/app/app.routes.ts b/agrilink_vocpro/src/app/app.routes.ts index 3a062f9..31b6cf6 100644 --- a/agrilink_vocpro/src/app/app.routes.ts +++ b/agrilink_vocpro/src/app/app.routes.ts @@ -1,7 +1,6 @@ import { Routes } from '@angular/router'; import { DashboardComponent } from './pages/dashboard/dashboard.component'; import { LayoutsComponent } from './pages/dashboard/layouts/layouts.component'; -import { GraphComponent } from './pages/dashboard/page/graph/graph.component'; import { AuthComponent } from './pages/auth/auth.component'; import { AuthGuard } from './cores/guards/auth.guard'; import { RegisterComponent } from './pages/register/register.component'; @@ -29,11 +28,6 @@ export const routes: Routes = [ component: DashboardComponent, canActivate: [AuthGuard] }, - { - path: 'graph', - component: GraphComponent, - canActivate: [AuthGuard] - }, ] } ]; diff --git a/agrilink_vocpro/src/app/cores/guards/auth.guard.ts b/agrilink_vocpro/src/app/cores/guards/auth.guard.ts index 8ed90a3..255cf26 100644 --- a/agrilink_vocpro/src/app/cores/guards/auth.guard.ts +++ b/agrilink_vocpro/src/app/cores/guards/auth.guard.ts @@ -1,16 +1,20 @@ import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { ToastrService } from 'ngx-toastr'; - +import { StorageService } from '../services/storage.service'; @Injectable({ providedIn: 'root' }) export class AuthGuard implements CanActivate { - constructor(private router: Router, private toast: ToastrService) {} + constructor( + private router: Router, + private toast: ToastrService, + private storageService: StorageService + ) {} canActivate(): boolean { - const token = localStorage.getItem('accessToken'); + const token = this.storageService.getToken(); if (token) { return true; } else { diff --git a/agrilink_vocpro/src/app/cores/interceptors/http-error-interceptor.service.ts b/agrilink_vocpro/src/app/cores/interceptors/http-error-interceptor.service.ts new file mode 100644 index 0000000..eb2420a --- /dev/null +++ b/agrilink_vocpro/src/app/cores/interceptors/http-error-interceptor.service.ts @@ -0,0 +1,43 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { ToastrService } from 'ngx-toastr'; +import { inject } from '@angular/core'; +import { catchError, throwError } from 'rxjs'; +import { HttpErrorResponse } from '@angular/common/http'; + +export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => { + const toast = inject(ToastrService); + + return next(req).pipe( + catchError((error: HttpErrorResponse) => { + let errorMessage = ''; + + if (error.error instanceof ErrorEvent) { + errorMessage = `Client error: ${error.error.message}`; + } else { + switch (error.status) { + case 0: + errorMessage = 'Network error: Please check your internet connection.'; + break; + case 401: + errorMessage = 'Unauthorized: Please log in again.'; + break; + case 403: + errorMessage = 'Forbidden: You do not have permission to access this resource.'; + break; + case 404: + errorMessage = 'Resource not found: The requested resource does not exist.'; + break; + case 500: + errorMessage = 'Server error: Please try again later.'; + break; + default: + errorMessage = `Unexpected error: ${error.message}`; + } + } + + toast.error(errorMessage, 'Error', { timeOut: 3000 }); + + return throwError(() => new Error(errorMessage)); + }) + ); +}; diff --git a/agrilink_vocpro/src/app/cores/interface/sensor-data.ts b/agrilink_vocpro/src/app/cores/interface/sensor-data.ts index 6474dff..165a711 100644 --- a/agrilink_vocpro/src/app/cores/interface/sensor-data.ts +++ b/agrilink_vocpro/src/app/cores/interface/sensor-data.ts @@ -24,9 +24,7 @@ export interface ApiResponse { }; statusCode: number; message: string; - } - - +} export interface DHTSensor { lightIntensity: number; temperature: number; diff --git a/agrilink_vocpro/src/app/cores/services/auth.service.ts b/agrilink_vocpro/src/app/cores/services/auth.service.ts index 376456b..a68eca2 100644 --- a/agrilink_vocpro/src/app/cores/services/auth.service.ts +++ b/agrilink_vocpro/src/app/cores/services/auth.service.ts @@ -1,12 +1,10 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { catchError, tap } from 'rxjs/operators'; +import { Observable, tap } from 'rxjs'; import { ApiService } from './api.service'; import { LoginData } from '../interface/auth'; -import {jwtDecode}from 'jwt-decode'; +import { jwtDecode } from 'jwt-decode'; import { StorageService } from './storage.service'; -import { ToastrService } from 'ngx-toastr'; @Injectable({ providedIn: 'root' @@ -16,9 +14,10 @@ export class AuthService extends ApiService { private logoutUrl = `${this.baseUrl}auth/logout`; private registerUrl = `${this.baseUrl}auth/register`; - constructor(http: HttpClient, + constructor( + http: HttpClient, private storageService: StorageService, - private toast: ToastrService) { + ) { super(http); } @@ -44,7 +43,7 @@ export class AuthService extends ApiService { } logout(): Observable { - const token = localStorage.getItem('accessToken'); + const token = this.storageService.getToken(); const headers = new HttpHeaders({ Authorization: `Bearer ${token}` }); @@ -54,21 +53,12 @@ export class AuthService extends ApiService { this.storageService.clearToken(); this.storageService.clearUserData(); }), - catchError(error => { - this.toast.error('Failed to logout'); - return error; - }) ); } register(data: any): Observable { const headers = new HttpHeaders({}); - return this.http.post(this.registerUrl, data, { headers }).pipe( - catchError(error => { - this.toast.error('Failed to register'); - return error; - }) - ); + return this.http.post(this.registerUrl, data, { headers }); } getUserFullName(): string | null { diff --git a/agrilink_vocpro/src/app/cores/services/sensor.service.ts b/agrilink_vocpro/src/app/cores/services/sensor.service.ts index 239ac8b..3988c74 100644 --- a/agrilink_vocpro/src/app/cores/services/sensor.service.ts +++ b/agrilink_vocpro/src/app/cores/services/sensor.service.ts @@ -18,7 +18,7 @@ export class SensorService extends ApiService { private getDataUrl = `${this.baseUrl}api/sensor/getData`; private getLatestUrl = `${this.baseUrl}api/sensor/getLatest`; - private getStatusRelay = `${this.baseUrl}api/get-relay`; + private getStatusRelay = `${this.baseUrl}api/relay/get-relay`; private createAuthHeaders(): HttpHeaders { const token = this.storageService.getToken(); @@ -38,7 +38,7 @@ export class SensorService extends ApiService { return this.http.get(this.getDataUrl, { params, headers }).pipe( catchError(error => { - this.toast.error('Failed to get sensor data, please try again'); + // this.toast.error('Failed to get sensor data for graphic, please try again'); return throwError(error); }) ); @@ -49,7 +49,7 @@ export class SensorService extends ApiService { return this.http.get(this.getLatestUrl, { headers }).pipe( catchError(error => { - this.toast.error('Failed to get sensor data, please try again'); + // this.toast.error('Failed to get latest sensor data, please try again'); return throwError(error); }) ); @@ -60,7 +60,7 @@ export class SensorService extends ApiService { return this.http.get(this.getStatusRelay, { headers }).pipe( catchError(error => { - this.toast.error('Failed to get relay status, please try again'); + // this.toast.error('Failed to get relay status, please try again'); return throwError(error); }) ); diff --git a/agrilink_vocpro/src/app/pages/auth/auth.component.html b/agrilink_vocpro/src/app/pages/auth/auth.component.html index 7a76fec..94994ac 100644 --- a/agrilink_vocpro/src/app/pages/auth/auth.component.html +++ b/agrilink_vocpro/src/app/pages/auth/auth.component.html @@ -8,24 +8,52 @@
-
- +
- + + +
- - + +
- + diff --git a/agrilink_vocpro/src/app/pages/auth/auth.component.scss b/agrilink_vocpro/src/app/pages/auth/auth.component.scss index b701036..a35f4ec 100644 --- a/agrilink_vocpro/src/app/pages/auth/auth.component.scss +++ b/agrilink_vocpro/src/app/pages/auth/auth.component.scss @@ -1,5 +1,5 @@ .login { - min-height: 100vh; + min-height: 100vh; font-family: "Onest", sans-serif; } @@ -19,11 +19,40 @@ padding: 0.75rem 1rem; } -.color-btn{ +.color-btn { background-color: #16423C; color: white; } -.forgot{ +.forgot { color: #16423C; } + +.form-floating { + position: relative; + + .btn-link { + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + border: none; + background: transparent; + cursor: pointer; + color: #16423C; + + &:hover { + color: #16423C; + } + } +} + +.form-check-input:checked { + background-color: #16423C; + border-color: #16423C; +} + +.form-check-input:checked::before { + color: white; +} + diff --git a/agrilink_vocpro/src/app/pages/auth/auth.component.ts b/agrilink_vocpro/src/app/pages/auth/auth.component.ts index ec21a19..b0f0364 100644 --- a/agrilink_vocpro/src/app/pages/auth/auth.component.ts +++ b/agrilink_vocpro/src/app/pages/auth/auth.component.ts @@ -5,11 +5,13 @@ import { StorageService } from '../../cores/services/storage.service'; import { FormsModule } from '@angular/forms'; import { LoginData } from '../../cores/interface/auth'; import { ToastrService } from 'ngx-toastr'; +import { CommonModule } from '@angular/common'; +import { catchError } from 'rxjs'; @Component({ selector: 'app-auth', standalone: true, - imports: [FormsModule, RouterModule], + imports: [FormsModule, RouterModule, CommonModule], templateUrl: './auth.component.html', styleUrls: ['./auth.component.scss'] }) @@ -17,25 +19,43 @@ export class AuthComponent { email: string = ''; password: string = ''; rememberMe: boolean = false; + loading: boolean = false; + passwordVisible: boolean = false; - constructor(private authService: AuthService, private storageService: StorageService, private router: Router, private toastr: ToastrService) {} + constructor(private authService: AuthService, + private storageService: StorageService, + private router: Router, + private toast: ToastrService) {} onSubmit() { + this.loading = true; + if (!this.email || !this.password) { + this.loading = false; + this.toast.error('Please fill in all fields.'); + return; + } + const loginData: LoginData = { email: this.email, password: this.password, rememberMe: this.rememberMe }; - + this.authService.login(loginData).subscribe( (response) => { this.storageService.saveToken(response.data.token); this.router.navigate(['/dashboard']); - this.toastr.success('Login successful'); + this.toast.success('Login successful'); + this.loading = false; }, (error) => { - this.toastr.error(error.error.message); + this.loading = false; + this.toast.error(error.error.message); } ); } -} + + togglePasswordVisibility() { + this.passwordVisible = !this.passwordVisible; // Toggle password visibility + } +} diff --git a/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.html b/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.html index 2e00c12..fdab7c1 100644 --- a/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.html +++ b/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.html @@ -1,150 +1,124 @@
-

Hello there

-

Welcome back to your management system

+

{{ greeting }}

+

Welcome back to your management system

-

Latest Update: {{latestUpdate}}

+

Latest Update: {{latestUpdate}}

- +
-
- Loading... +
+
-
-
-
-

{{sensorData.dht.lightIntensity}} Lux

-
Light Intensity
+ +
No available data
+
+ +
+
+
+
+
+

{{sensorData.dht.lightIntensity}} Lux

+
Cahaya
+
-
-
-
-

{{sensorData.dht.temperature}} °C

-
Temperature
+
+
+

{{sensorData.dht.temperature}} °C

+
Temperatur
+
-
-
-
-

{{sensorData.dht.humidity}} %RH

-
Humidity
+
+
+

{{sensorData.dht.humidity}} %RH

+
Kelembaban Udara
+
-
- -
-
-
-

{{ sensorData.npk1.temperature }} °C

-
Temperature
-
-
-
-
-

{{sensorData.npk1.moisture}} %RH

-
Moisture
-
-
-
-
-

{{sensorData.npk1.conductivity}} μS/cm

-
Conductivity
-
-
-
-
-

{{sensorData.npk1.ph}}

-
pH
-
-
-
-
-

{{sensorData.npk1.nitrogen}} PPM

-
Nitrogen
-
-
-
-
-

{{sensorData.npk1.phosphorus}} PPM

-
Phosphorus
-
-
-
-
-

{{sensorData.npk1.potassium}} PPM

-
Potassium
-
-
-
-
-

{{ sensorData.npk2.temperature }}°C

-
Temperature
-
-
-
-
-

{{ sensorData.npk1.moisture }} %RH

-
Moisture
-
-
-
-
-

{{ sensorData.npk1.conductivity }} μS/cm

-
Conductivity
-
-
-
-
-

{{ sensorData.npk1.ph }}

-
pH
-
-
-
-
-

{{ sensorData.npk1.nitrogen}} PPM

-
Nitrogen
-
-
-
-
-

{{ sensorData.npk2.phosphorus}} PPM

-
Phosphorus
-
-
-
-
-

{{ sensorData.npk2.potassium }} PPM

-
Potassium
+
+
+
+
+
+

{{sensorData.npk1.temperature}} °C

+
Temperature
+
+
+
+
+

{{sensorData.npk1.moisture}} %RH

+
Kelembaban Tanah
+
+
+
+
+

{{sensorData.npk1.conductivity}} μS/cm

+
Conductivity
+
+
+
-
-
-
-

- {{ relay.current_status ? 'ON' : 'OFF' }} -

-
Relay {{ relay.number }}
+
+
+
+
+
+

{{sensorData.npk2.temperature}} °C

+
Temperature
+
+
+
+
+

{{sensorData.npk2.moisture}} %RH

+
Kelembaban Tanah
+
+
+
+
+

{{sensorData.npk2.conductivity}} μS/cm

+
Conductivity
+
+
+ +
+
+
+ +
+
+
+
+
+

+ {{ relay.current_status ? 'ON' : 'OFF' }} +

+
Relay {{ relay.number }}
+
+
-
-
Monitoring
-
- -
+
Monitoring Graphs
+
+ +
+ diff --git a/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.scss b/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.scss index 44ca4c0..4da6bbb 100644 --- a/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.scss +++ b/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.scss @@ -97,7 +97,6 @@ button { .loading{ font-size: 18px; text-align: center; - color: #888; } .status-on { @@ -107,3 +106,7 @@ button { .status-off { color: rgb(144, 6, 6); } + +.spinner { + color: #16423C +} \ No newline at end of file diff --git a/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.ts b/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.ts index 5a47b28..0564e1c 100644 --- a/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.ts +++ b/agrilink_vocpro/src/app/pages/dashboard/dashboard.component.ts @@ -5,7 +5,7 @@ import { GraphComponent } from './page/graph/graph.component'; import { CommonModule } from '@angular/common'; import { SensorService } from '../../cores/services/sensor.service'; import { SensorData, StatusRelay } from '../../cores/interface/sensor-data'; -import { interval } from 'rxjs'; +import { ToastrService } from 'ngx-toastr'; @Component({ selector: 'app-dashboard', @@ -16,9 +16,12 @@ import { interval } from 'rxjs'; }) export class DashboardComponent implements OnInit { isLoaded: boolean = false; + hasError: boolean = false; + noData: boolean = false; selectedButton: string = ''; latestUpdate: string = ''; intervalId: any; + greeting: string = ''; sensorData: SensorData = { dht: { lightIntensity: 0, temperature: 0, humidity: 0 }, npk1: { temperature: 0, moisture: 0, conductivity: 0, ph: 0, nitrogen: 0, phosphorus: 0, potassium: 0 }, @@ -26,11 +29,12 @@ export class DashboardComponent implements OnInit { }; relayStatus: StatusRelay[] = []; - constructor(private apiService: SensorService) {} + constructor(private apiService: SensorService, private toast: ToastrService) {} ngOnInit(): void { this.selectedButton = 'dht'; this.startClock(); + this.updateGreeting(); this.loadData(); } @@ -61,68 +65,88 @@ export class DashboardComponent implements OnInit { this.latestUpdate = now.toLocaleString('en-GB', options); } + updateGreeting(): void { + const hour = new Date().getHours(); + if (hour < 12) { + this.greeting = 'Good Morning'; + } else if (hour < 18) { + this.greeting = 'Good Afternoon'; + } else { + this.greeting = 'Good Evening'; + } + } selectSensor(param: string): void { this.selectedButton = param; - if(param==='relay'){ + if(param === 'relay'){ this.loadRelayData(); - }else{ - this.loadData(); + } else { + this.loadData(); } } loadData(): void { this.isLoaded = true; - + this.hasError = false; + this.noData = false; + this.apiService.getLatestData().subscribe( (response) => { const data = response.data; console.log('Data:', data); - if (data.dht && data.dht.length > 0) { - this.sensorData.dht = { - lightIntensity: data.dht[0].viciluminosity ?? 0, - temperature: data.dht[0].vicitemperature ?? 0, - humidity: data.dht[0].vicihumidity ?? 0 - }; - } - - if (data.npk1 && data.npk1.length > 0) { - this.sensorData.npk1 = { - temperature: data.npk1[0].soiltemperature ?? 0, - moisture: data.npk1[0].soilhumidity ?? 0, - conductivity: data.npk1[0].soilconductivity ?? 0, - ph: data.npk1[0].soilph ?? 0, - nitrogen: data.npk1[0].soilnitrogen ?? 0, - phosphorus: data.npk1[0].soilphosphorus ?? 0, - potassium: data.npk1[0].soilpotassium ?? 0 - }; - } - - if (data.npk2 && data.npk2.length > 0) { - this.sensorData.npk2 = { - temperature: data.npk2[0].soiltemperature ?? 0, - moisture: data.npk2[0].soilhumidity ?? 0, - conductivity: data.npk2[0].soilconductivity ?? 0, - ph: data.npk2[0].soilph ?? 0, - nitrogen: data.npk2[0].soilnitrogen ?? 0, - phosphorus: data.npk2[0].soilphosphorus ?? 0, - potassium: data.npk2[0].soilpotassium ?? 0 - }; + + if ((!data.dht || data.dht.length === 0) && + (!data.npk1 || data.npk1.length === 0) && + (!data.npk2 || data.npk2.length === 0)) { + this.noData = true; + } else { + if (data.dht && data.dht.length > 0) { + this.sensorData.dht = { + lightIntensity: data.dht[0].viciluminosity ?? 0, + temperature: data.dht[0].vicitemperature ?? 0, + humidity: data.dht[0].vicihumidity ?? 0 + }; + } + + if (data.npk1 && data.npk1.length > 0) { + this.sensorData.npk1 = { + temperature: data.npk1[0].soiltemperature ?? 0, + moisture: data.npk1[0].soilhumidity ?? 0, + conductivity: data.npk1[0].soilconductivity ?? 0, + ph: data.npk1[0].soilph ?? 0, + nitrogen: data.npk1[0].soilnitrogen ?? 0, + phosphorus: data.npk1[0].soilphosphorus ?? 0, + potassium: data.npk1[0].soilpotassium ?? 0 + }; + } + + if (data.npk2 && data.npk2.length > 0) { + this.sensorData.npk2 = { + temperature: data.npk2[0].soiltemperature ?? 0, + moisture: data.npk2[0].soilhumidity ?? 0, + conductivity: data.npk2[0].soilconductivity ?? 0, + ph: data.npk2[0].soilph ?? 0, + nitrogen: data.npk2[0].soilnitrogen ?? 0, + phosphorus: data.npk2[0].soilphosphorus ?? 0, + potassium: data.npk2[0].soilpotassium ?? 0 + }; + } } this.updateLatestTime(); this.isLoaded = false; }, (error) => { - console.error('Error fetching sensor data:', error); this.isLoaded = false; + this.handleError(error); } ); } loadRelayData(): void { this.isLoaded = true; - + this.hasError = false; + this.apiService.getRelayStatus().subscribe( (response) => { if (Array.isArray(response.data)) { @@ -134,16 +158,26 @@ export class DashboardComponent implements OnInit { created_at: relay.created_at, current_status: relay.current_status })); + this.relayStatus.sort((a, b) => a.number - b.number); } - console.log('Relay Data:', response); this.isLoaded = false; }, (error) => { - console.error('Error fetching relay data:', error); + this.toast.error('Failed to get relay status, please try again'); + this.hasError = true; this.isLoaded = false; } ); } - -} + handleError(error: any): void { + if (this.selectedButton === 'dht') { + this.toast.error('Error fetching DHT sensor data. Please try again.', 'Error', { timeOut: 3000 }); + } else if (this.selectedButton === 'npk1') { + this.toast.error('Error fetching NPK1 sensor data. Please try again.', 'Error', { timeOut: 3000 }); + } else if (this.selectedButton === 'npk2') { + this.toast.error('Error fetching NPK2 sensor data. Please try again.', 'Error', { timeOut: 3000 }); + } + this.hasError = true; + } +} diff --git a/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.html b/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.html index 90d7ea4..b4d0e0e 100644 --- a/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.html +++ b/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.html @@ -1,17 +1,58 @@
- - - -
+ +
No available data
+
-
- Loading... -
+ +
+
Sensor BHT
+ +
+ +
+
+ + +

No available data

+
+
- +
+
+
Sensor NPK 1
+ +
+ +
+ +
+
+ + +

No available data

+
+
+ +
+
+
Sensor NPK 2
+ +
+ +
+ +
+
+ + +

No available data

+
+
+
+
diff --git a/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.scss b/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.scss index 7783e2b..5d34e0f 100644 --- a/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.scss +++ b/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.scss @@ -1,54 +1,74 @@ -.chart-container { - width: 100%; - max-width: 800px; - margin: auto; - text-align: center; - } - - canvas { - width: 100%; - height: 30vh; - margin: 20px; - align-items: center; - } +.container-graph { + display: flex; + flex-direction: column; + justify-content: flex-start; + height: max-content; - .form-select{ - width: max-content; - margin-top: 20px; - display: flexbox; - flex-direction: column; - margin-right: 20px; - } - - .container-graph{ + .sensor-wrapper { display: flex; - flex-wrap: wrap; - justify-content: left; - height: max-content; - } - - - @media (max-width: 768px) { - canvas{ - display: flex; - width: max-content; - flex: 1 1 45%; - } - } - - @media (max-width: 576px) { - canvas{ - display: flex; - width: max-content; - flex: 1 1 100%; + flex-direction: column; + align-items: center; + margin-bottom: 35px; + background-color: #f1f1f1; + border-radius: 10px; + + h2 { + font-size: 18px; + margin-bottom: 10px; } } - .loading{ - font-size: 18px; - text-align: center; - color: #888; + .title-with-dropdown { + display: flex; + align-items: center; + justify-content: center; + width: 50%; + + .title { + flex: 0 0 25%; + text-align: center; + } + + .form-select { + flex: 0 0 25%; + margin: 18px 0px 0px 15px; + } + } } - - \ No newline at end of file +.title { + text-align: center; + font-size: 20px; + margin: 18px 0px 0px 0px; +} + +canvas { + height: 500px !important; + width: 100%; + max-width: 2000px; + padding-bottom: 20px; + padding-top: 20px; +} + +@media (max-width: 768px) { + canvas { + height: 300px !important; + } +} + +.loading { + font-size: 18px; + text-align: center; +} + +.spinner { + color: #16423C +} + +.no-data { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + font-size: 18px; +} diff --git a/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.ts b/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.ts index 4fbf721..3b11f7f 100644 --- a/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.ts +++ b/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.ts @@ -1,22 +1,23 @@ import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core'; import { Chart, registerables } from 'chart.js'; import { SensorService } from '../../../../cores/services/sensor.service'; -import { ApiResponse, ParameterSensor} from '../../../../cores/interface/sensor-data'; +import { ApiResponse, ParameterSensor } from '../../../../cores/interface/sensor-data'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; Chart.register(...registerables); + const parameterColors: { [key: string]: string } = { vicitemperature: '#8F5A62', vicihumidity: '#16423C', viciluminosity: '#DF9B55', - soiltemperature: '#8F5A62', - soilhumidity: '#54909c', - soilconductivity: '#661311', - soilph: '#664735', - soilnitrogen: '#3a6635', - soilphosphorus: '#3f3566', - soilpotassium: '#5f3566', + soiltemperature: '#FF6347', + soilhumidity: '#00BFFF', + soilconductivity: '#A52A2A', + soilph: '#228B22', + soilnitrogen: '#FEDC56', + soilphosphorus: '#B80F0A', + soilpotassium: '#6F2DA8', }; @Component({ @@ -27,11 +28,19 @@ const parameterColors: { [key: string]: string } = { styleUrls: ['./graph.component.scss'] }) export class GraphComponent implements OnInit, AfterViewInit, OnDestroy { - @ViewChild('myChart', { static: false }) chartElement!: ElementRef; - selectedSensor: string = ''; - selectedParameter: string = ''; - parameters: string[] = []; - isLoading: boolean = true; + @ViewChild('myChartDHT', { static: false }) dhtChartElement!: ElementRef; + @ViewChild('myChartNPK1', { static: false }) npk1ChartElement!: ElementRef; + @ViewChild('myChartNPK2', { static: false }) npk2ChartElement!: ElementRef; + + isLoadingDHT: boolean = true; + isLoadingNPK1: boolean = true; + isLoadingNPK2: boolean = true; + + isNoDataDHT: boolean = false; + isNoDataNPK1: boolean = false; + isNoDataNPK2: boolean = false; + + allNoData: boolean = false; sensorParameters: { [key: string]: string[] } = { dht: ['vicitemperature', 'vicihumidity', 'viciluminosity'], @@ -47,28 +56,32 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy { soilhumidity: 'Soil Humidity (%)', soilconductivity: 'Conductivity (μS/cm)', soilph: 'pH', - soilnitrogen: 'Nitrogen (PPM)', - soilphosphorus: 'Phosphorus (PPM)', - soilpotassium: 'Potassium (PPM)', + soilnitrogen: 'Nitrogen (mg/l)', + soilphosphorus: 'Phosphorus (mg/l)', + soilpotassium: 'Potassium (mg/l)', }; - chart: Chart | undefined; - labelsHourly: string[] = []; + charts: { [key: string]: Chart | undefined } = { + dht: undefined, + npk1: undefined, + npk2: undefined, + }; + + selectedNPK1: string = 'npk'; + selectedNPK2: string = 'npk'; + private resizeListener!: () => void; constructor(private sensorService: SensorService) {} ngOnInit(): void { - this.selectedSensor = 'dht'; - this.updateParameters(); - this.selectedParameter = 'vicitemperature'; - this.updateChart(); this.resizeListener = this.onResize.bind(this); window.addEventListener('resize', this.resizeListener); + this.updateCharts(); } ngAfterViewInit(): void { - this.updateChart(); + this.onResize(); } ngOnDestroy(): void { @@ -76,12 +89,204 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy { } onResize(): void { - if (this.chart) { - this.chart.destroy(); - this.updateChart(); - } + Object.values(this.charts).forEach(chart => { + if (chart) { + chart.destroy(); + } + }); + this.updateCharts(); } + updateCharts(): void { + this.isLoadingDHT = this.isLoadingNPK1 = this.isLoadingNPK2 = true; + + const today = new Date(); + const year = today.getFullYear(); + const month = String(today.getMonth() + 1).padStart(2, '0'); + const day = String(today.getDate()).padStart(2, '0'); + + const startEnd = `${year}-${month}-${day}`; + // const startEnd = '2024-10-24'; + const timeRange = 'HOURLY'; + + // Fetch data for DHT + this.sensorService.getSensorData('dht', '', startEnd, timeRange).subscribe({ + next: (response) => { + if (response.statusCode === 200 && response.data.dht?.length > 0) { + this.createChart(this.dhtChartElement.nativeElement, response, 'dht', 'npk'); + this.isNoDataDHT = false; + this.allNoData = false; + } else { + this.isNoDataDHT = true; + } + this.isLoadingDHT = false; + }, + error: () => { + this.isLoadingDHT = false; + this.isNoDataDHT = true; + } + }); + + // Fetch data for NPK1 + this.sensorService.getSensorData('npk1', '', startEnd, timeRange).subscribe({ + next: (response) => { + if (response.statusCode === 200 && response.data.npk1?.length > 0) { + this.createChart(this.npk1ChartElement.nativeElement, response, 'npk1', this.selectedNPK1); + this.isNoDataNPK1 = false; + this.allNoData = false; + } else { + this.isNoDataNPK1 = true; + } + this.isLoadingNPK1 = false; + }, + error: () => { + this.isLoadingNPK1 = false; + this.isNoDataNPK1 = true; + } + }); + + // Fetch data for NPK2 + this.sensorService.getSensorData('npk2', '', startEnd, timeRange).subscribe({ + next: (response) => { + if (response.statusCode === 200 && response.data.npk2?.length > 0) { + this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2); + this.isNoDataNPK2 = false; + this.allNoData = false; + } else { + this.isNoDataNPK2 = true; + } + this.isLoadingNPK2 = false; + }, + error: () => { + this.isLoadingNPK2 = false; + this.isNoDataNPK2 = true; + } + }); + } + + createChart(canvas: HTMLCanvasElement, response: ApiResponse, sensor: string, selectedOption: string): void { + const ctx = canvas.getContext('2d'); + const parameters = this.sensorParameters[sensor]; + + if (!ctx) { + console.error('Failed to get canvas context for sensor:', sensor); + return; + } + + let datasets: any[] = []; + + if (sensor === 'dht') { + // Handle DHT parameters directly + datasets = ['vicitemperature', 'vicihumidity', 'viciluminosity'].map(parameter => { + const { data, labels } = this.getDataFromResponse(response, sensor, parameter); + + if (data.length === 0) { + console.warn(`No data found for parameter: ${parameter}`); + return null; + } + + const displayName = this.parameterDisplayNames[parameter] || parameter; + const borderColor = parameterColors[parameter] || '#000000'; + const backgroundColor = `${borderColor}4D`; + + return { + label: displayName, + data, + borderColor, + borderWidth: 1.5, + fill: true, + backgroundColor, + tension: 0.5, + pointRadius: 0, + pointHoverRadius: 0, + }; + }).filter(dataset => dataset !== null); + } else { + // Handle NPK1 and NPK2 as before + datasets = parameters.map(parameter => { + const { data, labels } = this.getDataFromResponse(response, sensor, parameter); + + if (data.length === 0) { + console.warn(`No data found for parameter: ${parameter}`); + return null; + } + + const displayName = this.parameterDisplayNames[parameter] || parameter; + const borderColor = parameterColors[parameter] || '#000000'; + const backgroundColor = `${borderColor}4D`; + + // Filter datasets based on the selected option + if (selectedOption === 'npk' && ['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter)) { + return { + label: displayName, + data, + borderColor, + borderWidth: 1.5, + fill: true, + backgroundColor, + tension: 0.5, + pointRadius: 0, + pointHoverRadius: 0, + }; + } else if (selectedOption === 'others' && !['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter)) { + return { + label: displayName, + data, + borderColor, + borderWidth: 1.5, + fill: true, + backgroundColor, + tension: 0.5, + pointRadius: 0, + pointHoverRadius: 0, + }; + } + + return null; + }).filter(dataset => dataset !== null); + } + + if (datasets.length === 0) { + console.warn('No valid datasets to render for sensor:', sensor); + return; + } + + if (this.charts[sensor]) { + this.charts[sensor]?.destroy(); + } + + this.charts[sensor] = new Chart(ctx, { + type: 'line', + data: { + labels: this.getLabels(response, sensor), + datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + tooltip: { + enabled: true, + mode: 'nearest', + intersect: false, + callbacks: { + label: (tooltipItem) => { + const paramLabel = tooltipItem.dataset.label; + const value = tooltipItem.formattedValue; + return `${paramLabel}: ${value}`; + } + } + }, + legend: { display: true } + }, + scales: { + x: { grid: { display: false }, beginAtZero: true }, + y: { grid: { display: false }, beginAtZero: true } + } + } + }); + } + getDataFromResponse(response: ApiResponse, sensor: string, parameter: string): { data: number[], labels: string[] } { const sensorData = response.data[sensor as keyof typeof response.data]; @@ -100,112 +305,8 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy { return { data, labels }; } - - createChart(data: number[], parameter: string, labels: string[]): void { - const canvas = this.chartElement.nativeElement; - const ctx = canvas.getContext('2d'); - - if (this.chart) { - this.chart.destroy(); - } - - if (ctx) { - const displayName = this.parameterDisplayNames[parameter] || parameter; - const borderColor = parameterColors[parameter] || '#000000'; - const backgroundColor = `${borderColor}4D`; - - this.chart = new Chart(ctx, { - type: 'line', - data: { - labels: labels, - datasets: [{ - label: displayName, - data, - borderColor, - borderWidth: 1.5, - fill: true, - backgroundColor, - tension: 0.5, - pointRadius: 0, - pointHoverRadius: 0, - pointBackgroundColor: borderColor, - }] - }, - options: { - responsive: true, - maintainAspectRatio: false, - plugins: { - tooltip: { - enabled: true, - mode: 'nearest', - intersect: false, - callbacks: { - label: (tooltipItem) => { - const paramLabel = displayName; - const value = tooltipItem.formattedValue; - return `${paramLabel}: ${value}`; - } - } - }, - legend: { display: false } - }, - scales: { - x: { grid: { display: false }, beginAtZero: true }, - y: { grid: { display: false }, beginAtZero: true } - } - } - }); - } - } - - - getParameterKey(displayName: string): string { - return Object.keys(this.parameterDisplayNames).find(key => this.parameterDisplayNames[key] === displayName) || ''; - } - - onSensorChange(event: Event): void { - const select = event.target as HTMLSelectElement; - this.selectedSensor = select.value; - this.updateParameters(); - if (this.selectedSensor === 'dht') { - this.selectedParameter = 'vicitemperature'; - } else if (this.selectedSensor === 'npk1' || this.selectedSensor === 'npk2') { - this.selectedParameter = 'soiltemperature'; - } - this.updateChart(); - } - - - updateParameters(): void { - this.parameters = this.sensorParameters[this.selectedSensor].map(param => this.parameterDisplayNames[param]); - this.selectedParameter = this.parameters[1]; - } - - updateChart(): void { - this.isLoading = true; - - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); - const day = String(today.getDate()).padStart(2, '0'); - - const startEnd= `${year}-${month}-${day}`; - const timeRange = 'HOURLY'; - - this.sensorService.getSensorData(this.selectedSensor, this.selectedParameter, startEnd, timeRange).subscribe( - (response: ApiResponse) => { - if (response.statusCode === 200) { - const { data, labels } = this.getDataFromResponse(response, this.selectedSensor, this.selectedParameter); - this.createChart(data, this.selectedParameter, labels); - } else { - console.error('Error fetching data:', response.message); - } - this.isLoading = false; - }, - (error) => { - console.error('API Error:', error); - this.isLoading = false; - } - ); + getLabels(response: ApiResponse, sensor: string): string[] { + const sensorData = response.data[sensor as keyof typeof response.data]; + return sensorData ? sensorData.map(item => `${item.hour}.00`) : []; } } diff --git a/agrilink_vocpro/src/app/pages/register/register.component.html b/agrilink_vocpro/src/app/pages/register/register.component.html index 29127e1..c89efc2 100644 --- a/agrilink_vocpro/src/app/pages/register/register.component.html +++ b/agrilink_vocpro/src/app/pages/register/register.component.html @@ -10,25 +10,44 @@
- +
- +
- +
- + + +
- +
diff --git a/agrilink_vocpro/src/app/pages/register/register.component.scss b/agrilink_vocpro/src/app/pages/register/register.component.scss index b701036..0511563 100644 --- a/agrilink_vocpro/src/app/pages/register/register.component.scss +++ b/agrilink_vocpro/src/app/pages/register/register.component.scss @@ -27,3 +27,22 @@ .forgot{ color: #16423C; } + +.form-floating { + position: relative; + + .btn-link { + position: absolute; + top: 50%; + right: 10px; + transform: translateY(-50%); + border: none; + background: transparent; + cursor: pointer; + color: #16423C; + + &:hover { + color: #16423C; + } + } +} diff --git a/agrilink_vocpro/src/app/pages/register/register.component.ts b/agrilink_vocpro/src/app/pages/register/register.component.ts index 279450b..c1767c7 100644 --- a/agrilink_vocpro/src/app/pages/register/register.component.ts +++ b/agrilink_vocpro/src/app/pages/register/register.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; // Import OnInit import { Router, RouterModule } from '@angular/router'; import { AuthService } from '../../cores/services/auth.service'; import { FormsModule } from '@angular/forms'; @@ -13,15 +13,35 @@ import { CommonModule } from '@angular/common'; templateUrl: './register.component.html', styleUrls: ['./register.component.scss'] }) -export class RegisterComponent { +export class RegisterComponent implements OnInit { username: string = ''; password: string = ''; email: string = ''; fullname: string = ''; + loading: boolean = false; + passwordVisible: boolean = false; constructor(private authService: AuthService, private router: Router, private toast: ToastrService) {} + ngOnInit(): void { + this.username = ''; + this.password = ''; + this.email = ''; + this.fullname = ''; + } + + togglePasswordVisibility() { + this.passwordVisible = !this.passwordVisible; + } + onSubmit() { + this.loading = true; + if (!this.username || !this.password || !this.email || !this.fullname) { + this.loading = false; + this.toast.error('Please fill in all fields.'); + return; + } + const registrationData: RegistrationData = { username: this.username, pwd: this.password, @@ -33,10 +53,12 @@ export class RegisterComponent { this.authService.register(registrationData).subscribe( (response) => { + this.loading = false; this.toast.success('Registration successful'); this.router.navigate(['/auth']); }, (error) => { + this.loading = false; this.toast.error(error.error.message); } ); diff --git a/agrilink_vocpro/src/index.html b/agrilink_vocpro/src/index.html index 42cbd9d..07b7a5c 100644 --- a/agrilink_vocpro/src/index.html +++ b/agrilink_vocpro/src/index.html @@ -17,6 +17,7 @@ +