fix(login+register+dashboard): adding interceptor handling error, changing interface graphs

This commit is contained in:
Desy Ayurianti 2024-10-25 10:55:52 +07:00
parent fac32f6978
commit ee463ec174
20 changed files with 755 additions and 413 deletions

View File

@ -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
]
};

View File

@ -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]
},
]
}
];

View File

@ -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 {

View File

@ -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));
})
);
};

View File

@ -24,9 +24,7 @@ export interface ApiResponse {
};
statusCode: number;
message: string;
}
}
export interface DHTSensor {
lightIntensity: number;
temperature: number;

View File

@ -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<any> {
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<any> {
const headers = new HttpHeaders({});
return this.http.post<any>(this.registerUrl, data, { headers }).pipe(
catchError(error => {
this.toast.error('Failed to register');
return error;
})
);
return this.http.post<any>(this.registerUrl, data, { headers });
}
getUserFullName(): string | null {

View File

@ -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<ApiResponse>(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<any>(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<any>(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);
})
);

View File

@ -8,24 +8,52 @@
<div class="col-md-9 col-lg-8 mx-auto">
<h3 class="login-heading mb-4">Welcome back!</h3>
<form (ngSubmit)="onSubmit()">
<div class="form-floating mb-3">
<input type="email" class="form-control" [(ngModel)]="email" name="email" id="floatingInput" placeholder="name@example.com" required>
<input type="email"
class="form-control"
[(ngModel)]="email"
name="email"
id="floatingInput"
placeholder="name@example.com"
required>
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating mb-3">
<input type="password" class="form-control" [(ngModel)]="password" name="password" id="floatingPassword" placeholder="Password" required>
<input [type]="passwordVisible ? 'text' : 'password'"
class="form-control"
[(ngModel)]="password"
name="password"
id="floatingPassword"
placeholder="Password"
required>
<label for="floatingPassword">Password</label>
<button type="button"
class="btn btn-link position-absolute end-0 me-3"
(click)="togglePasswordVisibility()">
<i class="fa" [ngClass]="passwordVisible ? 'fa-eye' : 'fa-eye-slash'"></i>
</button>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" [(ngModel)]="rememberMe" name="rememberMe" id="rememberPasswordCheck">
<label class="form-check-label" for="rememberPasswordCheck">Remember Me? </label>
<input class="form-check-input"
type="checkbox"
[(ngModel)]="rememberMe"
name="rememberMe"
id="rememberPasswordCheck">
<label class="form-check-label" for="rememberPasswordCheck">Remember me</label>
</div>
<div class="d-grid">
<button class="btn btn-lg color-btn btn-login text-uppercase fw-bold mb-2" type="submit">Sign in</button>
<button class="btn btn-lg color-btn btn-login text-uppercase fw-bold mb-2"
type="submit"
[disabled]="loading">
<span *ngIf="!loading">Sign in</span>
<span *ngIf="loading">
<i class="fa fa-spinner fa-spin"></i>
</span>
</button>
<div class="text-center">
<a class="small forgot" routerLink= '/register'>Register here</a>
</div>

View File

@ -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;
}

View File

@ -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,10 +19,22 @@ 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,
@ -31,11 +45,17 @@ export class AuthComponent {
(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
}
}

View File

@ -1,150 +1,124 @@
<div class="container">
<div>
<h1 class="title">Hello there</h1>
<h3 class="description">Welcome back to your management system</h3>
<h1 class="title">{{ greeting }}</h1>
<h3 class="description">Welcome back to your management system</h3>
</div>
<div>
<h2 class="update">Latest Update: {{latestUpdate}}</h2>
<h2 class="update">Latest Update: {{latestUpdate}}</h2>
</div>
<div>
<button [ngClass]="{'active-button': selectedButton === 'dht'}" (click)="selectSensor('dht')">BHT</button>
<button [ngClass]="{'active-button': selectedButton === 'dht'}" (click)="selectSensor('dht')">DHT</button>
<button [ngClass]="{'active-button': selectedButton === 'npk1'}" (click)="selectSensor('npk1')">NPK 1</button>
<button [ngClass]="{'active-button': selectedButton === 'npk2'}" (click)="selectSensor('npk2')">NPK 2</button>
<button [ngClass]="{'active-button': selectedButton === 'relay'}" (click)="selectSensor('relay')">Relay</button>
</div>
<div *ngIf="isLoaded " class="loading">
Loading...
<div *ngIf="isLoaded" class="loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div *ngIf="!isLoaded && selectedButton === 'dht'" class="card-container">
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.lightIntensity}} Lux</h3>
<h6>Light Intensity</h6>
<ng-template #noData>
<div class="loading">No available data</div>
</ng-template>
<div *ngIf="!isLoaded && selectedButton === 'dht'">
<div *ngIf="sensorData.dht.lightIntensity || sensorData.dht.temperature || sensorData.dht.humidity; else noData">
<div class="card-container">
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.lightIntensity}} Lux</h3>
<h6>Cahaya</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.temperature}} °C</h3>
<h6>Temperatur</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.humidity}} %RH</h3>
<h6>Kelembaban Udara</h6>
</div>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.temperature}} °C</h3>
<h6>Temperature</h6>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'npk1'">
<div *ngIf="sensorData.npk1.temperature || sensorData.npk1.moisture || sensorData.npk1.conductivity; else noData">
<div class="card-container">
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.temperature}} °C</h3>
<h6>Temperature</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.moisture}} %RH</h3>
<h6>Kelembaban Tanah</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.conductivity}} μS/cm</h3>
<h6>Conductivity</h6>
</div>
</div>
<!-- Additional parameters for NPK1... -->
</div>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'npk2'">
<div *ngIf="sensorData.npk2.temperature || sensorData.npk2.moisture || sensorData.npk2.conductivity; else noData">
<div class="card-container">
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.temperature}} °C</h3>
<h6>Temperature</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.moisture}} %RH</h3>
<h6>Kelembaban Tanah</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.conductivity}} μS/cm</h3>
<h6>Conductivity</h6>
</div>
</div>
<!-- Additional parameters for NPK2... -->
</div>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'relay'">
<div *ngIf="relayStatus.length > 0; else noData">
<div class="card-container" *ngFor="let relay of relayStatus;">
<div class="card-parameter">
<div>
<h3 [ngClass]="relay.current_status ? 'status-on' : 'status-off'">
{{ relay.current_status ? 'ON' : 'OFF' }}
</h3>
<h6>Relay {{ relay.number }}</h6>
</div>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.humidity}} %RH</h3>
<h6>Humidity</h6>
</div>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'npk1'" class="card-container">
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk1.temperature }} °C</h3>
<h6>Temperature</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.moisture}} %RH</h3>
<h6>Moisture</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.conductivity}} μS/cm</h3>
<h6>Conductivity</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.ph}}</h3>
<h6>pH</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.nitrogen}} PPM</h3>
<h6>Nitrogen</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.phosphorus}} PPM</h3>
<h6>Phosphorus</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.potassium}} PPM</h3>
<h6>Potassium</h6>
</div>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'npk2'" class="card-container">
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk2.temperature }}°C</h3>
<h6>Temperature</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk1.moisture }} %RH</h3>
<h6>Moisture</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk1.conductivity }} μS/cm</h3>
<h6>Conductivity</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk1.ph }}</h3>
<h6>pH</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk1.nitrogen}} PPM</h3>
<h6>Nitrogen</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk2.phosphorus}} PPM</h3>
<h6>Phosphorus</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{ sensorData.npk2.potassium }} PPM</h3>
<h6>Potassium</h6>
</div>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'relay'" class="card-container">
<div class="card-parameter" *ngFor="let relay of relayStatus;">
<div>
<h3 [ngClass]="relay.current_status ? 'status-on' : 'status-off'">
{{ relay.current_status ? 'ON' : 'OFF' }}
</h3>
<h6>Relay {{ relay.number }}</h6>
</div>
</div>
</div>
<div class="graph">
<div class="title-graph">Monitoring</div>
<div class="graph">
<app-graph></app-graph>
</div>
<div class="title-graph">Monitoring Graphs</div>
<div class="graph">
<app-graph></app-graph>
</div>
</div>
</div>

View File

@ -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
}

View File

@ -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,67 +65,87 @@ 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{
} 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.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.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.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) => {
@ -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;
}
}

View File

@ -1,17 +1,58 @@
<div class="container-graph">
<select class="form-select" id="sensorSelect" (change)="onSensorChange($event)">
<option value="dht">BHT</option>
<option value="npk1">NPK 1</option>
<option value="npk2">NPK 2</option>
</select>
<ng-container *ngIf="allNoData; else graphContent">
<div class="no-data">No available data</div>
</ng-container>
<select class="form-select" id="parameterSelect" (change)="updateChart()" [(ngModel)]="selectedParameter">
<option *ngFor="let parameter of parameters" [value]="getParameterKey(parameter)">{{ parameter }}</option>
</select>
<ng-template #graphContent>
<div class="sensor-wrapper">
<div class="title">Sensor BHT</div>
<ng-container *ngIf="isLoadingDHT; else dhtData">
<div class="d-flex align-items-center" style="padding: 50px 0px 50px 0px">
<i class="fa fa-spinner fa-spin"></i>
</div>
</ng-container>
<ng-template #dhtData>
<canvas #myChartDHT id="myChartDHT" width="600" height="300" style="height: 300px;"></canvas>
<p *ngIf="isNoDataDHT" class="no-data">No available data</p>
</ng-template>
</div>
<div class="sensor-wrapper">
<div class="title-with-dropdown">
<div class="title">Sensor NPK 1</div>
<select class="form-select" style="margin-top: 10px" [(ngModel)]="selectedNPK1" (change)="updateCharts()">
<option value="npk">NPK</option>
<option value="others">Lainnya</option>
</select>
</div>
<ng-container *ngIf="isLoadingNPK1; else npk1Data">
<div class="d-flex align-items-center" style="padding: 50px 0px 50px 0px">
<i class="fa fa-spinner fa-spin"></i>
</div>
</ng-container>
<ng-template #npk1Data>
<canvas #myChartNPK1 id="myChartNPK1"></canvas>
<p *ngIf="isNoDataNPK1" class="no-data">No available data</p>
</ng-template>
</div>
<div class="sensor-wrapper">
<div class="title-with-dropdown">
<div class="title">Sensor NPK 2</div>
<select class="form-select" style="margin-top: 10px" [(ngModel)]="selectedNPK2" (change)="updateCharts()">
<option value="npk">NPK</option>
<option value="others">Lainnya</option>
</select>
</div>
<ng-container *ngIf="isLoadingNPK2; else npk2Data">
<div class="d-flex align-items-center" style="padding: 50px 0px 50px 0px;">
<i class="fa fa-spinner fa-spin"></i>
</div>
</ng-container>
<ng-template #npk2Data>
<canvas #myChartNPK2 id="myChartNPK2"></canvas>
<p *ngIf="isNoDataNPK2" class="no-data">No available data</p>
</ng-template>
</div>
</ng-template>
</div>
<div *ngIf="isLoading" class="loading">
Loading...
</div>
<canvas #myChart id="myChart"></canvas>

View File

@ -1,54 +1,74 @@
.chart-container {
width: 100%;
max-width: 800px;
margin: auto;
text-align: center;
}
.container-graph {
display: flex;
flex-direction: column;
justify-content: flex-start;
height: max-content;
canvas {
width: 100%;
height: 30vh;
margin: 20px;
align-items: center;
}
.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;
}
flex-direction: column;
align-items: center;
margin-bottom: 35px;
background-color: #f1f1f1;
border-radius: 10px;
@media (max-width: 768px) {
canvas{
display: flex;
width: max-content;
flex: 1 1 45%;
h2 {
font-size: 18px;
margin-bottom: 10px;
}
}
@media (max-width: 576px) {
canvas{
display: flex;
width: max-content;
flex: 1 1 100%;
.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;
}
}
.loading{
font-size: 18px;
text-align: center;
color: #888;
}
.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;
}

View File

@ -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<HTMLCanvasElement>;
selectedSensor: string = '';
selectedParameter: string = '';
parameters: string[] = [];
isLoading: boolean = true;
@ViewChild('myChartDHT', { static: false }) dhtChartElement!: ElementRef<HTMLCanvasElement>;
@ViewChild('myChartNPK1', { static: false }) npk1ChartElement!: ElementRef<HTMLCanvasElement>;
@ViewChild('myChartNPK2', { static: false }) npk2ChartElement!: ElementRef<HTMLCanvasElement>;
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,10 +89,202 @@ 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[] } {
@ -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`) : [];
}
}

View File

@ -10,25 +10,44 @@
<form (ngSubmit)="onSubmit()">
<div class="form-floating mb-3">
<input type="text" class="form-control" [(ngModel)]="fullname" name="fullname" id="floatingFullname" placeholder="Full Name" required>
<input type="text" class="form-control" [(ngModel)]="fullname" name="fullname" id="floatingFullname" placeholder="Full Name" required autocomplete="off">
<label for="floatingFullname">Full Name</label>
</div>
<div class="form-floating mb-3">
<input type="text" class="form-control" [(ngModel)]="username" name="username" id="floatingUsername" placeholder="Username" required>
<input type="text" class="form-control" [(ngModel)]="username" name="username" id="floatingUsername" placeholder="Username" required autocomplete="off">
<label for="floatingUsername">Username</label>
</div>
<div class="form-floating mb-3">
<input type="email" class="form-control" [(ngModel)]="email" name="email" id="floatingInput" placeholder="name@example.com" required>
<input type="email" class="form-control" [(ngModel)]="email" name="email" id="floatingInput" placeholder="name@example.com" required autocomplete="off">
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating mb-3">
<input type="password" class="form-control" [(ngModel)]="password" name="password" id="floatingPassword" placeholder="Password" required>
<input [type]="passwordVisible ? 'text' : 'password'"
class="form-control"
[(ngModel)]="password"
name="password"
id="floatingPassword"
placeholder="Password"
required autocomplete="off">
<label for="floatingPassword">Password</label>
<button type="button"
class="btn btn-link position-absolute end-0 me-3"
(click)="togglePasswordVisibility()">
<i class="fa" [ngClass]="passwordVisible ? 'fa-eye' : 'fa-eye-slash'"></i>
</button>
</div>
<div class="d-grid">
<button class="btn btn-lg color-btn btn-login text-uppercase fw-bold mb-2" type="submit">Sign Up</button>
<button class="btn btn-lg color-btn btn-login text-uppercase fw-bold mb-2"
type="submit"
[disabled]="loading">
<span *ngIf="!loading">Sign up</span>
<span *ngIf="loading">
<i class="fa fa-spinner fa-spin"></i>
</span>
</button>
<div class="text-center">
<a class="small forgot" routerLink= '/auth'>Log In</a>
<a class="small forgot" routerLink='/auth'>Sign In here</a>
</div>
</div>
</form>

View File

@ -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;
}
}
}

View File

@ -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);
}
);

View File

@ -17,6 +17,7 @@
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<script src="https://code.highcharts.com/modules/export-data.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<app-root></app-root>