dev smartfarming #1

Merged
agrilink merged 53 commits from development into main 2024-12-30 05:53:19 +00:00
27 changed files with 515 additions and 248 deletions
Showing only changes of commit 879151ea6c - Show all commits

View File

@ -34,7 +34,8 @@
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css", "node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/bootstrap-icons/font/bootstrap-icons.css" "node_modules/bootstrap-icons/font/bootstrap-icons.css",
"node_modules/ngx-toastr/toastr.css"
], ],
"scripts": [ "scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
@ -97,7 +98,8 @@
"styles": [ "styles": [
"src/styles.scss", "src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css", "node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/bootstrap-icons/font/bootstrap-icons.css" "node_modules/bootstrap-icons/font/bootstrap-icons.css",
"node_modules/ngx-toastr/toastr.css"
], ],
"scripts": [ "scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"

View File

@ -8,7 +8,7 @@
"name": "agrilink-vocpro", "name": "agrilink-vocpro",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@angular/animations": "^18.2.0", "@angular/animations": "^18.2.8",
"@angular/common": "^18.2.0", "@angular/common": "^18.2.0",
"@angular/compiler": "^18.2.0", "@angular/compiler": "^18.2.0",
"@angular/core": "^18.2.0", "@angular/core": "^18.2.0",
@ -20,6 +20,8 @@
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"chart.js": "^4.4.4", "chart.js": "^4.4.4",
"highcharts-angular": "^4.0.1", "highcharts-angular": "^4.0.1",
"jwt-decode": "^4.0.0",
"ngx-toastr": "^19.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.14.10" "zone.js": "~0.14.10"
@ -272,9 +274,9 @@
} }
}, },
"node_modules/@angular/animations": { "node_modules/@angular/animations": {
"version": "18.2.4", "version": "18.2.8",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.4.tgz", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.8.tgz",
"integrity": "sha512-ajjXpLD+SyxbHnmhj2ZvYpXneviOjcBgU9L2UX4OVS0jVWxCNHLhJrcMqtqFHA6U5fPnhPNR9cmnt6tmqri0rA==", "integrity": "sha512-dMSn2hg70siv3lhP+vqhMbgc923xw6XBUvnpCPEzhZqFHvPXfh/LubmsD5RtqHmjWebXtgVcgS+zg3Gq3jB2lg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@ -283,7 +285,7 @@
"node": "^18.19.1 || ^20.11.1 || >=22.0.0" "node": "^18.19.1 || ^20.11.1 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/core": "18.2.4" "@angular/core": "18.2.8"
} }
}, },
"node_modules/@angular/build": { "node_modules/@angular/build": {
@ -3862,17 +3864,6 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.20.0", "version": "4.20.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz",
@ -7632,13 +7623,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/highcharts": {
"version": "11.4.8",
"resolved": "https://registry.npmjs.org/highcharts/-/highcharts-11.4.8.tgz",
"integrity": "sha512-5Tke9LuzZszC4osaFisxLIcw7xgNGz4Sy3Jc9pRMV+ydm6sYqsPYdU8ELOgpzGNrbrRNDRBtveoR5xS3SzneEA==",
"license": "https://www.highcharts.com/license",
"peer": true
},
"node_modules/highcharts-angular": { "node_modules/highcharts-angular": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/highcharts-angular/-/highcharts-angular-4.0.1.tgz", "resolved": "https://registry.npmjs.org/highcharts-angular/-/highcharts-angular-4.0.1.tgz",
@ -8633,6 +8617,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/jwt-decode": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/karma": { "node_modules/karma": {
"version": "6.4.4", "version": "6.4.4",
"resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz",
@ -10063,6 +10056,20 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/ngx-toastr": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz",
"integrity": "sha512-6pTnktwwWD+kx342wuMOWB4+bkyX9221pAgGz3SHOJH0/MI9erLucS8PeeJDFwbUYyh75nQ6AzVtolgHxi52dQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">=16.0.0-0",
"@angular/core": ">=16.0.0-0",
"@angular/platform-browser": ">=16.0.0-0"
}
},
"node_modules/nice-napi": { "node_modules/nice-napi": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",

View File

@ -10,7 +10,7 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^18.2.0", "@angular/animations": "^18.2.8",
"@angular/common": "^18.2.0", "@angular/common": "^18.2.0",
"@angular/compiler": "^18.2.0", "@angular/compiler": "^18.2.0",
"@angular/core": "^18.2.0", "@angular/core": "^18.2.0",
@ -22,6 +22,8 @@
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"chart.js": "^4.4.4", "chart.js": "^4.4.4",
"highcharts-angular": "^4.0.1", "highcharts-angular": "^4.0.1",
"jwt-decode": "^4.0.0",
"ngx-toastr": "^19.0.0",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0", "tslib": "^2.3.0",
"zone.js": "~0.14.10" "zone.js": "~0.14.10"

View File

@ -1,13 +1,16 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { routes } from './app.routes'; import { routes } from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes), provideRouter(routes),
provideHttpClient() provideHttpClient(),
provideAnimations(),
importProvidersFrom(ToastrModule.forRoot()),
] ]
}; };

View File

@ -3,28 +3,36 @@ import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { LayoutsComponent } from './pages/dashboard/layouts/layouts.component'; import { LayoutsComponent } from './pages/dashboard/layouts/layouts.component';
import { GraphComponent } from './pages/dashboard/page/graph/graph.component'; import { GraphComponent } from './pages/dashboard/page/graph/graph.component';
import { AuthComponent } from './pages/auth/auth.component'; import { AuthComponent } from './pages/auth/auth.component';
import { AuthGuard } from './cores/guard/guards/auth.guard';
import { RegisterComponent } from './pages/register/register.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
path: '', path: '',
redirectTo:'dashboard', redirectTo: 'auth',
pathMatch:'full' pathMatch: 'full'
}, },
{ {
path: 'auth', path: 'auth',
component: AuthComponent component: AuthComponent
}, },
{
path: 'register',
component: RegisterComponent
},
{ {
path: '', path: '',
component: LayoutsComponent, component: LayoutsComponent,
children:[ children: [
{ {
path: 'dashboard', path: 'dashboard',
component: DashboardComponent, component: DashboardComponent,
canActivate: [AuthGuard]
}, },
{ {
path: 'graph', path: 'graph',
component: GraphComponent component: GraphComponent,
canActivate: [AuthGuard]
}, },
] ]
} }

View File

@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { CanActivateFn } from '@angular/router';
import { authGuard } from './auth.guard';
describe('authGuard', () => {
const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authGuard(...guardParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeGuard).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private router: Router) {}
canActivate(): boolean {
const token = localStorage.getItem('accessToken');
if (token) {
return true;
} else {
this.router.navigate(['auth']);
return false;
}
}
}

View File

@ -0,0 +1,14 @@
export interface LoginData {
email: string;
password: string;
rememberMe?: boolean;
}
export interface RegistrationData {
username: string;
pwd: string;
email: string;
google_id: string;
fullname: string;
avatar?: string;
}

View File

@ -18,13 +18,14 @@ export interface ParameterSensor {
export interface ApiResponse { export interface ApiResponse {
data: { data: {
dht?: ParameterSensor[]; dht: Array<any>;
npk1?: ParameterSensor[]; npk1: Array<any>;
npk2?: ParameterSensor[]; npk2: Array<any>;
}; };
statusCode: number; statusCode: number;
message: string; message: string;
} }
export interface DHTSensor { export interface DHTSensor {
lightIntensity: number; lightIntensity: number;

View File

@ -1,29 +1,11 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ApiResponse } from '../interface/sensor-data';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class ApiService { export class ApiService {
private baseUrl = 'https://jx027dj4-3333.asse.devtunnels.ms/api/sensor/'; protected baseUrl = 'https://jx027dj4-3333.asse.devtunnels.ms/';
constructor(private http: HttpClient) {} constructor(protected http: HttpClient) {}
getSensorData(sensor: string, metric: string, startDate: string, timeRange: string): Observable<ApiResponse> {
const url = `${this.baseUrl}getData`;
const params = new HttpParams()
.set('range[start]', startDate)
.set('range[time_range]', timeRange)
.set('sensor', sensor)
.set('metric', metric);
return this.http.get<ApiResponse>(url, { params });
}
getLatestData(): Observable<ApiResponse> {
const url = `${this.baseUrl}getLatest`;
return this.http.get<ApiResponse>(url);
}
} }

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let service: AuthService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AuthService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { ApiService } from './api.service';
import { LoginData } from '../interface/auth';
import {jwtDecode}from 'jwt-decode';
@Injectable({
providedIn: 'root'
})
export class AuthService extends ApiService {
private authUrl = `${this.baseUrl}auth/login`;
private logoutUrl = `${this.baseUrl}auth/logout`;
private registerUrl = `${this.baseUrl}auth/register`;
constructor(http: HttpClient) {
super(http);
}
login(data: LoginData): Observable<any> {
const headers = new HttpHeaders({
Authorization: 'Basic ' + btoa(`${data.email}:${data.password}`)
});
const formData = new FormData();
formData.append('remember_me', data.rememberMe ? 'true' : 'false');
return this.http.post<any>(this.authUrl, formData, { headers }).pipe(
tap(response => {
const accessToken = response.data.token;
this.saveTokens(accessToken);
const jwtToken = response.data.jwtToken;
const decodedToken: any = jwtDecode(jwtToken);
this.saveUserDataToStorage(decodedToken.user.fullname, decodedToken.user.avatar);
})
);
}
saveTokens(token: string) {
localStorage.setItem('accessToken', token);
}
logout(): Observable<any> {
const token = localStorage.getItem('accessToken');
const headers = new HttpHeaders({
Authorization: `Bearer ${token}`
});
return this.http.post<any>(this.logoutUrl, {}, { headers }).pipe(
tap(() => {
localStorage.removeItem('accessToken');
this.clearUserDataFromStorage();
})
);
}
register(data: any): Observable<any> {
const headers = new HttpHeaders({});
return this.http.post<any>(this.registerUrl, data, { headers }).pipe(
// tap(response => {
// console.log('Registration response:', response);
// })
);
}
getUserFullName(): string | null {
return localStorage.getItem('userFullName');
}
getAvatar(): string | null {
return localStorage.getItem('avatar');
}
private saveUserDataToStorage(fullName: string | null, avatar: string | null) {
localStorage.setItem('userFullName', fullName || '');
localStorage.setItem('avatar', avatar || '');
}
private clearUserDataFromStorage() {
localStorage.removeItem('userFullName');
localStorage.removeItem('avatar');
}
}

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SensorService } from './sensor.service';
describe('SensorService', () => {
let service: SensorService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SensorService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,55 @@
import { Injectable } from '@angular/core';
import { HttpParams, HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { catchError, Observable, throwError, tap } from 'rxjs';
import { ApiService } from './api.service';
import { ApiResponse } from '../interface/sensor-data';
@Injectable({
providedIn: 'root'
})
export class SensorService extends ApiService {
constructor(http: HttpClient) {
super(http);
}
getSensorData(sensor: string, metric: string, startEnd: string, timeRange: string): Observable<ApiResponse> {
const url = `${this.baseUrl}api/sensor/getData`;
const params = new HttpParams()
.set('range[end]', startEnd)
.set('range[time_range]', timeRange)
.set('sensor', sensor)
.set('metric', metric);
const token = localStorage.getItem('accessToken');
const headers = new HttpHeaders({
Authorization: 'Bearer ' + token,
});
return this.http.get<ApiResponse>(url, { params, headers }).pipe(
catchError((error: HttpErrorResponse) => {
if (error.error instanceof ErrorEvent) {
console.error('An error occurred:', error.error.message);
} else {
console.error(`Backend returned code ${error.status}, body was: ${error.error}`);
}
return throwError('Something went wrong; please try again later.');
})
);
}
getLatestData(): Observable<ApiResponse> {
const url = `${this.baseUrl}api/sensor/getLatest`;
const headers = new HttpHeaders({
Authorization: 'Bearer ' + localStorage.getItem('accessToken'),
});
return this.http.get<any>(url, { headers }).pipe(
catchError(error => {
console.error('API Error:', error);
return throwError(error);
})
);
}
}

View File

@ -1,43 +1,40 @@
<div class="container-fluid ps-md-0"> <div class="container-fluid ps-md-0">
<div class="row g-0"> <div class="row g-0">
<div class="d-none d-md-flex col-md-4 col-lg-6 bg-image"></div> <div class="d-none d-md-flex col-md-4 col-lg-6 bg-image"></div>
<div class="col-md-8 col-lg-6"> <div class="col-md-8 col-lg-6">
<div class="login d-flex align-items-center py-5"> <div class="login d-flex align-items-center py-5">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-9 col-lg-8 mx-auto"> <div class="col-md-9 col-lg-8 mx-auto">
<h3 class="login-heading mb-4">Welcome back!</h3> <h3 class="login-heading mb-4">Welcome back!</h3>
<!-- Sign In Form -->
<form>
<div class="form-floating mb-3">
<input type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating mb-3">
<input type="password" class="form-control" id="floatingPassword" placeholder="Password">
<label for="floatingPassword">Password</label>
</div>
<div class="form-check mb-3"> <form (ngSubmit)="onSubmit()">
<input class="form-check-input chechkbox" type="checkbox" value="" id="rememberPasswordCheck"> <div class="form-floating mb-3">
<label class="form-check-label" for="rememberPasswordCheck"> <input type="email" class="form-control" [(ngModel)]="email" name="email" id="floatingInput" placeholder="name@example.com" required>
Remember password <label for="floatingInput">Email address</label>
</label> </div>
</div> <div class="form-floating mb-3">
<input type="password" class="form-control" [(ngModel)]="password" name="password" id="floatingPassword" placeholder="Password" required>
<label for="floatingPassword">Password</label>
</div>
<div class="d-grid"> <div class="form-check mb-3">
<a class="btn btn-lg color-btn btn-login text-uppercase fw-bold mb-2" routerLink=''>Sign in</a> <input class="form-check-input" type="checkbox" [(ngModel)]="rememberMe" name="rememberMe" id="rememberPasswordCheck">
<div class="text-center"> <label class="form-check-label" for="rememberPasswordCheck">Remember Me? </label>
<a class="small forgot" href="#">Forgot password?</a> </div>
</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>
<div class="text-center">
<a class="small forgot" routerLink= '/register'>Register here</a>
</div> </div>
</form> </div>
</div> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>

View File

@ -1,14 +1,40 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterLink } from '@angular/router'; import { Router, RouterModule } from '@angular/router';
import { AuthService } from '../../cores/services/auth.service';
import { FormsModule } from '@angular/forms';
import { LoginData } from '../../cores/interface/auth';
import { ToastrService } from 'ngx-toastr';
@Component({ @Component({
selector: 'app-auth', selector: 'app-auth',
standalone: true, standalone: true,
imports: [RouterLink], imports: [FormsModule, RouterModule],
templateUrl: './auth.component.html', templateUrl: './auth.component.html',
styleUrl: './auth.component.scss' styleUrls: ['./auth.component.scss']
}) })
export class AuthComponent { export class AuthComponent {
email: string = '';
password: string = '';
rememberMe: boolean = false;
constructor(private authService: AuthService, private router: Router, private toastr: ToastrService) {}
onSubmit() {
const loginData: LoginData = {
email: this.email,
password: this.password,
rememberMe: this.rememberMe
};
this.authService.login(loginData).subscribe(
(response) => {
this.authService.saveTokens(response.data.token);
this.router.navigate(['/dashboard']);
this.toastr.success('Login successful');
},
(error) => {
this.toastr.error(error.error.message);
}
);
}
} }

View File

@ -1,17 +1,16 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { SidebarComponent } from './layouts/sidebar/sidebar.component'; import { SidebarComponent } from './layouts/sidebar/sidebar.component';
import { GaugeChartComponent } from './page/gauge-chart/gauge-chart.component';
import { GraphComponent } from './page/graph/graph.component'; import { GraphComponent } from './page/graph/graph.component';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { ApiService } from '../../cores/services/api.service'; import { SensorService } from '../../cores/services/sensor.service';
import { SensorData } from '../../cores/interface/sensor-data'; import { SensorData } from '../../cores/interface/sensor-data';
import { interval } from 'rxjs'; import { interval } from 'rxjs';
@Component({ @Component({
selector: 'app-dashboard', selector: 'app-dashboard',
standalone: true, standalone: true,
imports: [RouterOutlet, SidebarComponent, GaugeChartComponent, CommonModule, GraphComponent], imports: [RouterOutlet, SidebarComponent, CommonModule, GraphComponent],
templateUrl: './dashboard.component.html', templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'] styleUrls: ['./dashboard.component.scss']
}) })
@ -26,7 +25,7 @@ export class DashboardComponent implements OnInit {
npk2: { temperature: 0, moisture: 0, conductivity: 0, ph: 0, nitrogen: 0, phosphorus: 0, potassium: 0 } npk2: { temperature: 0, moisture: 0, conductivity: 0, ph: 0, nitrogen: 0, phosphorus: 0, potassium: 0 }
}; };
constructor(private apiService: ApiService) {} constructor(private apiService: SensorService) {}
ngOnInit(): void { ngOnInit(): void {
this.selectedButton = 'dht'; this.selectedButton = 'dht';
@ -58,7 +57,7 @@ export class DashboardComponent implements OnInit {
minute: '2-digit', minute: '2-digit',
second: '2-digit' second: '2-digit'
}; };
this.latestUpdate = now.toLocaleString('en-GB', options); // Update waktu ke format yang sesuai this.latestUpdate = now.toLocaleString('en-GB', options);
} }
@ -105,7 +104,7 @@ export class DashboardComponent implements OnInit {
}; };
} }
this.updateLatestTime(); // Update waktu setelah data diambil this.updateLatestTime();
this.isLoaded = false; this.isLoaded = false;
}, },
(error) => { (error) => {

View File

@ -13,13 +13,15 @@
</li> --> </li> -->
</ul> </ul>
<hr> <hr>
<div class="dropdown pb-4"> <div class="dropdown pb-4">
<a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false"> <a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
<img src="https://github.com/mdo.png" alt="hugenerd" width="30" height="30" class="rounded-circle"> <img [src]="avatar || 'https://github.com/mdo.png'" alt="User Avatar" width="30" height="30" class="rounded-circle"> <!-- Use avatar or a default image -->
<span class="d-none d-sm-inline mx-1">Tiffany</span> <span class="d-none d-sm-inline mx-1">{{ fullName }}</span>
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1"> <ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1">
<li><a class="dropdown-item" routerLink='/auth'>Sign out</a></li> <li><a class="dropdown-item" (click)="onLogout()">Sign out</a></li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -1,13 +1,32 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RouterLink } from '@angular/router'; import { AuthService } from '../../../../cores/services/auth.service';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
@Component({ @Component({
selector: 'app-sidebar', selector: 'app-sidebar',
standalone: true, standalone: true,
imports: [RouterLink],
templateUrl: './sidebar.component.html', templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.scss' styleUrls: ['./sidebar.component.scss']
}) })
export class SidebarComponent { export class SidebarComponent {
fullName: string | null = null;
avatar: string | null = null;
constructor(private authService: AuthService, private router: Router, private toast: ToastrService) {
this.fullName = this.authService.getUserFullName();
this.avatar = this.authService.getAvatar();
}
onLogout(): void {
this.authService.logout().subscribe(
() => {
this.router.navigate(['/auth']);
this.toast.success('Logout successful');
},
(error: any) => {
this.toast.error(error.error.message);
}
);
}
} }

View File

@ -1,10 +0,0 @@
<div *ngIf="isLoading" class="loading">
Loading...
</div>
<highcharts-chart
*ngIf="!isLoading"
class="gauge-chart"
[Highcharts]="Highcharts"
[options]="chartOptions"
>
</highcharts-chart>

View File

@ -1,11 +0,0 @@
.gauge-chart{
width: 50%;
display: inline-block;
}
.loading{
font-size: 18px;
text-align: center;
color: #888;
}

View File

@ -1,99 +0,0 @@
import { Component, Input } from '@angular/core';
import * as Highcharts from 'highcharts';
import HighchartsMore from 'highcharts/highcharts-more';
import HC_solidGauge from 'highcharts/modules/solid-gauge';
import { HighchartsChartModule } from 'highcharts-angular';
import { CommonModule } from '@angular/common';
HighchartsMore(Highcharts);
HC_solidGauge(Highcharts);
@Component({
selector: 'app-gauge-chart',
standalone: true,
imports: [HighchartsChartModule, CommonModule],
templateUrl: './gauge-chart.component.html',
styleUrls: ['./gauge-chart.component.scss']
})
export class GaugeChartComponent {
@Input() gaugeTitle: string = 'Gauge';
@Input() gaugeData: number = 0;
@Input() colorStops: [number, string][] = [];
@Input() maxValue: number = 100;
@Input() satuanData: string = '';
isLoading: boolean = true;
Highcharts: typeof Highcharts = Highcharts;
chartOptions: Highcharts.Options = {};
constructor() {}
ngOnChanges() {
this.chartOptions = {
chart: {
type: 'solidgauge',
height: '100%',
backgroundColor: 'transparent'
},
credits: {
enabled: false
},
title: {
text: this.gaugeTitle,
style: {
fontSize: '15px',
fontFamily: 'Onest, sans-serif',
}
},
pane: {
startAngle: -180,
endAngle: 180,
background: [{
innerRadius: '50%',
outerRadius: '110%',
shape: 'arc'
}]
},
yAxis: {
min: 0,
max: this.maxValue,
stops: this.colorStops.length ? this.colorStops : [
[0.1, '#55BF3B'],
[0.5, '#DDDF0D'],
[0.9, '#DF5353']
],
lineWidth: 0,
tickWidth: 0,
tickAmount: 2,
labels: {
enabled: false
}
},
series: [{
name: this.gaugeTitle,
data: [this.gaugeData],
tooltip: {
valueSuffix: this.satuanData
},
dataLabels: {
enabled: true,
y: -10,
x: 0,
borderColor: 'transparent',
borderWidth: 0,
style: {
fontSize: '15px',
fontFamily: 'Onest, sans-serif',
}
}
}] as Highcharts.SeriesSolidgaugeOptions[]
};
this.isLoading = false;
}
}

View File

@ -1,6 +1,6 @@
import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core'; import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js';
import { ApiService } from '../../../../cores/services/api.service'; 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 { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@ -56,7 +56,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
labelsHourly: string[] = []; labelsHourly: string[] = [];
private resizeListener!: () => void; private resizeListener!: () => void;
constructor(private sensorService: ApiService) {} constructor(private sensorService: SensorService) {}
ngOnInit(): void { ngOnInit(): void {
this.selectedSensor = 'dht'; this.selectedSensor = 'dht';
@ -189,10 +189,10 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
const month = String(today.getMonth() + 1).padStart(2, '0'); const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0'); const day = String(today.getDate()).padStart(2, '0');
const startDate = `${year}-${month}-${day}`; const startEnd= `${year}-${month}-${day}`;
const timeRange = 'HOURLY'; const timeRange = 'HOURLY';
this.sensorService.getSensorData(this.selectedSensor, this.selectedParameter, startDate, timeRange).subscribe( this.sensorService.getSensorData(this.selectedSensor, this.selectedParameter, startEnd, timeRange).subscribe(
(response: ApiResponse) => { (response: ApiResponse) => {
if (response.statusCode === 200) { if (response.statusCode === 200) {
const { data, labels } = this.getDataFromResponse(response, this.selectedSensor, this.selectedParameter); const { data, labels } = this.getDataFromResponse(response, this.selectedSensor, this.selectedParameter);

View File

@ -0,0 +1,41 @@
<div class="container-fluid ps-md-0">
<div class="row g-0">
<div class="d-none d-md-flex col-md-4 col-lg-6 bg-image"></div>
<div class="col-md-8 col-lg-6">
<div class="login d-flex align-items-center py-5">
<div class="container">
<div class="row">
<div class="col-md-9 col-lg-8 mx-auto">
<h3 class="login-heading mb-4">Create an Account!</h3>
<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>
<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>
<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>
<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>
<label for="floatingPassword">Password</label>
</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>
<div class="text-center">
<a class="small forgot" routerLink= '/auth'>Log In</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,29 @@
.login {
min-height: 100vh;
font-family: "Onest", sans-serif;
}
.bg-image {
background-image: url('../../../assets/images/auth.png');
background-size: cover;
background-position: left;
}
.login-heading {
font-weight: 300;
}
.btn-login {
font-size: 0.9rem;
letter-spacing: 0.05rem;
padding: 0.75rem 1rem;
}
.color-btn{
background-color: #16423C;
color: white;
}
.forgot{
color: #16423C;
}

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GaugeChartComponent } from './gauge-chart.component'; import { RegisterComponent } from './register.component';
describe('GaugeChartComponent', () => { describe('RegisterComponent', () => {
let component: GaugeChartComponent; let component: RegisterComponent;
let fixture: ComponentFixture<GaugeChartComponent>; let fixture: ComponentFixture<RegisterComponent>;
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [GaugeChartComponent] imports: [RegisterComponent]
}) })
.compileComponents(); .compileComponents();
fixture = TestBed.createComponent(GaugeChartComponent); fixture = TestBed.createComponent(RegisterComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@ -0,0 +1,44 @@
import { Component } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { AuthService } from '../../cores/services/auth.service';
import { FormsModule } from '@angular/forms';
import { RegistrationData } from '../../cores/interface/auth';
import { ToastrService } from 'ngx-toastr';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-register',
standalone: true,
imports: [FormsModule, RouterModule, CommonModule],
templateUrl: './register.component.html',
styleUrls: ['./register.component.scss']
})
export class RegisterComponent {
username: string = '';
password: string = '';
email: string = '';
fullname: string = '';
constructor(private authService: AuthService, private router: Router, private toast: ToastrService) {}
onSubmit() {
const registrationData: RegistrationData = {
username: this.username,
pwd: this.password,
email: this.email,
google_id: '1',
fullname: this.fullname,
avatar: ''
};
this.authService.register(registrationData).subscribe(
(response) => {
this.toast.success('Registration successful');
this.router.navigate(['/auth']);
},
(error) => {
this.toast.error(error.error.message);
}
);
}
}