dev smartfarming #1
985
agrilink_vocpro/package-lock.json
generated
985
agrilink_vocpro/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
|
@ -10,17 +10,19 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.2.8",
|
||||
"@angular/animations": "^18.2.4",
|
||||
"@angular/common": "^18.2.0",
|
||||
"@angular/compiler": "^18.2.0",
|
||||
"@angular/core": "^18.2.0",
|
||||
"@angular/forms": "^18.2.0",
|
||||
"@angular/material": "^17.3.6",
|
||||
"@angular/platform-browser": "^18.2.0",
|
||||
"@angular/platform-browser-dynamic": "^18.2.0",
|
||||
"@angular/router": "^18.2.0",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"chart.js": "^4.4.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"highcharts-angular": "^4.0.1",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
|
|
@ -32,6 +34,7 @@
|
|||
"@angular-devkit/build-angular": "^18.2.4",
|
||||
"@angular/cli": "^18.2.4",
|
||||
"@angular/compiler-cli": "^18.2.0",
|
||||
"@types/date-fns": "^2.5.3",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.2.0",
|
||||
"karma": "~6.4.0",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { AuthComponent } from './pages/auth/auth.component';
|
|||
import { AuthGuard } from './cores/guards/auth.guard';
|
||||
import { RegisterComponent } from './pages/register/register.component';
|
||||
import { HistorygraphComponent } from './pages/dashboard/page/historygraph/historygraph.component';
|
||||
import { ActualgraphComponent } from './pages/dashboard/page/actualgraph/actualgraph.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
|
|
@ -33,6 +34,11 @@ export const routes: Routes = [
|
|||
path: 'historygraph',
|
||||
component: HistorygraphComponent,
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'actualgraph',
|
||||
component: ActualgraphComponent,
|
||||
canActivate: [AuthGuard]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export interface ParameterSensor {
|
||||
hour: number;
|
||||
day: number;
|
||||
date: number;
|
||||
|
||||
//for DHT sensor
|
||||
vicitemperature?: number;
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@
|
|||
<h1 class="title">{{ greeting }}</h1>
|
||||
<h3 class="description">Welcome back to your management system</h3>
|
||||
</div>
|
||||
<div>
|
||||
<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 === 'npk1'}" (click)="selectSensor('npk1')">NPK 1</button>
|
||||
|
|
@ -158,10 +155,15 @@
|
|||
Katup Nutrisi
|
||||
</ng-container>
|
||||
<ng-template #checkRelay>
|
||||
<ng-container *ngIf="relay.number === 2; else defaultRelay">
|
||||
<ng-container *ngIf="relay.number === 2; else checkRelay2">
|
||||
Katup Air
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-template #checkRelay2>
|
||||
<ng-container *ngIf="relay.number === 3; else defaultRelay">
|
||||
Pompa Air
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-template #defaultRelay>
|
||||
Relay {{ relay.number }}
|
||||
</ng-template>
|
||||
|
|
@ -170,9 +172,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="graph">
|
||||
<div class="title-graph">Monitoring Graphs</div>
|
||||
|
|
|
|||
|
|
@ -16,13 +16,7 @@
|
|||
.description {
|
||||
color: #49473C;
|
||||
font-size: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.update{
|
||||
color: #49473C;
|
||||
font-size: 15px;
|
||||
margin-top: 18px;
|
||||
margin: 10px 0px 15px 0px;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
|
|
@ -102,6 +96,10 @@
|
|||
margin-top: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.relay-container{
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 344px) {
|
||||
|
|
@ -126,6 +124,10 @@
|
|||
margin-top: 20px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.relay-container{
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ export class DashboardComponent implements OnInit {
|
|||
|
||||
ngOnInit(): void {
|
||||
this.selectedButton = 'dht';
|
||||
this.startClock();
|
||||
this.updateGreeting();
|
||||
this.loadData();
|
||||
}
|
||||
|
|
@ -44,26 +43,6 @@ export class DashboardComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
startClock(): void {
|
||||
this.updateLatestTime();
|
||||
|
||||
this.intervalId = setInterval(() => {
|
||||
this.updateLatestTime();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
updateLatestTime(): void {
|
||||
const now = new Date();
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
};
|
||||
this.latestUpdate = now.toLocaleString('en-GB', options);
|
||||
}
|
||||
|
||||
updateGreeting(): void {
|
||||
const hour = new Date().getHours();
|
||||
|
|
@ -131,8 +110,6 @@ export class DashboardComponent implements OnInit {
|
|||
};
|
||||
}
|
||||
}
|
||||
|
||||
this.updateLatestTime();
|
||||
this.isLoaded = false;
|
||||
},
|
||||
(error) => {
|
||||
|
|
@ -157,10 +134,10 @@ export class DashboardComponent implements OnInit {
|
|||
created_at: relay.created_at,
|
||||
current_status: relay.current_status
|
||||
}));
|
||||
this.relayStatus.sort((a, b) => a.number - b.number);
|
||||
this.noData = false; // Data available
|
||||
this.relayStatus.sort((a, b) => b.number - a.number);
|
||||
|
||||
this.noData = false;
|
||||
} else {
|
||||
// Throw an error if relay data is empty
|
||||
this.handleError({ message: 'No available relay data.' });
|
||||
this.noData = true;
|
||||
}
|
||||
|
|
@ -172,9 +149,8 @@ export class DashboardComponent implements OnInit {
|
|||
this.hasError = true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
handleError(error: any): void {
|
||||
if (this.selectedButton === 'dht') {
|
||||
this.toast.error('Error fetching DHT sensor data. Please try again.', 'Error', { timeOut: 3000 });
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@
|
|||
<a routerLink='/historygraph' data-bs-toggle="collapse" class="nav-link px-0 align-middle">
|
||||
<i class="bi bi-file-earmark-text title"></i> <span class="ms-1 d-none d-sm-inline menu">History Graph</span> </a>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink='/actualgraph' data-bs-toggle="collapse" class="nav-link px-0 align-middle">
|
||||
<i class="bi bi-bar-chart title"></i> <span class="ms-1 d-none d-sm-inline menu">Actual Graph</span> </a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
<div class="container">
|
||||
<div>
|
||||
<h1 class="title">{{ greeting }}</h1>
|
||||
<h3 class="description">Compare your actual data with the standard data</h3>
|
||||
</div>
|
||||
<div>
|
||||
<button (click)="loadData('npk1')" [ngClass]="{'active-button': selectedButton === 'npk1'}">NPK 1</button>
|
||||
<button (click)="loadData('npk2')" [ngClass]="{'active-button': selectedButton === 'npk2'}">NPK 2</button>
|
||||
</div>
|
||||
|
||||
<div class="graph loading">
|
||||
<div class="title-graph" *ngIf="selectedButton === 'npk1'">Actual Graph Sensor NPK 1</div>
|
||||
<div class="title-graph" *ngIf="selectedButton === 'npk2'">Actual Graph Sensor NPK 2</div>
|
||||
<div class="graph-content">
|
||||
<canvas *ngIf="!isLoading && !isNoData" #chartCanvas></canvas>
|
||||
<i *ngIf="isLoading" class="fa fa-spinner fa-spin spinner"></i>
|
||||
<p *ngIf="isNoData" class="no-data">No available data</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
.container {
|
||||
font-family: "Onest", sans-serif;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #49473C;
|
||||
font-size: 30px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
color: #49473C;
|
||||
font-size: 15px;
|
||||
margin: 10px 0px 15px 0px;
|
||||
}
|
||||
|
||||
.graph {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #E5E5E5;
|
||||
padding: 20px 20px 0px 20px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.title-graph {
|
||||
color: #49473C;
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.graph-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
font-size: 30px;
|
||||
color: #16423C;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.graph {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.title-graph {
|
||||
margin: auto;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 344px) {
|
||||
button {
|
||||
color: #49473C;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title-graph {
|
||||
font-size: 15px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 5px 10px;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.active-button {
|
||||
background-color: #cad849;
|
||||
color: white;
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ActualgraphComponent } from './actualgraph.component';
|
||||
|
||||
describe('ActualgraphComponent', () => {
|
||||
let component: ActualgraphComponent;
|
||||
let fixture: ComponentFixture<ActualgraphComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ActualgraphComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ActualgraphComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,201 @@
|
|||
import { Component, OnInit, ViewChild, ElementRef, ChangeDetectorRef } 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 { CommonModule } from '@angular/common';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
@Component({
|
||||
selector: 'app-actualgraph',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './actualgraph.component.html',
|
||||
styleUrls: ['./actualgraph.component.scss']
|
||||
})
|
||||
export class ActualgraphComponent implements OnInit {
|
||||
@ViewChild('chartCanvas', { static: false }) chartCanvas?: ElementRef<HTMLCanvasElement>;
|
||||
private chart: Chart | undefined;
|
||||
selectedButton: string = 'npk1';
|
||||
isLoading: boolean = true;
|
||||
isNoData: boolean = false;
|
||||
greeting = '';
|
||||
|
||||
constructor(
|
||||
private sensorService: SensorService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateGreeting();
|
||||
this.loadData('npk1');
|
||||
}
|
||||
|
||||
loadData(sensorType: string): void {
|
||||
console.log('Loading data for sensor:', sensorType);
|
||||
this.selectedButton = sensorType;
|
||||
this.isLoading = true;
|
||||
this.isNoData = false;
|
||||
|
||||
this.sensorService.getLatestData().subscribe(
|
||||
(response: ApiResponse) => {
|
||||
console.log('Data received:', response);
|
||||
if (response.statusCode === 200) {
|
||||
const selectedData = (response.data as { [key: string]: any[] })[sensorType];
|
||||
|
||||
if (selectedData && selectedData.length > 0) {
|
||||
const npkData: ParameterSensor = selectedData[0];
|
||||
|
||||
if (sensorType === 'npk1' || sensorType === 'npk2') {
|
||||
const actualData = [
|
||||
{ x: npkData.soilnitrogen ?? 0, y: 0 },
|
||||
{ x: npkData.soilphosphorus ?? 0, y: 1 },
|
||||
{ x: npkData.soilpotassium ?? 0, y: 2 }
|
||||
];
|
||||
|
||||
this.chartData.datasets[0].data = actualData;
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
|
||||
setTimeout(() => {
|
||||
this.initializeChart();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.isNoData = true;
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
} else {
|
||||
console.error('Error loading data: ', response.message);
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error loading data:', error);
|
||||
this.isLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize Chart
|
||||
private initializeChart(): void {
|
||||
if (this.chartCanvas?.nativeElement) {
|
||||
console.log('Initializing chart...');
|
||||
const ctx = this.chartCanvas.nativeElement.getContext('2d');
|
||||
if (ctx) {
|
||||
this.chart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: this.chartData,
|
||||
options: this.chartOptions
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn('Chart canvas is not available');
|
||||
}
|
||||
}
|
||||
|
||||
updateGreeting(): void {
|
||||
const hour = new Date().getHours();
|
||||
this.greeting = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening';
|
||||
}
|
||||
|
||||
public chartData = {
|
||||
labels: ['Nitrogen', 'Phosphor', 'Kalium'],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Actual Data',
|
||||
data: [],
|
||||
backgroundColor: 'rgb(18, 55, 42)',
|
||||
borderColor: 'rgb(18, 55, 42)',
|
||||
borderWidth: 1,
|
||||
type: 'bubble'
|
||||
},
|
||||
{
|
||||
label: 'Start Data Range',
|
||||
data: [
|
||||
{ x: 100, y: 0 },
|
||||
{ x: 90, y: 1 },
|
||||
{ x: 220, y: 2 }
|
||||
],
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
borderWidth: 0,
|
||||
type: 'bar',
|
||||
stack: 'range',
|
||||
},
|
||||
{
|
||||
label: 'Standard Data Range',
|
||||
data: [
|
||||
{ x: 100, y: 0 },
|
||||
{ x: 35, y: 1 },
|
||||
{ x: 200, y: 2 }
|
||||
],
|
||||
backgroundColor: 'rgb(212, 231, 197)',
|
||||
borderWidth: 1,
|
||||
type: 'bar',
|
||||
stack: 'range'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
public chartOptions = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
indexAxis: 'y',
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Value (mg/L)',
|
||||
},
|
||||
min: 0,
|
||||
max: 300,
|
||||
type: 'linear',
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Sensor Parameter',
|
||||
},
|
||||
stacked: true,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
title: function (tooltipItem: any) {
|
||||
// Display the label (Nitrogen, Phosphorus, or Kalium)
|
||||
return tooltipItem[0].label;
|
||||
},
|
||||
label: function (tooltipItem: any) {
|
||||
const dataset = tooltipItem.dataset;
|
||||
const dataIndex = tooltipItem.dataIndex;
|
||||
const data = dataset.data[dataIndex];
|
||||
|
||||
let tooltipText = `Actual Value: ${data.x} mg/L`;
|
||||
|
||||
if (dataset.type === 'bar') {
|
||||
const startRangeDataset = tooltipItem.chart.data.datasets.find((ds: any) => ds.label === 'Start Data Range');
|
||||
const standardRangeDataset = tooltipItem.chart.data.datasets.find((ds: any) => ds.label === 'Standard Data Range');
|
||||
|
||||
const startRangeValue = startRangeDataset?.data.find((point: any) => point.y === data.y)?.x;
|
||||
const standardRangeValue = standardRangeDataset?.data.find((point: any) => point.y === data.y)?.x;
|
||||
|
||||
if (startRangeValue !== undefined && standardRangeValue !== undefined) {
|
||||
const minValue = startRangeValue;
|
||||
const maxValue = startRangeValue + standardRangeValue;
|
||||
tooltipText = `(Standard Range: ${minValue} - ${maxValue} mg/L)`;
|
||||
}
|
||||
}
|
||||
|
||||
return tooltipText;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,18 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-template #graphContent>
|
||||
<div class="date-picker">
|
||||
<mat-form-field>
|
||||
<mat-label>Enter a date range</mat-label>
|
||||
<mat-date-range-input [rangePicker]="picker">
|
||||
<input matStartDate placeholder="Start date" [(ngModel)]="startDate" (dateChange)="dateChange()">
|
||||
<input matEndDate placeholder="End date" [(ngModel)]="endDate" (dateChange)="dateChange()">
|
||||
</mat-date-range-input>
|
||||
<mat-hint>MM/DD/YYYY – MM/DD/YYYY</mat-hint>
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-date-range-picker #picker></mat-date-range-picker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="sensor-wrapper">
|
||||
<div class="title">Sensor BHT</div>
|
||||
<ng-container *ngIf="isLoadingDHT; else dhtData">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,26 @@
|
|||
.container-graph {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
height: max-content;
|
||||
|
||||
.date-picker{
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.mat-form-field{
|
||||
font-family: 'Onest', sans-serif;
|
||||
}
|
||||
|
||||
mat-datepicker-toggle, mat-date-range-input{
|
||||
color: #16423C;
|
||||
}
|
||||
|
||||
.mat-form-field.mat-focused .mat-date-range-input {
|
||||
color: #16423C;
|
||||
}
|
||||
|
||||
.sensor-wrapper {
|
||||
position: relative;
|
||||
|
|
@ -62,6 +79,17 @@
|
|||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.date-picker{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
mat-hint{
|
||||
font-size: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 344px) {
|
||||
|
|
@ -70,6 +98,17 @@
|
|||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.date-picker{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
mat-hint{
|
||||
font-size: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
|
@ -88,3 +127,5 @@
|
|||
height: 100%;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,13 @@ import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy, OnC
|
|||
import { Chart, registerables } from 'chart.js';
|
||||
import { SensorService } from '../../../../cores/services/sensor.service';
|
||||
import { ApiResponse, ParameterSensor } from '../../../../cores/interface/sensor-data';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CommonModule, formatDate } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { provideNativeDateAdapter} from '@angular/material/core';
|
||||
import { MatDatepickerModule} from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule} from '@angular/material/form-field';
|
||||
import { format } from 'date-fns';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
|
|
@ -23,7 +28,8 @@ const parameterColors: { [key: string]: string } = {
|
|||
@Component({
|
||||
selector: 'app-graph',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
imports: [CommonModule, FormsModule, MatDatepickerModule, MatFormFieldModule],
|
||||
providers: [provideNativeDateAdapter()],
|
||||
templateUrl: './graph.component.html',
|
||||
styleUrls: ['./graph.component.scss']
|
||||
})
|
||||
|
|
@ -34,6 +40,9 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
@Input() interval: string = 'hourly';
|
||||
selectedInterval: string = 'HOURLY';
|
||||
|
||||
startDate: Date | null = null;
|
||||
endDate: Date | null = null;
|
||||
|
||||
isLoadingDHT: boolean = true;
|
||||
isLoadingNPK1: boolean = true;
|
||||
isLoadingNPK2: boolean = true;
|
||||
|
|
@ -73,14 +82,15 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
selectedNPK2: string = 'npk';
|
||||
|
||||
private resizeListener!: () => void;
|
||||
private updateTimeout: any;
|
||||
|
||||
constructor(private sensorService: SensorService, private cdr: ChangeDetectorRef) {}
|
||||
constructor(private sensorService: SensorService, private toast: ToastrService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.resizeListener = this.onResize.bind(this);
|
||||
window.addEventListener('resize', this.resizeListener);
|
||||
this.updateCharts();
|
||||
}
|
||||
|
||||
|
||||
ngOnChanges(changes: SimpleChanges): void {
|
||||
if (changes['interval'] && changes['interval'].previousValue !== changes['interval'].currentValue) {
|
||||
|
|
@ -92,6 +102,18 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
this.onResize();
|
||||
}
|
||||
|
||||
dateChange(): void {
|
||||
if(!this.startDate || !this.endDate){
|
||||
this.toast.warning('Select both start and end dates');
|
||||
return;
|
||||
} else {
|
||||
clearTimeout(this.updateTimeout);
|
||||
this.updateTimeout = setTimeout(() => {
|
||||
this.updateCharts();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
window.removeEventListener('resize', this.resizeListener);
|
||||
}
|
||||
|
|
@ -105,13 +127,21 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
this.updateCharts();
|
||||
}
|
||||
|
||||
formatDate(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
getDate(): string {
|
||||
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');
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
const hasil = `${year}-${month}-${day}`;
|
||||
return hasil;
|
||||
}
|
||||
|
||||
getDateAgo():string{
|
||||
|
|
@ -125,13 +155,13 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
fetchDHTData(timeRange: string): void {
|
||||
const hEnd = this.getDate();
|
||||
const hStart = this.getDate();
|
||||
const dEnd = this.getDate();
|
||||
const dStart = this.getDateAgo();
|
||||
fetchDHTData(timeRange: string, startDate?: Date, endDate?: Date): void {
|
||||
const hStart = startDate ? this.formatDate(startDate) : this.getDate();
|
||||
const hEnd = endDate ? this.formatDate(endDate) : this.getDate();
|
||||
const dStart= startDate ? this.formatDate(startDate) : this.getDateAgo();
|
||||
const dEnd = endDate ? this.formatDate(endDate) : this.getDate();
|
||||
|
||||
if(timeRange === 'HOURLY'){
|
||||
if (timeRange === 'HOURLY') {
|
||||
this.sensorService.getSensorDataHourly('dht', 'npk', hStart, hEnd, timeRange).subscribe({
|
||||
next: (response) => {
|
||||
this.isLoadingDHT = false;
|
||||
|
|
@ -147,7 +177,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
this.isNoDataDHT = true;
|
||||
}
|
||||
});
|
||||
} else if(timeRange === 'DAILY'){
|
||||
} else if (timeRange === 'DAILY') {
|
||||
this.sensorService.getSensorDataDaily('dht', 'npk', dStart, dEnd, timeRange).subscribe({
|
||||
next: (response) => {
|
||||
this.isLoadingDHT = false;
|
||||
|
|
@ -166,14 +196,14 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
}
|
||||
}
|
||||
|
||||
fetchNPK1Data(timeRange: string): void {
|
||||
const hEnd = this.getDate();
|
||||
const hStart = this.getDate();
|
||||
const dEnd = this.getDate();
|
||||
const dStart = this.getDateAgo();
|
||||
fetchNPK1Data(timeRange: string, startDate?: Date, endDate?: Date): void {
|
||||
const hStart = startDate ? this.formatDate(startDate) : this.getDate();
|
||||
const hEnd = endDate ? this.formatDate(endDate) : this.getDate();
|
||||
const dStart = startDate ? this.formatDate(startDate) : this.getDateAgo();
|
||||
const dEnd= endDate ? this.formatDate(endDate) : this.getDate();
|
||||
|
||||
if(timeRange === 'HOURLY'){
|
||||
this.sensorService.getSensorDataHourly('npk1', this.selectedNPK1, hEnd, hStart, timeRange).subscribe({
|
||||
this.sensorService.getSensorDataHourly('npk1', this.selectedNPK1, hStart, hEnd, timeRange).subscribe({
|
||||
next: (response) => {
|
||||
this.isLoadingNPK1 = false;
|
||||
if (response.statusCode === 200 && response.data.npk1?.length > 0) {
|
||||
|
|
@ -187,7 +217,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
this.isLoadingNPK1 = false;
|
||||
this.isNoDataNPK1 = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
}else if(timeRange === 'DAILY'){
|
||||
this.sensorService.getSensorDataDaily('npk1', this.selectedNPK1, dStart, dEnd, timeRange).subscribe({
|
||||
next: (response) => {
|
||||
|
|
@ -207,17 +237,18 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
}
|
||||
}
|
||||
|
||||
fetchNPK2Data(savedTimeRange: string): void {
|
||||
const hEnd = this.getDate();
|
||||
const hStart = this.getDate();
|
||||
const dEnd = this.getDate();
|
||||
const dStart = this.getDateAgo();
|
||||
fetchNPK2Data(savedTimeRange: string, startDate?: Date, endDate?: Date): void {
|
||||
const hStart = startDate ? this.formatDate(startDate) : this.getDate();
|
||||
const hEnd = endDate ? this.formatDate(endDate) : this.getDate();
|
||||
const dStart = startDate ? this.formatDate(startDate) : this.getDateAgo();
|
||||
const dEnd = endDate ? this.formatDate(endDate) : this.getDate();
|
||||
|
||||
if(savedTimeRange === 'HOURLY'){
|
||||
this.sensorService.getSensorDataHourly('npk2', this.selectedNPK2, hStart, hEnd, savedTimeRange).subscribe({
|
||||
next: (response) => {
|
||||
this.isLoadingNPK2 = false;
|
||||
if (response.statusCode === 200 && response.data.npk2?.length > 0) {
|
||||
console.log('NPK2 Data:', response);
|
||||
this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2);
|
||||
this.isNoDataNPK2 = false;
|
||||
} else {
|
||||
|
|
@ -254,13 +285,20 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
Object.keys(this.charts).forEach(key => {
|
||||
if (this.charts[key]) {
|
||||
this.charts[key]?.destroy();
|
||||
this.charts[key].data.labels = [];
|
||||
this.charts[key].data.datasets = [];
|
||||
this.charts[key] = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
this.fetchDHTData(interval);
|
||||
this.fetchNPK1Data(interval);
|
||||
this.fetchNPK2Data(interval);
|
||||
if (this.startDate && this.endDate) {
|
||||
this.fetchDHTData(interval, this.startDate, this.endDate);
|
||||
this.fetchNPK1Data(interval, this.startDate, this.endDate);
|
||||
this.fetchNPK2Data(interval, this.startDate, this.endDate);
|
||||
} else {
|
||||
this.fetchDHTData(interval);
|
||||
this.fetchNPK1Data(interval);
|
||||
this.fetchNPK2Data(interval);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -369,7 +407,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
mode: 'nearest',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: (tooltipItem) => {
|
||||
label: (tooltipItem: any) => {
|
||||
const paramLabel = tooltipItem.dataset.label;
|
||||
const value = tooltipItem.formattedValue;
|
||||
return `${paramLabel}: ${value}`;
|
||||
|
|
@ -383,7 +421,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
grid: { display: false },
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
callback: (value, index, values) => {
|
||||
callback: (value: string | number, index: number, values: any) => {
|
||||
const labels = this.getLabels(response, sensor);
|
||||
return labels[index] || value;
|
||||
}
|
||||
|
|
@ -406,7 +444,8 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
sensorData.forEach(item => {
|
||||
data.push(item[parameter as keyof ParameterSensor] ?? 0);
|
||||
if(this.interval === 'HOURLY') {
|
||||
labels.push(`${item.hour}.00`);
|
||||
labels.push(item.date);
|
||||
labels.push(item.hour);
|
||||
}
|
||||
else if (this.interval === 'DAILY') {
|
||||
labels.push(item.day);
|
||||
|
|
@ -423,11 +462,14 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
const sensorData = response.data[sensor as keyof typeof response.data];
|
||||
|
||||
return sensorData.map(item => {
|
||||
const formatDate = format(new Date(item.date), 'MMM dd');
|
||||
|
||||
if (this.interval === 'HOURLY' || this.selectedInterval === 'HOURLY') {
|
||||
return `${item.hour}.00`;
|
||||
return `${formatDate}, ${item.hour}.00`;
|
||||
} else if (this.interval === 'DAILY') {
|
||||
const day = item.day;
|
||||
return this.convertDateToDay(day);
|
||||
const day = item.date;
|
||||
const convertDateToDay = this.convertDateToDay(day);
|
||||
return `${convertDateToDay}, ${formatDate}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
|
@ -436,7 +478,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
|
|||
|
||||
convertDateToDay(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thrusday', 'Friday', 'Saturday'];
|
||||
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thr', 'Fri', 'Sat'];
|
||||
return days[date.getDay()];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,6 @@
|
|||
<h1 class="title">{{ greeting }}</h1>
|
||||
<h3 class="description">View your historical data with customizable time ranges</h3>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="update">Latest Update: {{ latestUpdate }}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button (click)="updateInterval('HOURLY')" [class.active-button]="selectedButton === 'hourly'">Hourly</button>
|
||||
<button (click)="updateInterval('DAILY')" [class.active-button]="selectedButton === 'daily'">Daily</button>
|
||||
|
|
|
|||
|
|
@ -11,13 +11,7 @@
|
|||
.description {
|
||||
color: #49473C;
|
||||
font-size: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.update{
|
||||
color: #49473C;
|
||||
font-size: 15px;
|
||||
margin-top: 18px;
|
||||
margin: 10px 0px 15px 0px;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { ChangeDetectorRef } from '@angular/core';
|
|||
export class HistorygraphComponent implements OnInit, OnDestroy {
|
||||
selectedButton: string = '';
|
||||
selectedInterval: string = '';
|
||||
latestUpdate: string = '';
|
||||
intervalId: any;
|
||||
greeting: string = '';
|
||||
|
||||
|
|
@ -22,7 +21,6 @@ export class HistorygraphComponent implements OnInit, OnDestroy {
|
|||
@ViewChild(GraphComponent) graphComponent!: GraphComponent;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.startClock();
|
||||
this.updateGreeting();
|
||||
this.selectedButton = 'hourly';
|
||||
this.selectedInterval = 'HOURLY';
|
||||
|
|
@ -49,23 +47,6 @@ export class HistorygraphComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
startClock(): void {
|
||||
this.updateLatestTime();
|
||||
this.intervalId = setInterval(() => this.updateLatestTime(), 1000);
|
||||
}
|
||||
|
||||
updateLatestTime(): void {
|
||||
const now = new Date();
|
||||
this.latestUpdate = now.toLocaleString('en-GB', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
updateGreeting(): void {
|
||||
const hour = new Date().getHours();
|
||||
if (hour < 12) {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<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">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,30 @@
|
|||
@import '@angular/material/prebuilt-themes/indigo-pink.css';
|
||||
body{
|
||||
font-family: "Onest", sans-serif;
|
||||
|
||||
}
|
||||
.mat-form-field .mat-input-element:focus {
|
||||
border-color: #16423C !important;
|
||||
box-shadow: 0 1px 5px rgba(0, 128, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.mat-form-field .mat-label.mat-focus-indicator {
|
||||
color: #16423C !important;
|
||||
}
|
||||
|
||||
.mat-form-field .mat-input-element:focus {
|
||||
caret-color: #16423C !important;
|
||||
}
|
||||
|
||||
.mat-form-field .mat-icon {
|
||||
color: #16423C !important;
|
||||
}
|
||||
|
||||
|
||||
.mat-datepicker-toggle.mat-accent {
|
||||
color: #16423C !important;
|
||||
}
|
||||
|
||||
.mat-calendar .mat-calendar-body-selected {
|
||||
background-color: #16423C !important;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user