dev smartfarming #1

Merged
agrilink merged 53 commits from development into main 2024-12-30 05:53:19 +00:00
20 changed files with 1492 additions and 139 deletions
Showing only changes of commit a68c0f045b - Show all commits

File diff suppressed because it is too large Load Diff

View File

@ -10,17 +10,19 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^18.2.8", "@angular/animations": "^18.2.4",
"@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",
"@angular/forms": "^18.2.0", "@angular/forms": "^18.2.0",
"@angular/material": "^17.3.6",
"@angular/platform-browser": "^18.2.0", "@angular/platform-browser": "^18.2.0",
"@angular/platform-browser-dynamic": "^18.2.0", "@angular/platform-browser-dynamic": "^18.2.0",
"@angular/router": "^18.2.0", "@angular/router": "^18.2.0",
"bootstrap": "^5.3.3", "bootstrap": "^5.3.3",
"bootstrap-icons": "^1.11.3", "bootstrap-icons": "^1.11.3",
"chart.js": "^4.4.4", "chart.js": "^4.4.4",
"date-fns": "^4.1.0",
"highcharts-angular": "^4.0.1", "highcharts-angular": "^4.0.1",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"ngx-toastr": "^19.0.0", "ngx-toastr": "^19.0.0",
@ -32,6 +34,7 @@
"@angular-devkit/build-angular": "^18.2.4", "@angular-devkit/build-angular": "^18.2.4",
"@angular/cli": "^18.2.4", "@angular/cli": "^18.2.4",
"@angular/compiler-cli": "^18.2.0", "@angular/compiler-cli": "^18.2.0",
"@types/date-fns": "^2.5.3",
"@types/jasmine": "~5.1.0", "@types/jasmine": "~5.1.0",
"jasmine-core": "~5.2.0", "jasmine-core": "~5.2.0",
"karma": "~6.4.0", "karma": "~6.4.0",

View File

@ -5,6 +5,7 @@ import { AuthComponent } from './pages/auth/auth.component';
import { AuthGuard } from './cores/guards/auth.guard'; import { AuthGuard } from './cores/guards/auth.guard';
import { RegisterComponent } from './pages/register/register.component'; import { RegisterComponent } from './pages/register/register.component';
import { HistorygraphComponent } from './pages/dashboard/page/historygraph/historygraph.component'; import { HistorygraphComponent } from './pages/dashboard/page/historygraph/historygraph.component';
import { ActualgraphComponent } from './pages/dashboard/page/actualgraph/actualgraph.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -33,6 +34,11 @@ export const routes: Routes = [
path: 'historygraph', path: 'historygraph',
component: HistorygraphComponent, component: HistorygraphComponent,
canActivate: [AuthGuard] canActivate: [AuthGuard]
},
{
path: 'actualgraph',
component: ActualgraphComponent,
canActivate: [AuthGuard]
} }
] ]
} }

View File

@ -1,6 +1,7 @@
export interface ParameterSensor { export interface ParameterSensor {
hour: number; hour: number;
day: number; day: number;
date: number;
//for DHT sensor //for DHT sensor
vicitemperature?: number; vicitemperature?: number;

View File

@ -3,10 +3,7 @@
<h1 class="title">{{ greeting }}</h1> <h1 class="title">{{ greeting }}</h1>
<h3 class="description">Welcome back to your management system</h3> <h3 class="description">Welcome back to your management system</h3>
</div> </div>
<div>
<h2 class="update">Latest Update: {{latestUpdate}}</h2>
</div>
<div> <div>
<button [ngClass]="{'active-button': selectedButton === 'dht'}" (click)="selectSensor('dht')">BHT</button> <button [ngClass]="{'active-button': selectedButton === 'dht'}" (click)="selectSensor('dht')">BHT</button>
<button [ngClass]="{'active-button': selectedButton === 'npk1'}" (click)="selectSensor('npk1')">NPK 1</button> <button [ngClass]="{'active-button': selectedButton === 'npk1'}" (click)="selectSensor('npk1')">NPK 1</button>
@ -158,10 +155,15 @@
Katup Nutrisi Katup Nutrisi
</ng-container> </ng-container>
<ng-template #checkRelay> <ng-template #checkRelay>
<ng-container *ngIf="relay.number === 2; else defaultRelay"> <ng-container *ngIf="relay.number === 2; else checkRelay2">
Katup Air Katup Air
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-template #checkRelay2>
<ng-container *ngIf="relay.number === 3; else defaultRelay">
Pompa Air
</ng-container>
</ng-template>
<ng-template #defaultRelay> <ng-template #defaultRelay>
Relay {{ relay.number }} Relay {{ relay.number }}
</ng-template> </ng-template>
@ -170,9 +172,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="graph"> <div class="graph">
<div class="title-graph">Monitoring Graphs</div> <div class="title-graph">Monitoring Graphs</div>

View File

@ -16,13 +16,7 @@
.description { .description {
color: #49473C; color: #49473C;
font-size: 15px; font-size: 15px;
margin-top: 10px; margin: 10px 0px 15px 0px;
}
.update{
color: #49473C;
font-size: 15px;
margin-top: 18px;
} }
.card-container { .card-container {
@ -102,6 +96,10 @@
margin-top: 20px; margin-top: 20px;
justify-content: center; justify-content: center;
} }
.relay-container{
justify-content: center;
}
} }
@media (max-width: 344px) { @media (max-width: 344px) {
@ -126,6 +124,10 @@
margin-top: 20px; margin-top: 20px;
justify-content: center; justify-content: center;
} }
.relay-container{
justify-content: center;
}
} }
button { button {

View File

@ -33,7 +33,6 @@ export class DashboardComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.selectedButton = 'dht'; this.selectedButton = 'dht';
this.startClock();
this.updateGreeting(); this.updateGreeting();
this.loadData(); 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 { updateGreeting(): void {
const hour = new Date().getHours(); const hour = new Date().getHours();
@ -131,8 +110,6 @@ export class DashboardComponent implements OnInit {
}; };
} }
} }
this.updateLatestTime();
this.isLoaded = false; this.isLoaded = false;
}, },
(error) => { (error) => {
@ -157,10 +134,10 @@ export class DashboardComponent implements OnInit {
created_at: relay.created_at, created_at: relay.created_at,
current_status: relay.current_status current_status: relay.current_status
})); }));
this.relayStatus.sort((a, b) => a.number - b.number); this.relayStatus.sort((a, b) => b.number - a.number);
this.noData = false; // Data available
this.noData = false;
} else { } else {
// Throw an error if relay data is empty
this.handleError({ message: 'No available relay data.' }); this.handleError({ message: 'No available relay data.' });
this.noData = true; this.noData = true;
} }
@ -172,9 +149,8 @@ export class DashboardComponent implements OnInit {
this.hasError = true; this.hasError = true;
} }
); );
} }
handleError(error: any): void { handleError(error: any): void {
if (this.selectedButton === 'dht') { if (this.selectedButton === 'dht') {
this.toast.error('Error fetching DHT sensor data. Please try again.', 'Error', { timeOut: 3000 }); this.toast.error('Error fetching DHT sensor data. Please try again.', 'Error', { timeOut: 3000 });

View File

@ -11,6 +11,10 @@
<a routerLink='/historygraph' data-bs-toggle="collapse" class="nav-link px-0 align-middle"> <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> <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>
<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> </ul>
<hr> <hr>

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,18 @@
</ng-container> </ng-container>
<ng-template #graphContent> <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="sensor-wrapper">
<div class="title">Sensor BHT</div> <div class="title">Sensor BHT</div>
<ng-container *ngIf="isLoadingDHT; else dhtData"> <ng-container *ngIf="isLoadingDHT; else dhtData">

View File

@ -1,9 +1,26 @@
.container-graph { .container-graph {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; position: relative;
height: max-content; 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 { .sensor-wrapper {
position: relative; position: relative;
@ -62,6 +79,17 @@
width: 100%; width: 100%;
max-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) { @media (max-width: 344px) {
@ -70,6 +98,17 @@
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
} }
.date-picker{
display: flex;
justify-content: center;
padding: 10px 20px;
}
mat-hint{
font-size: 10px;
align-items: center;
}
} }
.loading { .loading {
@ -88,3 +127,5 @@
height: 100%; height: 100%;
font-size: 18px; font-size: 18px;
} }

View File

@ -2,8 +2,13 @@ import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy, OnC
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js';
import { SensorService } from '../../../../cores/services/sensor.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, formatDate } from '@angular/common';
import { FormsModule } from '@angular/forms'; 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); Chart.register(...registerables);
@ -23,7 +28,8 @@ const parameterColors: { [key: string]: string } = {
@Component({ @Component({
selector: 'app-graph', selector: 'app-graph',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule], imports: [CommonModule, FormsModule, MatDatepickerModule, MatFormFieldModule],
providers: [provideNativeDateAdapter()],
templateUrl: './graph.component.html', templateUrl: './graph.component.html',
styleUrls: ['./graph.component.scss'] styleUrls: ['./graph.component.scss']
}) })
@ -34,6 +40,9 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
@Input() interval: string = 'hourly'; @Input() interval: string = 'hourly';
selectedInterval: string = 'HOURLY'; selectedInterval: string = 'HOURLY';
startDate: Date | null = null;
endDate: Date | null = null;
isLoadingDHT: boolean = true; isLoadingDHT: boolean = true;
isLoadingNPK1: boolean = true; isLoadingNPK1: boolean = true;
isLoadingNPK2: boolean = true; isLoadingNPK2: boolean = true;
@ -73,14 +82,15 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
selectedNPK2: string = 'npk'; selectedNPK2: string = 'npk';
private resizeListener!: () => void; private resizeListener!: () => void;
private updateTimeout: any;
constructor(private sensorService: SensorService, private cdr: ChangeDetectorRef) {} constructor(private sensorService: SensorService, private toast: ToastrService) {}
ngOnInit(): void { ngOnInit(): void {
this.resizeListener = this.onResize.bind(this); this.resizeListener = this.onResize.bind(this);
window.addEventListener('resize', this.resizeListener); window.addEventListener('resize', this.resizeListener);
this.updateCharts();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes['interval'] && changes['interval'].previousValue !== changes['interval'].currentValue) { if (changes['interval'] && changes['interval'].previousValue !== changes['interval'].currentValue) {
@ -92,6 +102,18 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
this.onResize(); 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 { ngOnDestroy(): void {
window.removeEventListener('resize', this.resizeListener); window.removeEventListener('resize', this.resizeListener);
} }
@ -105,13 +127,21 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
this.updateCharts(); 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 { getDate(): string {
const today = new Date(); const today = new Date();
const year = today.getFullYear(); const year = today.getFullYear();
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');
return `${year}-${month}-${day}`; const hasil = `${year}-${month}-${day}`;
return hasil;
} }
getDateAgo():string{ getDateAgo():string{
@ -125,13 +155,13 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
fetchDHTData(timeRange: string): void { fetchDHTData(timeRange: string, startDate?: Date, endDate?: Date): void {
const hEnd = this.getDate(); const hStart = startDate ? this.formatDate(startDate) : this.getDate();
const hStart = this.getDate(); const hEnd = endDate ? this.formatDate(endDate) : this.getDate();
const dEnd = this.getDate(); const dStart= startDate ? this.formatDate(startDate) : this.getDateAgo();
const dStart = 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({ this.sensorService.getSensorDataHourly('dht', 'npk', hStart, hEnd, timeRange).subscribe({
next: (response) => { next: (response) => {
this.isLoadingDHT = false; this.isLoadingDHT = false;
@ -147,7 +177,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
this.isNoDataDHT = true; this.isNoDataDHT = true;
} }
}); });
} else if(timeRange === 'DAILY'){ } else if (timeRange === 'DAILY') {
this.sensorService.getSensorDataDaily('dht', 'npk', dStart, dEnd, timeRange).subscribe({ this.sensorService.getSensorDataDaily('dht', 'npk', dStart, dEnd, timeRange).subscribe({
next: (response) => { next: (response) => {
this.isLoadingDHT = false; this.isLoadingDHT = false;
@ -166,14 +196,14 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
} }
} }
fetchNPK1Data(timeRange: string): void { fetchNPK1Data(timeRange: string, startDate?: Date, endDate?: Date): void {
const hEnd = this.getDate(); const hStart = startDate ? this.formatDate(startDate) : this.getDate();
const hStart = this.getDate(); const hEnd = endDate ? this.formatDate(endDate) : this.getDate();
const dEnd = this.getDate(); const dStart = startDate ? this.formatDate(startDate) : this.getDateAgo();
const dStart = this.getDateAgo(); const dEnd= endDate ? this.formatDate(endDate) : this.getDate();
if(timeRange === 'HOURLY'){ 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) => { next: (response) => {
this.isLoadingNPK1 = false; this.isLoadingNPK1 = false;
if (response.statusCode === 200 && response.data.npk1?.length > 0) { 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.isLoadingNPK1 = false;
this.isNoDataNPK1 = true; this.isNoDataNPK1 = true;
} }
}) });
}else if(timeRange === 'DAILY'){ }else if(timeRange === 'DAILY'){
this.sensorService.getSensorDataDaily('npk1', this.selectedNPK1, dStart, dEnd, timeRange).subscribe({ this.sensorService.getSensorDataDaily('npk1', this.selectedNPK1, dStart, dEnd, timeRange).subscribe({
next: (response) => { next: (response) => {
@ -207,17 +237,18 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
} }
} }
fetchNPK2Data(savedTimeRange: string): void { fetchNPK2Data(savedTimeRange: string, startDate?: Date, endDate?: Date): void {
const hEnd = this.getDate(); const hStart = startDate ? this.formatDate(startDate) : this.getDate();
const hStart = this.getDate(); const hEnd = endDate ? this.formatDate(endDate) : this.getDate();
const dEnd = this.getDate(); const dStart = startDate ? this.formatDate(startDate) : this.getDateAgo();
const dStart = this.getDateAgo(); const dEnd = endDate ? this.formatDate(endDate) : this.getDate();
if(savedTimeRange === 'HOURLY'){ if(savedTimeRange === 'HOURLY'){
this.sensorService.getSensorDataHourly('npk2', this.selectedNPK2, hStart, hEnd, savedTimeRange).subscribe({ this.sensorService.getSensorDataHourly('npk2', this.selectedNPK2, hStart, hEnd, savedTimeRange).subscribe({
next: (response) => { next: (response) => {
this.isLoadingNPK2 = false; this.isLoadingNPK2 = false;
if (response.statusCode === 200 && response.data.npk2?.length > 0) { if (response.statusCode === 200 && response.data.npk2?.length > 0) {
console.log('NPK2 Data:', response);
this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2); this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2);
this.isNoDataNPK2 = false; this.isNoDataNPK2 = false;
} else { } else {
@ -254,13 +285,20 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
Object.keys(this.charts).forEach(key => { Object.keys(this.charts).forEach(key => {
if (this.charts[key]) { if (this.charts[key]) {
this.charts[key]?.destroy(); this.charts[key]?.destroy();
this.charts[key].data.labels = [];
this.charts[key].data.datasets = [];
this.charts[key] = undefined; this.charts[key] = undefined;
} }
}); });
if (this.startDate && this.endDate) {
this.fetchDHTData(interval); this.fetchDHTData(interval, this.startDate, this.endDate);
this.fetchNPK1Data(interval); this.fetchNPK1Data(interval, this.startDate, this.endDate);
this.fetchNPK2Data(interval); 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', mode: 'nearest',
intersect: false, intersect: false,
callbacks: { callbacks: {
label: (tooltipItem) => { label: (tooltipItem: any) => {
const paramLabel = tooltipItem.dataset.label; const paramLabel = tooltipItem.dataset.label;
const value = tooltipItem.formattedValue; const value = tooltipItem.formattedValue;
return `${paramLabel}: ${value}`; return `${paramLabel}: ${value}`;
@ -383,7 +421,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
grid: { display: false }, grid: { display: false },
beginAtZero: true, beginAtZero: true,
ticks: { ticks: {
callback: (value, index, values) => { callback: (value: string | number, index: number, values: any) => {
const labels = this.getLabels(response, sensor); const labels = this.getLabels(response, sensor);
return labels[index] || value; return labels[index] || value;
} }
@ -406,7 +444,8 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
sensorData.forEach(item => { sensorData.forEach(item => {
data.push(item[parameter as keyof ParameterSensor] ?? 0); data.push(item[parameter as keyof ParameterSensor] ?? 0);
if(this.interval === 'HOURLY') { if(this.interval === 'HOURLY') {
labels.push(`${item.hour}.00`); labels.push(item.date);
labels.push(item.hour);
} }
else if (this.interval === 'DAILY') { else if (this.interval === 'DAILY') {
labels.push(item.day); 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]; const sensorData = response.data[sensor as keyof typeof response.data];
return sensorData.map(item => { return sensorData.map(item => {
const formatDate = format(new Date(item.date), 'MMM dd');
if (this.interval === 'HOURLY' || this.selectedInterval === 'HOURLY') { if (this.interval === 'HOURLY' || this.selectedInterval === 'HOURLY') {
return `${item.hour}.00`; return `${formatDate}, ${item.hour}.00`;
} else if (this.interval === 'DAILY') { } else if (this.interval === 'DAILY') {
const day = item.day; const day = item.date;
return this.convertDateToDay(day); const convertDateToDay = this.convertDateToDay(day);
return `${convertDateToDay}, ${formatDate}`;
} else { } else {
return ''; return '';
} }
@ -436,7 +478,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
convertDateToDay(dateString: string): string { convertDateToDay(dateString: string): string {
const date = new Date(dateString); 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()]; return days[date.getDay()];
} }

View File

@ -3,10 +3,6 @@
<h1 class="title">{{ greeting }}</h1> <h1 class="title">{{ greeting }}</h1>
<h3 class="description">View your historical data with customizable time ranges</h3> <h3 class="description">View your historical data with customizable time ranges</h3>
</div> </div>
<div>
<h2 class="update">Latest Update: {{ latestUpdate }}</h2>
</div>
<div> <div>
<button (click)="updateInterval('HOURLY')" [class.active-button]="selectedButton === 'hourly'">Hourly</button> <button (click)="updateInterval('HOURLY')" [class.active-button]="selectedButton === 'hourly'">Hourly</button>
<button (click)="updateInterval('DAILY')" [class.active-button]="selectedButton === 'daily'">Daily</button> <button (click)="updateInterval('DAILY')" [class.active-button]="selectedButton === 'daily'">Daily</button>

View File

@ -11,13 +11,7 @@
.description { .description {
color: #49473C; color: #49473C;
font-size: 15px; font-size: 15px;
margin-top: 10px; margin: 10px 0px 15px 0px;
}
.update{
color: #49473C;
font-size: 15px;
margin-top: 18px;
} }
.card-container { .card-container {

View File

@ -13,7 +13,6 @@ import { ChangeDetectorRef } from '@angular/core';
export class HistorygraphComponent implements OnInit, OnDestroy { export class HistorygraphComponent implements OnInit, OnDestroy {
selectedButton: string = ''; selectedButton: string = '';
selectedInterval: string = ''; selectedInterval: string = '';
latestUpdate: string = '';
intervalId: any; intervalId: any;
greeting: string = ''; greeting: string = '';
@ -22,7 +21,6 @@ export class HistorygraphComponent implements OnInit, OnDestroy {
@ViewChild(GraphComponent) graphComponent!: GraphComponent; @ViewChild(GraphComponent) graphComponent!: GraphComponent;
ngOnInit(): void { ngOnInit(): void {
this.startClock();
this.updateGreeting(); this.updateGreeting();
this.selectedButton = 'hourly'; this.selectedButton = 'hourly';
this.selectedInterval = '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 { updateGreeting(): void {
const hour = new Date().getHours(); const hour = new Date().getHours();
if (hour < 12) { if (hour < 12) {

View File

@ -18,6 +18,7 @@
<script src="https://code.highcharts.com/modules/export-data.js"></script> <script src="https://code.highcharts.com/modules/export-data.js"></script>
<script src="https://code.highcharts.com/modules/accessibility.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 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> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>

View File

@ -1,3 +1,30 @@
@import '@angular/material/prebuilt-themes/indigo-pink.css';
body{ body{
font-family: "Onest", sans-serif; 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;
} }