diff --git a/agrilink_vocpro/src/app/app.routes.ts b/agrilink_vocpro/src/app/app.routes.ts
index 31b6cf6..b31b1b4 100644
--- a/agrilink_vocpro/src/app/app.routes.ts
+++ b/agrilink_vocpro/src/app/app.routes.ts
@@ -4,6 +4,7 @@ import { LayoutsComponent } from './pages/dashboard/layouts/layouts.component';
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';
export const routes: Routes = [
{
@@ -28,6 +29,11 @@ export const routes: Routes = [
component: DashboardComponent,
canActivate: [AuthGuard]
},
+ {
+ path: 'historygraph',
+ component: HistorygraphComponent,
+ canActivate: [AuthGuard],
+ }
]
}
];
diff --git a/agrilink_vocpro/src/app/cores/interface/sensor-data.ts b/agrilink_vocpro/src/app/cores/interface/sensor-data.ts
index 165a711..51a219c 100644
--- a/agrilink_vocpro/src/app/cores/interface/sensor-data.ts
+++ b/agrilink_vocpro/src/app/cores/interface/sensor-data.ts
@@ -1,5 +1,6 @@
export interface ParameterSensor {
hour: number;
+ day: number;
//for DHT sensor
vicitemperature?: number;
diff --git a/agrilink_vocpro/src/app/pages/dashboard/layouts/sidebar/sidebar.component.html b/agrilink_vocpro/src/app/pages/dashboard/layouts/sidebar/sidebar.component.html
index eb16a82..879e7fa 100644
--- a/agrilink_vocpro/src/app/pages/dashboard/layouts/sidebar/sidebar.component.html
+++ b/agrilink_vocpro/src/app/pages/dashboard/layouts/sidebar/sidebar.component.html
@@ -7,10 +7,10 @@
-
+
+
+
+
diff --git a/agrilink_vocpro/src/app/pages/dashboard/layouts/sidebar/sidebar.component.ts b/agrilink_vocpro/src/app/pages/dashboard/layouts/sidebar/sidebar.component.ts
index 898c5d0..5d6289c 100644
--- a/agrilink_vocpro/src/app/pages/dashboard/layouts/sidebar/sidebar.component.ts
+++ b/agrilink_vocpro/src/app/pages/dashboard/layouts/sidebar/sidebar.component.ts
@@ -1,11 +1,13 @@
import { Component } from '@angular/core';
import { AuthService } from '../../../../cores/services/auth.service';
-import { Router } from '@angular/router';
+import { Router, RouterModule } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
+
@Component({
selector: 'app-sidebar',
standalone: true,
+ imports: [RouterModule],
templateUrl: './sidebar.component.html',
styleUrls: ['./sidebar.component.scss']
})
diff --git a/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.ts b/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.ts
index dc13980..5cc5a37 100644
--- a/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.ts
+++ b/agrilink_vocpro/src/app/pages/dashboard/page/graph/graph.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
+import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy, OnChanges, Input, SimpleChanges, 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';
@@ -27,10 +27,12 @@ const parameterColors: { [key: string]: string } = {
templateUrl: './graph.component.html',
styleUrls: ['./graph.component.scss']
})
-export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
+export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges{
@ViewChild('myChartDHT', { static: false }) dhtChartElement!: ElementRef;
@ViewChild('myChartNPK1', { static: false }) npk1ChartElement!: ElementRef;
@ViewChild('myChartNPK2', { static: false }) npk2ChartElement!: ElementRef;
+ @Input() interval: string = 'hourly';
+ selectedInterval: string = 'HOURLY';
isLoadingDHT: boolean = true;
isLoadingNPK1: boolean = true;
@@ -72,14 +74,23 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
private resizeListener!: () => void;
- constructor(private sensorService: SensorService) {}
+ constructor(private sensorService: SensorService, private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
this.resizeListener = this.onResize.bind(this);
window.addEventListener('resize', this.resizeListener);
- this.updateCharts();
+ this.updateCharts();
}
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['interval'] && !changes['interval'].firstChange) {
+ this.selectedInterval = changes['interval'].currentValue;
+ this.updateCharts();
+ }
+ }
+
+
ngAfterViewInit(): void {
this.onResize();
}
@@ -97,45 +108,37 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
this.updateCharts();
}
- updateCharts(): void {
- this.isLoadingDHT = this.isLoadingNPK1 = this.isLoadingNPK2 = true;
-
+ 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');
-
- const startEnd = `${year}-${month}-${day}`;
- const timeRange = 'HOURLY';
+ return `${year}-${month}-${day}`;
+ }
- Object.keys(this.charts).forEach(key => {
- if (this.charts[key]) {
- this.charts[key]?.destroy();
- this.charts[key] = undefined;
- }
- });
-
- // Fetch data for DHT
- this.sensorService.getSensorData('dht', '', startEnd, timeRange).subscribe({
+ fetchDHTData(timeRange: string): void {
+ const startEnd = this.getDate();
+ this.sensorService.getSensorData('dht', 'npk', startEnd, timeRange).subscribe({
next: (response) => {
- this.isLoadingDHT = false;
- if (response.statusCode === 200 && response.data.dht?.length > 0) {
- this.createChart(this.dhtChartElement.nativeElement, response, 'dht', 'npk');
- this.isNoDataDHT = false;
- } else {
- this.isNoDataDHT = true;
- }
+ this.isLoadingDHT = false;
+ if (response.statusCode === 200 && response.data.dht?.length > 0) {
+ this.createChart(this.dhtChartElement.nativeElement, response, 'dht', 'npk');
+ this.isNoDataDHT = false;
+ } else {
+ this.isNoDataDHT = true;
+ }
},
error: () => {
- this.isLoadingDHT = false;
- this.isNoDataDHT = true;
+ this.isLoadingDHT = false;
+ this.isNoDataDHT = true;
}
- });
+ });
+ }
-
- // Fetch data for NPK1
- this.sensorService.getSensorData('npk1', '', startEnd, timeRange).subscribe({
+ fetchNPK1Data(timeRange: string): void {
+ const startEnd = this.getDate();
+ this.sensorService.getSensorData('npk1', this.selectedNPK1, startEnd, timeRange).subscribe({
next: (response) => {
this.isLoadingNPK1 = false;
if (response.statusCode === 200 && response.data.npk1?.length > 0) {
@@ -150,10 +153,14 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
this.isNoDataNPK1 = true;
}
});
+ }
- // Fetch data for NPK2
- this.sensorService.getSensorData('npk2', '', startEnd, timeRange).subscribe({
+ fetchNPK2Data(savedTimeRange: string): void {
+ const startEnd = this.getDate();
+ const timeRange = this.interval;
+ this.sensorService.getSensorData('npk2', this.selectedNPK2, startEnd, savedTimeRange).subscribe({
next: (response) => {
+ console.log(savedTimeRange);
this.isLoadingNPK2 = false;
if (response.statusCode === 200 && response.data.npk2?.length > 0) {
this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2);
@@ -169,123 +176,155 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
+ updateCharts(): void {
+ const interval = this.selectedInterval;
+
+ Object.keys(this.charts).forEach(key => {
+ if (this.charts[key]) {
+ this.charts[key]?.destroy();
+ this.charts[key] = undefined;
+ }
+ });
+
+ this.fetchDHTData(interval);
+ this.fetchNPK1Data(interval);
+ this.fetchNPK2Data(interval);
+ }
+
createChart(canvas: HTMLCanvasElement, response: ApiResponse, sensor: string, selectedOption: string): void {
const ctx = canvas.getContext('2d');
const parameters = this.sensorParameters[sensor];
-
+
if (!ctx) {
- console.error('Failed to get canvas context for sensor:', sensor);
- return;
+ console.error('Failed to get canvas context for sensor:', sensor);
+ return;
}
-
+
let datasets: any[] = [];
-
+
if (sensor === 'dht') {
- datasets = ['vicitemperature', 'viciluminosity', 'vicihumidity'].map(parameter => {
- const { data, labels } = this.getDataFromResponse(response, sensor, parameter);
-
- if (data.length === 0) {
- console.warn(`No data found for parameter: ${parameter}`);
- return null;
- }
-
- const displayName = this.parameterDisplayNames[parameter] || parameter;
- const borderColor = parameterColors[parameter] || '#000000';
- const backgroundColor = `${borderColor}4D`;
-
- return {
- label: displayName,
- data,
- borderColor,
- borderWidth: 1.5,
- fill: true,
- backgroundColor,
- tension: 0.5,
- pointRadius: 0,
- pointHoverRadius: 0,
- };
- }).filter(dataset => dataset !== null);
+ datasets = ['vicitemperature', 'viciluminosity', 'vicihumidity'].map(parameter => {
+ const { data, labels } = this.getDataFromResponse(response, sensor, parameter);
+
+ if (data.length === 0) {
+ console.warn(`No data found for parameter: ${parameter}`);
+ return null;
+ }
+
+ const displayName = this.parameterDisplayNames[parameter] || parameter;
+ const borderColor = parameterColors[parameter] || '#000000';
+ const backgroundColor = `${borderColor}4D`;
+
+ const pointRadius = data.length === 1 ? 5 : 0;
+ const pointHoverRadius = data.length === 1 ? 7 : 0;
+
+ return {
+ label: displayName,
+ data,
+ borderColor,
+ borderWidth: 1.5,
+ fill: true,
+ backgroundColor,
+ tension: 0.5,
+ pointRadius,
+ pointHoverRadius,
+ };
+ }).filter(dataset => dataset !== null);
} else {
- datasets = parameters
- .filter(parameter => {
- if (selectedOption === 'npk') {
- return ['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter);
- } else if (selectedOption === 'others') {
- return !['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter);
- }
- return true;
- })
- .map(parameter => {
- const { data, labels } = this.getDataFromResponse(response, sensor, parameter);
-
- if (data.length === 0) {
- console.warn(`No data found for parameter: ${parameter}`);
- return null;
- }
-
- const displayName = this.parameterDisplayNames[parameter] || parameter;
- const borderColor = parameterColors[parameter] || '#000000';
- const backgroundColor = `${borderColor}4D`;
-
- return {
- label: displayName,
- data,
- borderColor,
- borderWidth: 1.5,
- fill: true,
- backgroundColor,
- tension: 0.5,
- pointRadius: 0,
- pointHoverRadius: 0,
- };
- })
- .filter(dataset => dataset !== null);
+ datasets = parameters
+ .filter(parameter => {
+ if (selectedOption === 'npk') {
+ return ['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter);
+ } else if (selectedOption === 'others') {
+ return !['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter);
+ }
+ return true;
+ })
+ .map(parameter => {
+ const { data, labels } = this.getDataFromResponse(response, sensor, parameter);
+
+ if (data.length === 0) {
+ console.warn(`No data found for parameter: ${parameter}`);
+ return null;
+ }
+
+ const displayName = this.parameterDisplayNames[parameter] || parameter;
+ const borderColor = parameterColors[parameter] || '#000000';
+ const backgroundColor = `${borderColor}4D`;
+
+ const pointRadius = data.length === 1 ? 5 : 0;
+ const pointHoverRadius = data.length === 1 ? 7 : 0;
+
+ return {
+ label: displayName,
+ data,
+ borderColor,
+ borderWidth: 1.5,
+ fill: true,
+ backgroundColor,
+ tension: 0.5,
+ pointRadius,
+ pointHoverRadius,
+ };
+ })
+ .filter(dataset => dataset !== null);
}
-
+
if (datasets.length === 0) {
- console.warn('No valid datasets to render for sensor:', sensor);
- return;
+ console.warn('No valid datasets to render for sensor:', sensor);
+ return;
}
-
+
if (this.charts[sensor]) {
- this.charts[sensor]?.destroy();
+ this.charts[sensor]?.destroy();
}
-
+
+
this.charts[sensor] = new Chart(ctx, {
type: 'line',
data: {
- labels: this.getLabels(response, sensor),
- datasets,
+ labels: this.getLabels(response, sensor),
+ datasets,
},
options: {
- responsive: true,
- maintainAspectRatio: false,
- aspectRatio: 2,
- plugins: {
- tooltip: {
- enabled: true,
- mode: 'nearest',
- intersect: false,
- callbacks: {
- label: (tooltipItem) => {
- const paramLabel = tooltipItem.dataset.label;
- const value = tooltipItem.formattedValue;
- return `${paramLabel}: ${value}`;
- }
- }
+ responsive: true,
+ maintainAspectRatio: false,
+ aspectRatio: 2,
+ plugins: {
+ tooltip: {
+ enabled: true,
+ mode: 'nearest',
+ intersect: false,
+ callbacks: {
+ label: (tooltipItem) => {
+ const paramLabel = tooltipItem.dataset.label;
+ const value = tooltipItem.formattedValue;
+ return `${paramLabel}: ${value}`;
+ }
+ }
+ },
+ legend: { display: true }
},
- legend: { display: true }
- },
- scales: {
- x: { grid: { display: false }, beginAtZero: true },
- y: { grid: { display: false }, beginAtZero: true }
- }
+ scales: {
+ x: {
+ grid: { display: false },
+ beginAtZero: true,
+ ticks: {
+ callback: (value, index, values) => {
+ const labels = this.getLabels(response, sensor);
+ return labels[index] || value;
+ }
+ }
+ },
+ y: { grid: { display: false }, beginAtZero: true }
+ }
}
- });
- }
+ });
+ }
+
getDataFromResponse(response: ApiResponse, sensor: string, parameter: string): { data: number[], labels: string[] } {
const sensorData = response.data[sensor as keyof typeof response.data];
@@ -295,7 +334,12 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
if (sensorData) {
sensorData.forEach(item => {
data.push(item[parameter as keyof ParameterSensor] ?? 0);
- labels.push(`${item.hour}.00`);
+ if(this.interval === 'HOURLY') {
+ labels.push(`${item.hour}.00`);
+ }
+ else if (this.interval === 'DAILY') {
+ labels.push(item.day);
+ }
});
} else {
console.error('No data found for sensor:', sensor);
@@ -306,6 +350,23 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy {
getLabels(response: ApiResponse, sensor: string): string[] {
const sensorData = response.data[sensor as keyof typeof response.data];
- return sensorData ? sensorData.map(item => `${item.hour}.00`) : [];
+
+ return sensorData.map(item => {
+ if (this.interval === 'HOURLY' || this.selectedInterval === 'HOURLY') {
+ return `${item.hour}.00`;
+ } else if (this.interval === 'DAILY') {
+ const day = item.day;
+ return this.convertDateToDay(day);
+ } else {
+ return '';
+ }
+ });
}
+
+ convertDateToDay(dateString: string): string {
+ const date = new Date(dateString);
+ const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thrusday', 'Friday', 'Saturday'];
+ return days[date.getDay()];
+ }
+
}
diff --git a/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.html b/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.html
new file mode 100644
index 0000000..05ad280
--- /dev/null
+++ b/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.html
@@ -0,0 +1,19 @@
+
+
+
{{ greeting }}
+ View your historical data with customizable time ranges
+
+
+
Latest Update: {{ latestUpdate }}
+
+
+
+
+
+
+
+
+
+
diff --git a/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.scss b/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.scss
new file mode 100644
index 0000000..5f1aca4
--- /dev/null
+++ b/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.scss
@@ -0,0 +1,137 @@
+.container {
+ font-family: "Onest", sans-serif;
+}
+
+.title {
+ color: #49473C;
+ font-size: 30px;
+ margin-top: 10px;
+}
+
+.description {
+ color: #49473C;
+ font-size: 15px;
+ margin-top: 10px;
+}
+
+.update{
+ color: #49473C;
+ font-size: 15px;
+ margin-top: 18px;
+}
+
+.card-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 30px;
+ margin-top: 20px;
+ justify-content: flex-start;
+}
+
+.card-parameter{
+ border: 1px solid #16423C;
+ color: #16423C;
+ padding: 20px 0px 20px 0px;
+ border-radius: 8px;
+ text-align: center;
+ flex: 1 1 30%;
+ max-width: 30%;
+ min-width: 200px;
+}
+
+.card-parameter:hover{
+ background-color: #16423C;
+ color: white;
+}
+
+.card-content{
+ text-align: center;
+ margin:auto;
+}
+
+.title-graph{
+ color: #49473C;
+ font-size: 23px;
+ font-weight: 400;
+ margin-top: 45px;
+}
+
+.graph{
+ margin-top: 22px;
+}
+
+.relay-container {
+ padding: 20px 0px 20px 0px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 30px;
+ justify-content: flex-start;
+}
+
+.relay-card {
+ border-radius: 8px;
+ flex: 1 1 30%;
+ max-width: 30%;
+ min-width: 200px;
+}
+
+@media (max-width: 768px) {
+ .card-parameter{
+ flex: 1 1 45%;
+ }
+
+ .card-container{
+ justify-content: center;
+ }
+}
+
+@media (max-width: 576px) {
+ .card-parameter{
+ flex: 1 1 100%;
+ }
+
+ .card-container{
+ justify-content: center;
+ }
+}
+
+button {
+ border: none;
+ border-radius: 10px;
+ padding: 5px 10px;
+ font-size: 15px;
+ cursor: pointer;
+ margin-right: 20px;
+}
+
+
+button {
+ border: none;
+ border-radius: 10px;
+ padding: 5px 10px;
+ font-size: 15px;
+ cursor: pointer;
+ margin-right: 20px;
+}
+
+.active-button {
+ background-color: #cad849;
+ color: white;
+}
+
+.loading{
+ font-size: 18px;
+ text-align: center;
+}
+
+.status-on {
+ color: #16423C;
+}
+
+.status-off {
+ color: rgb(144, 6, 6);
+}
+
+.spinner {
+ color: #16423C
+}
\ No newline at end of file
diff --git a/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.spec.ts b/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.spec.ts
new file mode 100644
index 0000000..c80b804
--- /dev/null
+++ b/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HistorygraphComponent } from './historygraph.component';
+
+describe('HistorygraphComponent', () => {
+ let component: HistorygraphComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [HistorygraphComponent]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(HistorygraphComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.ts b/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.ts
new file mode 100644
index 0000000..ba07afd
--- /dev/null
+++ b/agrilink_vocpro/src/app/pages/dashboard/page/historygraph/historygraph.component.ts
@@ -0,0 +1,81 @@
+import { Component, ViewChild, OnInit, OnDestroy } from '@angular/core';
+import { GraphComponent } from '../graph/graph.component';
+import { CommonModule } from '@angular/common';
+import { ChangeDetectorRef } from '@angular/core';
+
+@Component({
+ selector: 'app-historygraph',
+ standalone: true,
+ imports: [GraphComponent, CommonModule],
+ templateUrl: './historygraph.component.html',
+ styleUrls: ['./historygraph.component.scss']
+})
+export class HistorygraphComponent implements OnInit, OnDestroy {
+ selectedButton: string = '';
+ selectedInterval: string = '';
+ latestUpdate: string = '';
+ intervalId: any;
+ greeting: string = '';
+
+ constructor(private cdr: ChangeDetectorRef) {}
+
+ @ViewChild(GraphComponent) graphComponent!: GraphComponent;
+
+ ngOnInit(): void {
+ this.startClock();
+ this.updateGreeting();
+ this.selectedButton = 'hourly';
+ this.selectedInterval = 'HOURLY';
+ }
+
+
+ private debounceTimeout: any;
+
+ updateInterval(interval: string): void {
+ clearTimeout(this.debounceTimeout);
+ this.debounceTimeout = setTimeout(() => {
+ if (this.selectedInterval !== interval) {
+ this.selectedInterval = interval;
+ this.selectedButton = interval.toLowerCase();
+ this.cdr.detectChanges();
+ this.graphComponent.updateCharts();
+ }
+ }, 300);
+ }
+
+
+ ngOnDestroy(): void {
+ if (this.intervalId) {
+ clearInterval(this.intervalId);
+ }
+ }
+
+ 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) {
+ this.greeting = 'Good Morning';
+ } else if (hour < 18) {
+ this.greeting = 'Good Afternoon';
+ } else {
+ this.greeting = 'Good Evening';
+ }
+ }
+}
+