Compare commits

..

No commits in common. "a4ab48ab20dc3918b05ac60227fae220860c14a5" and "703c5229273b60c68bc7d19f5a4d7aa19a65ccf9" have entirely different histories.

14 changed files with 67 additions and 292 deletions

View File

@ -20,8 +20,7 @@ The data that will display on the graph is 24 hour trend data and there will be
## Technology Stack ## Technology Stack
- Node JS version 18.20.4 - Angular 18 (Front-end Web Framework)
- Angular 18.2.4 (Front-end Web Framework)
- REST API (Backend communication) - REST API (Backend communication)
- GitLab for version control - GitLab for version control
@ -46,25 +45,21 @@ git clone https://gitlab.com/profile-image/kedaireka/smartfarming/frontend-smart
``` ```
cd agrilink_vocpro cd agrilink_vocpro
``` ```
3. Install Dependencies: 3. Run the project:
```
npm install
```
4. Run the project:
``` ```
ng serve ng serve
``` ```
## Project Structure (Angular) ## Project Structure (Angular)
- `src/app/`: Contains the Angular application source code. - `src/app/`: Contains the Angular application source code.
- `cores/`: Contains all constants, interfaces, and services needed for the project. - `cores/`: Contains all constants, interfaces, and services needed for the project.
- `guards/`: Contains guards for
- `interceptors`: Contains handling error
- `interfaces/`: Contains TypeScript interfaces for data models and types used throughout the application. - `interfaces/`: Contains TypeScript interfaces for data models and types used throughout the application.
- `services/`: Contains service files for managing API requests. - `services/`: Contains service files for managing API requests.
- `pages/`: Contains Angular components and views representing different pages of the application. - `pages/`: Contains Angular components and views representing different pages of the application.
- `angular.json`: Angular project configuration file. - `angular.json`: Angular project configuration file.
- `package.json`: Contains project dependencies and scripts for building and running the project. - `package.json`: Contains project dependencies and scripts for building and running the project.

View File

@ -27,9 +27,6 @@ export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => {
case 404: case 404:
errorMessage = 'Resource not found: The requested resource does not exist.'; errorMessage = 'Resource not found: The requested resource does not exist.';
break; break;
case 422:
errorMessage = 'Enter a different username or email';
break;
case 500: case 500:
errorMessage = 'Server error: Please try again later.'; errorMessage = 'Server error: Please try again later.';
break; break;

View File

@ -1,12 +1,13 @@
<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-3 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-9 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>
<form (ngSubmit)="onSubmit()"> <form (ngSubmit)="onSubmit()">
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input type="email" <input type="email"
@ -54,17 +55,13 @@
</span> </span>
</button> </button>
<div class="text-center"> <div class="text-center">
<a class="small forgot" routerLink='/register'>Register here</a> <a class="small forgot" routerLink= '/register'>Register here</a>
</div> </div>
</div> </div>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
<div class="text-center-copyright">
<div>&copy;2024 Agrilink</div>
<div>Powered by <strong>Politeknik Negeri Malang</strong></div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,17 +1,12 @@
.login { .login {
min-height: 100vh; min-height: 100vh;
font-family: "Onest", sans-serif; font-family: "Onest", sans-serif;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
.bg-image { .bg-image {
background-image: url('../../../assets/images/bg-auth.jpg'); background-image: url('../../../assets/images/auth.png');
background-size: cover; background-size: cover;
background-position: right; background-position: left;
height: 100vh;
border-radius: 0px 10px 10px 0px;
} }
.login-heading { .login-heading {
@ -61,17 +56,3 @@
color: white; color: white;
} }
.text-center {
margin-bottom: 20px;
}
.text-center-copyright {
text-align: center;
font-size: 0.85rem;
color: #636363;
padding: 15px 0;
}
.container{
padding-top: 50px;
}

View File

@ -20,7 +20,7 @@
<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]="avatar || 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png'" alt="User Avatar" width="30" height="30" class="rounded-circle"> <!-- Use avatar or a default image --> <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">{{ fullName }}</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">

View File

@ -92,12 +92,6 @@ export class ActualgraphComponent implements OnInit {
y: { y: {
stacked: true, stacked: true,
beginAtZero: false, beginAtZero: false,
ticks: {
callback: (value: string | number, index: number, values: any) => {
return `${value} mg/L`;
}
}
} }
} }
} }

View File

@ -18,23 +18,6 @@
</div> </div>
<div class="sensor-wrapper"> <div class="sensor-wrapper">
<div class="title">Sensor DHT</div> <div class="title">Sensor DHT</div>
<div class="button-param">
<button
[ngClass]="{'active': activeButton === 'vicitemperature'}"
(click)="filterData('vicitemperature')">
Temperatur Udara
</button>
<button
[ngClass]="{'active': activeButton === 'vicihumidity'}"
(click)="filterData('vicihumidity')">
Kelembaban Udara
</button>
<button
[ngClass]="{'active': activeButton === 'viciluminosity'}"
(click)="filterData('viciluminosity')">
Intensitas Cahaya
</button>
</div>
<ng-container *ngIf="isLoadingDHT; else dhtData"> <ng-container *ngIf="isLoadingDHT; else dhtData">
<div class="d-flex align-items-center" style="padding: 50px 0px 50px 0px"> <div class="d-flex align-items-center" style="padding: 50px 0px 50px 0px">
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin"></i>
@ -48,7 +31,7 @@
<div class="sensor-wrapper"> <div class="sensor-wrapper">
<div class="title-with-dropdown"> <div class="title-with-dropdown">
<div class="title">Sensor NPK1</div> <div class="title">Sensor NPK 1</div>
<select class="form-select" style="margin-top: 10px" [(ngModel)]="selectedNPK1" (change)="onResize()"> <select class="form-select" style="margin-top: 10px" [(ngModel)]="selectedNPK1" (change)="onResize()">
<option value="npk">NPK</option> <option value="npk">NPK</option>
<option value="temperature">Temperatur Tanah</option> <option value="temperature">Temperatur Tanah</option>
@ -70,7 +53,7 @@
<div class="sensor-wrapper"> <div class="sensor-wrapper">
<div class="title-with-dropdown"> <div class="title-with-dropdown">
<div class="title">Sensor NPK2</div> <div class="title">Sensor NPK 2</div>
<select class="form-select" style="margin-top: 10px" [(ngModel)]="selectedNPK2" (change)=" onResize()"> <select class="form-select" style="margin-top: 10px" [(ngModel)]="selectedNPK2" (change)=" onResize()">
<option value="npk">NPK</option> <option value="npk">NPK</option>
<option value="temperature">Temperatur Tanah</option> <option value="temperature">Temperatur Tanah</option>

View File

@ -70,7 +70,7 @@
.title { .title {
text-align: center; text-align: center;
font-size: 20px; font-size: 20px;
margin: 18px 0px 18px 0px; margin: 18px 0px 0px 0px;
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -90,18 +90,6 @@
font-size: 10px; font-size: 10px;
align-items: center; align-items: center;
} }
.button-param {
flex-direction: column;
align-items: center;
}
button {
width: 80%;
max-width: 300px;
text-align: center;
}
} }
@media (max-width: 344px) { @media (max-width: 344px) {
@ -121,17 +109,6 @@
font-size: 10px; font-size: 10px;
align-items: center; align-items: center;
} }
.button-param {
flex-direction: column;
align-items: center;
}
button {
width: 80%;
max-width: 300px;
text-align: center;
}
} }
.loading { .loading {
@ -151,28 +128,4 @@
font-size: 18px; font-size: 18px;
} }
.button-param {
display: flex;
justify-content: center;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
margin: 10px 0 10px 0px;
}
button {
font-family: 'Onest', sans-serif;
margin: 0;
padding: 5px 10px;
background-color: #E5E5E5;
color: #16423C;
border: none;
border-radius: 10px;
transition: all 0.3s ease;
}
button.active {
background-color: #16423C;
color: white;
}

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy, OnChanges, Input, SimpleChanges, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy, OnChanges, Input, SimpleChanges, ChangeDetectorRef } from '@angular/core';
import { Chart, registerables, Tooltip } 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, formatDate } from '@angular/common'; import { CommonModule, formatDate } from '@angular/common';
@ -16,10 +16,10 @@ const parameterColors: { [key: string]: string } = {
vicitemperature: '#8F5A62', vicitemperature: '#8F5A62',
vicihumidity: '#16423C', vicihumidity: '#16423C',
viciluminosity: '#DF9B55', viciluminosity: '#DF9B55',
soiltemperature: '#8c2d1c', soiltemperature: '#FF6347',
soilhumidity: '#3d7b8f', soilhumidity: '#0389b5',
soilconductivity: '#f2b8b8', soilconductivity: '#A52A2A',
soilph: '#459645', soilph: '#228B22',
soilnitrogen: '#fece48', soilnitrogen: '#fece48',
soilphosphorus: '#B80F0A', soilphosphorus: '#B80F0A',
soilpotassium: '#4c1f74', soilpotassium: '#4c1f74',
@ -52,7 +52,6 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
isNoDataNPK2: boolean = false; isNoDataNPK2: boolean = false;
allNoData: boolean = false; allNoData: boolean = false;
activeButton: string = 'vicitemperature';
sensorParameters: { [key: string]: string[] } = { sensorParameters: { [key: string]: string[] } = {
dht: ['vicitemperature', 'vicihumidity', 'viciluminosity'], dht: ['vicitemperature', 'vicihumidity', 'viciluminosity'],
@ -100,9 +99,6 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
} }
ngAfterViewInit(): void { ngAfterViewInit(): void {
setTimeout(() => {
this.updateCharts();
}, 0);
this.onResize(); this.onResize();
} }
@ -166,7 +162,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
const dEnd = endDate ? this.formatDate(endDate) : this.getDate(); const dEnd = endDate ? this.formatDate(endDate) : this.getDate();
if (timeRange === 'HOURLY') { if (timeRange === 'HOURLY') {
this.sensorService.getSensorDataHourly('dht', '', hStart, hEnd, timeRange).subscribe({ this.sensorService.getSensorDataHourly('dht', 'npk', hStart, hEnd, timeRange).subscribe({
next: (response) => { next: (response) => {
this.isLoadingDHT = false; this.isLoadingDHT = false;
if (response.statusCode === 200 && response.data.dht?.length > 0) { if (response.statusCode === 200 && response.data.dht?.length > 0) {
@ -284,6 +280,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
updateCharts(): void { updateCharts(): void {
const interval = this.selectedInterval; const interval = this.selectedInterval;
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();
@ -301,23 +298,6 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
this.fetchNPK1Data(interval); this.fetchNPK1Data(interval);
this.fetchNPK2Data(interval); this.fetchNPK2Data(interval);
} }
if (this.charts['dht']) {
this.filterData(this.activeButton);
console.log(this.activeButton);
}
}
filterData(parameter: string): void {
this.activeButton = parameter;
const chart = this.charts['dht'];
if (chart) {
chart.data.datasets.forEach((dataset: any) => {
dataset.hidden = dataset.label !== this.parameterDisplayNames[parameter];
});
chart.update();
}
} }
@ -347,7 +327,6 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
const pointRadius = data.length === 1 ? 5 : 0; const pointRadius = data.length === 1 ? 5 : 0;
const pointHoverRadius = data.length === 1 ? 7 : 0; const pointHoverRadius = data.length === 1 ? 7 : 0;
const isHidden = parameter !== this.activeButton;
return { return {
label: displayName, label: displayName,
@ -359,7 +338,6 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
tension: 0.5, tension: 0.5,
pointRadius, pointRadius,
pointHoverRadius, pointHoverRadius,
hidden : isHidden
}; };
}).filter(dataset => dataset !== null); }).filter(dataset => dataset !== null);
} else { } else {
@ -367,6 +345,8 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
.filter(parameter => { .filter(parameter => {
if (selectedOption === 'npk') { if (selectedOption === 'npk') {
return ['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter); return ['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter);
} else if (selectedOption === 'others') {
return !['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter);
} else if (selectedOption === 'temperature') { } else if (selectedOption === 'temperature') {
return ['soiltemperature'].includes(parameter); return ['soiltemperature'].includes(parameter);
} else if (selectedOption === 'humidity') { } else if (selectedOption === 'humidity') {
@ -392,6 +372,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
const pointRadius = data.length === 1 ? 5 : 0; const pointRadius = data.length === 1 ? 5 : 0;
const pointHoverRadius = data.length === 1 ? 7 : 0; const pointHoverRadius = data.length === 1 ? 7 : 0;
return { return {
label: displayName, label: displayName,
data, data,
@ -416,7 +397,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
this.charts[sensor]?.destroy(); this.charts[sensor]?.destroy();
} }
if(sensor === 'dht'){
this.charts[sensor] = new Chart(ctx, { this.charts[sensor] = new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
@ -440,9 +421,7 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
} }
} }
}, },
legend: { legend: { display: true }
display: false,
},
}, },
scales: { scales: {
x: { x: {
@ -455,87 +434,10 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
} }
} }
}, },
y: { y: { grid: { display: false }, beginAtZero: true }
grid: { display: false },
beginAtZero: true,
ticks: {
callback: (value: string | number, index: number, values: any) => {
if (this.activeButton === 'vicitemperature') {
return `${value} °C`;
} else if (this.activeButton === 'vicihumidity') {
return `${value} %`;
} else if (this.activeButton === 'viciluminosity') {
return `${value} lux`;
}
return value;
}
},
}
}
}
});
}else if(sensor === 'npk1' || sensor === 'npk2'){
this.charts[sensor] = new Chart(ctx, {
type: 'line',
data: {
labels: this.getLabels(response, sensor),
datasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
aspectRatio: 2,
plugins: {
tooltip: {
enabled: true,
mode: 'nearest',
intersect: false,
callbacks: {
label: (tooltipItem: any) => {
const paramLabel = tooltipItem.dataset.label;
const value = tooltipItem.formattedValue;
return `${paramLabel}: ${value}`;
}
}
},
legend: {
display: true,
},
},
scales: {
x: {
grid: { display: false },
beginAtZero: true,
ticks: {
callback: (value: string | number, index: number, values: any) => {
const labels = this.getLabels(response, sensor);
return labels[index] || value;
}
}
},
y: {
grid: { display: false },
beginAtZero: true,
ticks:{
callback: (value: string | number, index: number, values: any) => {
if(selectedOption === 'npk'){
return `${value} mg/L`;
} else if(selectedOption === 'temperature'){
return `${value} °C`;
} else if(selectedOption === 'humidity'){
return `${value} %`;
} else if(selectedOption === 'conductivity'){
return `${value} μS/cm`;
} else if(selectedOption === 'ph'){
return `${value} pH`;
} return value;
}
},
},
} }
} }
}); });
}
} }
@ -586,5 +488,5 @@ export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChang
const days = ['Sun', 'Mon', 'Tues', 'Wed', 'Thrus', 'Fri', 'Sat']; const days = ['Sun', 'Mon', 'Tues', 'Wed', 'Thrus', 'Fri', 'Sat'];
return days[date.getDay()]; return days[date.getDay()];
} }
};
}

View File

@ -54,10 +54,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="text-center-copyright">
<div>&copy;2024 Agrilink</div>
<div>Powered by <strong>Politeknik Negeri Malang</strong></div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,17 +1,12 @@
.login { .login {
min-height: 100vh; min-height: 100vh;
font-family: "Onest", sans-serif; font-family: "Onest", sans-serif;
display: flex;
flex-direction: column;
justify-content: space-between;
} }
.bg-image { .bg-image {
background-image: url('../../../assets/images/bg-auth.jpg'); background-image: url('../../../assets/images/auth.png');
background-size: cover; background-size: cover;
background-position: right; background-position: left;
height: 100vh;
border-radius: 0px 10px 10px 0px;
} }
.login-heading { .login-heading {
@ -51,10 +46,3 @@
} }
} }
} }
.text-center-copyright {
text-align: center;
font-size: 0.85rem;
color: #636363;
padding: 15px 0;
}

View File

@ -40,17 +40,6 @@ export class RegisterComponent implements OnInit {
this.loading = false; this.loading = false;
this.toast.error('Please fill in all fields.'); this.toast.error('Please fill in all fields.');
return; return;
} else if(this.password.length<8){
this.loading= false;
this.toast.error('Password must be at least 8 characters')
} else if(!this.email.includes('@')){
this.loading=false;
this.toast.error('Invalid Email');
} else {
(error: any) => {
this.loading = false;
this.toast.error(error.error.message);
}
} }
const registrationData: RegistrationData = { const registrationData: RegistrationData = {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB