dev smartfarming #1

Merged
agrilink merged 53 commits from development into main 2024-12-30 05:53:19 +00:00
66 changed files with 19045 additions and 71 deletions

136
README.md
View File

@ -1,93 +1,87 @@
# frontend-smartfarming # Agrilink Vocpro (Smart Farming Web Application)
A Website application that using Angular 18 for monitoring greenhouse conditions with IoT device that have 2 NPK, DHT, and BH1750 sensors
## Overview
This application enables user to monitor greenhouse conditions by IoT device that include 2 NPK, DHT, and BH1750 sensors.
The data that will display on the graph is 24 hour trend data and there will be last 1 hour data on the card interface
## Getting started ## Features
To make it easy for you to get started with GitLab, here's a list of recommended next steps. - Monitor sensor data
- User authentication
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files ## Technology Stack
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
- Node JS version 18.20.4
- Angular 18.2.4 (Front-end Web Framework)
- REST API (Backend communication)
- GitLab for version control
## Angular Dependency Stack
This project uses several packages to support various functionalities. Here is a list of the key dependencies:
- **[Chart.js](https://www.chartjs.org/)**: A powerful JavaScript library used for creating dynamic, interactive charts in the application.
- **[Bootstrap](https://getbootstrap.com/)**: A front-end framework for building responsive and mobile-first web designs, providing pre-designed components and utilities.
- **Event Listener**: Used to manage events like resizing the window, allowing the chart to adjust automatically to screen size changes.
- **[Angular FormsModule](https://angular.io/api/forms/FormsModule)**: Supports template-driven forms, enabling form handling and user input in the application.
## Installation Guide
1. Clone the repository:
``` ```
cd existing_repo git clone https://gitlab.com/profile-image/kedaireka/smartfarming/frontend-smartfarming.git
git remote add origin https://gitlab.com/profile-image/kedaireka/smartfarming/frontend-smartfarming.git ```
git branch -M main 2. Navigate to the project directory:
git push -uf origin main ```
cd agrilink_vocpro
```
3. Install Dependencies:
```
npm install
```
4. Run the project:
```
ng serve
``` ```
## Integrate with your tools
- [ ] [Set up project integrations](https://gitlab.com/profile-image/kedaireka/smartfarming/frontend-smartfarming/-/settings/integrations) ## Project Structure (Angular)
## Collaborate with your team - `src/app/`: Contains the Angular application source code.
- `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.
- `services/`: Contains service files for managing API requests.
- `pages/`: Contains Angular components and views representing different pages of the application.
- `angular.json`: Angular project configuration file.
- `package.json`: Contains project dependencies and scripts for building and running the project.
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy ## Progress Report
Use the built-in continuous integration in GitLab. | Date | Type | Description |
|------------|-------|-----------------------------------------------------------------------------------------------------|
| 2024-10-11 | feat | Completed the integration for sensor data by using REST API on graphic and card at dashboard. |
| 2024-10-08 | fix | Adding lazy load and fixing responsive UI for data sensor at card section |
| 2024-09-30 | fix | Fixing responsive UI for data sensor in graph |
| 2024-09-27 | fix | Fix default data on graphic if change into other sensor, adding different color for each parameters on sensor, adding information about data units for parameters on the graph |
| 2024-09-20 | fix | Fix slicing interface at dashboard |
| 2024-09-20 | feat | Initial commit for slicing dashboard and login interface |
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License ## License
For open source projects, say how it is licensed. For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.

View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

42
agrilink_vocpro/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

20
agrilink_vocpro/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

42
agrilink_vocpro/.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

27
agrilink_vocpro/README.md Normal file
View File

@ -0,0 +1,27 @@
# AgrilinkVocpro
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 18.2.4.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View File

@ -0,0 +1,115 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"agrilink_vocpro": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/agrilink_vocpro",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/bootstrap-icons/font/bootstrap-icons.css",
"node_modules/ngx-toastr/toastr.css"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kB",
"maximumError": "4kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "agrilink_vocpro:build:production"
},
"development": {
"buildTarget": "agrilink_vocpro:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.scss",
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/bootstrap-icons/font/bootstrap-icons.css",
"node_modules/ngx-toastr/toastr.css"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]
}
}
}
}
},
"cli": {
"analytics": false
}
}

15440
agrilink_vocpro/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
{
"name": "agrilink-vocpro",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@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",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.14.10"
},
"devDependencies": {
"@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",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.5.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@ -0,0 +1,29 @@
import { TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'agrilink_vocpro' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('agrilink_vocpro');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, agrilink_vocpro');
});
});

View File

@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'agrilink_vocpro';
}

View File

@ -0,0 +1,18 @@
import { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimations } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { routes } from './app.routes';
import { httpErrorInterceptor } from './cores/interceptors/http-error-interceptor.service';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
provideAnimations(),
importProvidersFrom(ToastrModule.forRoot()),
provideHttpClient(withInterceptors([httpErrorInterceptor])) // Register your HttpErrorInterceptor
]
};

View File

@ -0,0 +1,45 @@
import { Routes } from '@angular/router';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
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';
import { ActualgraphComponent } from './pages/dashboard/page/actualgraph/actualgraph.component';
export const routes: Routes = [
{
path: '',
component: AuthComponent,
},
{
path: 'auth',
component: AuthComponent
},
{
path: 'register',
component: RegisterComponent
},
{
path: '',
component: LayoutsComponent,
children: [
{
path: 'dashboard',
component: DashboardComponent,
canActivate: [AuthGuard]
},
{
path: 'historygraph',
component: HistorygraphComponent,
canActivate: [AuthGuard]
},
{
path: 'actualgraph',
component: ActualgraphComponent,
canActivate: [AuthGuard]
}
]
}
];

View File

@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { StorageService } from '../services/storage.service';
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(
private router: Router,
private toast: ToastrService,
private storageService: StorageService
) {}
canActivate(): boolean {
if(this.storageService.getToken()) {
return true;
}else{
this.router.navigate(['/auth']);
this.toast.error('You need to login first');
return false;
}
}
}

View File

@ -0,0 +1,46 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { HttpErrorResponse } from '@angular/common/http';
export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => {
const toast = inject(ToastrService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
let errorMessage = '';
if (error.error instanceof ErrorEvent) {
errorMessage = `Client error: ${error.error.message}`;
} else {
switch (error.status) {
case 0:
errorMessage = 'Network error: Please check your internet connection.';
break;
case 401:
errorMessage = 'Unauthorized: Please log in again.';
break;
case 403:
errorMessage = 'Forbidden: You do not have permission to access this resource.';
break;
case 404:
errorMessage = 'Resource not found: The requested resource does not exist.';
break;
case 422:
errorMessage = 'Enter a different username or email';
break;
case 500:
errorMessage = 'Server error: Please try again later.';
break;
default:
errorMessage = `Unexpected error: ${error.message}`;
}
}
toast.error(errorMessage, 'Error', { timeOut: 3000 });
return throwError(() => new Error(errorMessage));
})
);
};

View File

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

View File

@ -0,0 +1,56 @@
export interface ParameterSensor {
hour: number;
day: number;
date: number;
//for DHT sensor
vicitemperature?: number;
vicihumidity?: number;
viciluminosity?: number;
// for NPK1 & NPK2 sensor
soiltemperature?: number;
soilhumidity?: number;
soilconductivity?: number;
soilph?: number;
soilnitrogen?: number;
soilphosphorus?: number;
soilpotassium?: number;
}
export interface ApiResponse {
data: {
dht: Array<any>;
npk1: Array<any>;
npk2: Array<any>;
};
statusCode: number;
message: string;
}
export interface DHTSensor {
lightIntensity: number;
temperature: number;
humidity: number;
}
export interface NPKSensor {
temperature: number;
moisture: number;
conductivity: number;
ph: number;
nitrogen: number;
phosphorus: number;
potassium: number;
}
export interface SensorData {
dht: DHTSensor;
npk1: NPKSensor;
npk2: NPKSensor;
}
export interface StatusRelay {
number: number;
current_status: boolean;
}

View File

@ -0,0 +1,11 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class ApiService {
protected baseUrl = 'http://54.196.58.97:3333/';
constructor(protected http: HttpClient) {}
}

View File

@ -0,0 +1,77 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, tap, catchError } from 'rxjs';
import { ApiService } from './api.service';
import { LoginData } from '../interface/auth';
import { jwtDecode } from 'jwt-decode';
import { StorageService } from './storage.service';
import { ToastrService } from 'ngx-toastr';
@Injectable({
providedIn: 'root'
})
export class AuthService extends ApiService {
private authUrl = `${this.baseUrl}auth/login`;
private logoutUrl = `${this.baseUrl}auth/logout`;
private registerUrl = `${this.baseUrl}auth/register`;
constructor(
http: HttpClient,
private storageService: StorageService,
private toast: ToastrService,
) {
super(http);
}
login(data: LoginData): Observable<any> {
const headers = new HttpHeaders({
Authorization: 'Basic ' + btoa(`${data.email}:${data.password}`)
});
const formData = new FormData();
formData.append('remember_me', data.rememberMe ? 'true' : 'false');
return this.http.post<any>(this.authUrl, formData, { headers }).pipe(
tap(response => {
const accessToken = response.data.token;
this.storageService.saveToken(accessToken, data.rememberMe ?? false);
const jwtToken = response.data.jwtToken;
const decodedToken: any = jwtDecode(jwtToken);
this.storageService.saveUserData(decodedToken.user.fullname, decodedToken.user.avatar);
}),
catchError(error => {
this.toast.error('Login failed, please try again');
throw error;
})
);
}
logout(): Observable<any> {
const token = this.storageService.getToken();
const headers = new HttpHeaders({
Authorization: `Bearer ${token}`
});
return this.http.post<any>(this.logoutUrl, {}, { headers }).pipe(
tap(() => {
this.storageService.clearToken();
this.storageService.clearUserData();
}),
);
}
register(data: any): Observable<any> {
const headers = new HttpHeaders({});
return this.http.post<any>(this.registerUrl, data, { headers });
}
getUserFullName(): string | null {
return this.storageService.getUserFullName();
}
getAvatar(): string | null {
return this.storageService.getAvatar();
}
}

View File

@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import { HttpParams, HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { catchError, Observable, throwError, tap } from 'rxjs';
import { ApiService } from './api.service';
import { ApiResponse } from '../interface/sensor-data';
import { StorageService } from './storage.service';
import { ToastrService } from 'ngx-toastr';
@Injectable({
providedIn: 'root'
})
export class SensorService extends ApiService {
constructor(http: HttpClient,
private storageService: StorageService,
private toast: ToastrService) {
super(http);
}
private getDataUrl = `${this.baseUrl}api/sensor/getData`;
private getLatestUrl = `${this.baseUrl}api/sensor/getLatest`;
private getStatusRelay = `${this.baseUrl}api/relay/get-relay`;
private createAuthHeaders(): HttpHeaders {
const token = this.storageService.getToken();
return new HttpHeaders({
Authorization: `Bearer ${token}`
});
}
getSensorDataHourly(sensor: string, metric: string, start: string, end: string, timeRange: string): Observable<ApiResponse> {
const params = new HttpParams()
.set('range[start]', start)
.set('range[end]', end)
.set('range[time_range]', timeRange)
.set('sensor', sensor)
.set('metric', metric);
const headers= this.createAuthHeaders();
return this.http.get<ApiResponse>(this.getDataUrl, { params, headers }).pipe(
catchError(error => {
// this.toast.error('Failed to get sensor data for graphic, please try again');
return throwError(error);
})
);
}
getSensorDataDaily(sensor: string, metric: string, start: string, end: string, timeRange: string): Observable<ApiResponse> {
const params = new HttpParams()
.set('range[start]', start)
.set('range[end]', end)
.set('range[time_range]', timeRange)
.set('sensor', sensor)
.set('metric', metric);
const headers= this.createAuthHeaders();
return this.http.get<ApiResponse>(this.getDataUrl, { params, headers }).pipe(
catchError(error => {
// this.toast.error('Failed to get sensor data for graphic, please try again');
return throwError(error);
})
);
}
getLatestData(): Observable<ApiResponse> {
const headers = this.createAuthHeaders();
return this.http.get<any>(this.getLatestUrl, { headers }).pipe(
catchError(error => {
// this.toast.error('Failed to get latest sensor data, please try again');
return throwError(error);
})
);
}
getRelayStatus(): Observable<ApiResponse> {
const headers = this.createAuthHeaders();
return this.http.get<any>(this.getStatusRelay, { headers }).pipe(
catchError(error => {
// this.toast.error('Failed to get relay status, please try again');
return throwError(error);
})
);
}
}

View File

@ -0,0 +1,44 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
saveToken(token: string, rememberMe: boolean): void {
if (rememberMe) {
localStorage.setItem('accessToken', token);
localStorage.setItem('rememberMe', 'true');
} else {
sessionStorage.setItem('accessToken', token);
localStorage.removeItem('rememberMe')
}
}
getToken(): string | null {
return localStorage.getItem('accessToken') || sessionStorage.getItem('accessToken');
}
clearToken(): void {
localStorage.removeItem('accessToken');
sessionStorage.removeItem('accessToken');
localStorage.removeItem('rememberMe');
}
saveUserData(fullName: string | null, avatar: string | null): void {
localStorage.setItem('userFullName', fullName || '');
localStorage.setItem('avatar', avatar || '');
}
getUserFullName(): string | null {
return localStorage.getItem('userFullName');
}
getAvatar(): string | null {
return localStorage.getItem('avatar');
}
clearUserData(): void {
localStorage.removeItem('userFullName');
localStorage.removeItem('avatar');
}
}

View File

@ -0,0 +1,71 @@
<div class="container-fluid ps-md-0">
<div class="row g-0">
<div class="d-none d-md-flex col-md-3 col-lg-6 bg-image"></div>
<div class="col-md-9 col-lg-6">
<div class="login d-flex align-items-center py-5">
<div class="container">
<div class="row">
<div class="col-md-9 col-lg-8 mx-auto">
<h3 class="login-heading mb-4">Welcome back!</h3>
<form (ngSubmit)="onSubmit()">
<div class="form-floating mb-3">
<input type="email"
class="form-control"
[(ngModel)]="email"
name="email"
id="floatingInput"
placeholder="name@example.com"
required>
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating mb-3">
<input [type]="passwordVisible ? 'text' : 'password'"
class="form-control"
[(ngModel)]="password"
name="password"
id="floatingPassword"
placeholder="Password"
required>
<label for="floatingPassword">Password</label>
<button type="button"
class="btn btn-link position-absolute end-0 me-3"
(click)="togglePasswordVisibility()">
<i class="fa" [ngClass]="passwordVisible ? 'fa-eye' : 'fa-eye-slash'"></i>
</button>
</div>
<div class="form-check mb-3">
<input class="form-check-input"
type="checkbox"
[(ngModel)]="rememberMe"
name="rememberMe"
id="rememberPasswordCheck">
<label class="form-check-label" for="rememberPasswordCheck">Remember me</label>
</div>
<div class="d-grid">
<button class="btn btn-lg color-btn btn-login text-uppercase fw-bold mb-2"
type="submit"
[disabled]="loading">
<span *ngIf="!loading">Sign in</span>
<span *ngIf="loading">
<i class="fa fa-spinner fa-spin"></i>
</span>
</button>
<div class="text-center">
<a class="small forgot" routerLink='/register'>Register here</a>
</div>
</div>
</form>
</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>

View File

@ -0,0 +1,77 @@
.login {
min-height: 100vh;
font-family: "Onest", sans-serif;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.bg-image {
background-image: url('../../../assets/images/bg-auth.jpg');
background-size: cover;
background-position: right;
height: 100vh;
border-radius: 0px 10px 10px 0px;
}
.login-heading {
font-weight: 300;
}
.btn-login {
font-size: 0.9rem;
letter-spacing: 0.05rem;
padding: 0.75rem 1rem;
}
.color-btn {
background-color: #16423C;
color: white;
}
.forgot {
color: #16423C;
}
.form-floating {
position: relative;
.btn-link {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
border: none;
background: transparent;
cursor: pointer;
color: #16423C;
&:hover {
color: #16423C;
}
}
}
.form-check-input:checked {
background-color: #16423C;
border-color: #16423C;
}
.form-check-input:checked::before {
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

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AuthComponent } from './auth.component';
describe('AuthComponent', () => {
let component: AuthComponent;
let fixture: ComponentFixture<AuthComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AuthComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AuthComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,67 @@
import { Component } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { AuthService } from '../../cores/services/auth.service';
import { StorageService } from '../../cores/services/storage.service';
import { FormsModule } from '@angular/forms';
import { LoginData } from '../../cores/interface/auth';
import { ToastrService } from 'ngx-toastr';
import { CommonModule } from '@angular/common';
import { catchError } from 'rxjs';
@Component({
selector: 'app-auth',
standalone: true,
imports: [FormsModule, RouterModule, CommonModule],
templateUrl: './auth.component.html',
styleUrls: ['./auth.component.scss']
})
export class AuthComponent {
email: string = '';
password: string = '';
rememberMe: boolean = false;
loading: boolean = false;
passwordVisible: boolean = false;
constructor(private authService: AuthService,
private storageService: StorageService,
private router: Router,
private toast: ToastrService) {}
ngOnInit(): void {
if (this.storageService.getToken()) {
this.router.navigate(['/dashboard']);
}
}
onSubmit() {
this.loading = true;
if (!this.email || !this.password) {
this.loading = false;
this.toast.error('Please fill in all fields.');
return;
}
const loginData: LoginData = {
email: this.email,
password: this.password,
rememberMe: this.rememberMe
};
this.authService.login(loginData).subscribe(
(response) => {
this.storageService.saveToken(response.data.token, this.rememberMe);
this.router.navigate(['/dashboard']);
this.toast.success('Login successful');
this.loading = false;
},
(error) => {
this.loading = false;
this.toast.error(error.error.message);
}
);
}
togglePasswordVisibility() {
this.passwordVisible = !this.passwordVisible;
}
}

View File

@ -0,0 +1,184 @@
<div class="container">
<div>
<h1 class="title">{{ greeting }}</h1>
<h3 class="description">Welcome back to your management system</h3>
</div>
<div>
<button [ngClass]="{'active-button': selectedButton === 'dht'}" (click)="selectSensor('dht')">DHT</button>
<button [ngClass]="{'active-button': selectedButton === 'npk1'}" (click)="selectSensor('npk1')">NPK 1</button>
<button [ngClass]="{'active-button': selectedButton === 'npk2'}" (click)="selectSensor('npk2')">NPK 2</button>
<button [ngClass]="{'active-button': selectedButton === 'relay'}" (click)="selectSensor('relay')">Relay</button>
</div>
<div *ngIf="isLoaded" class="loading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<ng-template #noData>
<div class="loading">No available data</div>
</ng-template>
<div *ngIf="!isLoaded && selectedButton === 'dht'">
<div *ngIf="sensorData.dht.lightIntensity || sensorData.dht.temperature || sensorData.dht.humidity; else noData">
<div class="card-container">
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.lightIntensity}} Lux</h3>
<h6>Intensitas Cahaya</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.temperature}} °C</h3>
<h6>Temperatur Udara</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.dht.humidity}} %</h3>
<h6>Kelembaban Udara</h6>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'npk1'">
<div *ngIf="sensorData.npk1.temperature || sensorData.npk1.moisture || sensorData.npk1.conductivity; else noData">
<div class="card-container">
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.nitrogen}} mg/L</h3>
<h6>Nitrogen</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.phosphorus}} mg/L</h3>
<h6>Phosphorus</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.potassium}} mg/L</h3>
<h6>Kalium</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.temperature}} °C</h3>
<h6>Temperatur Tanah</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.moisture}} %</h3>
<h6>Kelembaban Tanah</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.conductivity}} μS/cm</h3>
<h6>Conductivity</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk1.ph}}</h3>
<h6>pH</h6>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'npk2'">
<div *ngIf="sensorData.npk2.temperature || sensorData.npk2.moisture || sensorData.npk2.conductivity; else noData">
<div class="card-container">
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.nitrogen}} mg/L</h3>
<h6>Nitrogen</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.phosphorus}} mg/L</h3>
<h6>Phosphorus</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.potassium}} mg/L</h3>
<h6>Kalium</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.temperature}} °C</h3>
<h6>Temperatur Tanah</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.moisture}} %</h3>
<h6>Kelembaban Tanah</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.conductivity}} μS/cm</h3>
<h6>Conductivity</h6>
</div>
</div>
<div class="card-parameter">
<div>
<h3>{{sensorData.npk2.ph}}</h3>
<h6>pH</h6>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="!isLoaded && selectedButton === 'relay'">
<div *ngIf="relayStatus.length > 0; else noData">
<div class="relay-container">
<div class="card-parameter relay-card" *ngFor="let relay of relayStatus;">
<div>
<h3 [ngClass]="relay.current_status ? 'status-on' : 'status-off'">
{{ relay.current_status ? 'ON' : 'OFF' }}
</h3>
<h6>
<ng-container *ngIf="relay.number === 1; else checkRelay">
Katup Nutrisi
</ng-container>
<ng-template #checkRelay>
<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>
</h6>
</div>
</div>
</div>
</div>
</div>
<div class="graph">
<div class="title-graph">Monitoring Graphs</div>
<div class="graph">
<app-graph></app-graph>
</div>
</div>
</div>

View File

@ -0,0 +1,157 @@
.container {
font-family: "Onest", sans-serif;
.active-button {
background-color: #cad849;
color: white;
}
}
.title {
color: #49473C;
font-size: 30px;
margin-top: 10px;
}
.description {
color: #49473C;
font-size: 15px;
margin: 10px 0px 15px 0px;
}
.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%;
}
.active-button {
background-color: #cad849;
color: white;
}
button {
color: black;
margin-bottom: 20px;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 30px;
margin-top: 20px;
justify-content: center;
}
.relay-container{
justify-content: center;
}
}
@media (max-width: 344px) {
.card-parameter{
flex: 1 1 100%;
}
button {
color: black;
margin-bottom: 20px;
}
.active-button {
background-color: #cad849;
color: white;
}
.card-container {
display: flex;
flex-wrap: wrap;
gap: 30px;
margin-top: 20px;
justify-content: center;
}
.relay-container{
justify-content: center;
}
}
button {
border: none;
border-radius: 10px;
padding: 5px 10px;
font-size: 15px;
cursor: pointer;
margin-right: 20px;
}
.loading{
font-size: 18px;
text-align: center;
}
.status-on {
color: #16423C;
}
.status-off {
color: rgb(144, 6, 6);
}
.spinner {
color: #16423C
}

View File

@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DashboardComponent]
})
.compileComponents();
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,166 @@
import { Component, OnInit } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SidebarComponent } from './layouts/sidebar/sidebar.component';
import { GraphComponent } from './page/graph/graph.component';
import { CommonModule } from '@angular/common';
import { SensorService } from '../../cores/services/sensor.service';
import { SensorData, StatusRelay } from '../../cores/interface/sensor-data';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [RouterOutlet, SidebarComponent, CommonModule, GraphComponent],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss']
})
export class DashboardComponent implements OnInit {
isLoaded: boolean = false;
hasError: boolean = false;
noData: boolean = false;
selectedButton: string = '';
latestUpdate: string = '';
intervalId: any;
greeting: string = '';
sensorData: SensorData = {
dht: { lightIntensity: 0, temperature: 0, humidity: 0 },
npk1: { temperature: 0, moisture: 0, conductivity: 0, ph: 0, nitrogen: 0, phosphorus: 0, potassium: 0 },
npk2: { temperature: 0, moisture: 0, conductivity: 0, ph: 0, nitrogen: 0, phosphorus: 0, potassium: 0 }
};
relayStatus: StatusRelay[] = [];
constructor(private apiService: SensorService, private toast: ToastrService) {}
ngOnInit(): void {
this.selectedButton = 'dht';
this.updateGreeting();
this.loadData();
}
ngOnDestroy(): void {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
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';
}
}
selectSensor(param: string): void {
this.selectedButton = param;
if(param === 'relay'){
this.loadRelayData();
} else {
this.loadData();
}
}
loadData(): void {
this.isLoaded = true;
this.hasError = false;
this.noData = false;
this.apiService.getLatestData().subscribe(
(response) => {
const data = response.data;
if ((!data.dht || data.dht.length === 0) &&
(!data.npk1 || data.npk1.length === 0) &&
(!data.npk2 || data.npk2.length === 0)) {
this.noData = true;
} else {
if (data.dht && data.dht.length > 0) {
this.sensorData.dht = {
lightIntensity: data.dht[0].viciluminosity ?? 0,
temperature: data.dht[0].vicitemperature ?? 0,
humidity: data.dht[0].vicihumidity ?? 0
};
}
if (data.npk1 && data.npk1.length > 0) {
this.sensorData.npk1 = {
temperature: data.npk1[0].soiltemperature ?? 0,
moisture: data.npk1[0].soilhumidity ?? 0,
conductivity: data.npk1[0].soilconductivity ?? 0,
ph: data.npk1[0].soilph ?? 0,
nitrogen: data.npk1[0].soilnitrogen ?? 0,
phosphorus: data.npk1[0].soilphosphorus ?? 0,
potassium: data.npk1[0].soilpotassium ?? 0
};
}
if (data.npk2 && data.npk2.length > 0) {
this.sensorData.npk2 = {
temperature: data.npk2[0].soiltemperature ?? 0,
moisture: data.npk2[0].soilhumidity ?? 0,
conductivity: data.npk2[0].soilconductivity ?? 0,
ph: data.npk2[0].soilph ?? 0,
nitrogen: data.npk2[0].soilnitrogen ?? 0,
phosphorus: data.npk2[0].soilphosphorus ?? 0,
potassium: data.npk2[0].soilpotassium ?? 0
};
}
}
this.isLoaded = false;
},
(error) => {
this.isLoaded = false;
this.handleError(error);
}
);
}
loadRelayData(): void {
this.isLoaded = true;
this.hasError = false;
this.apiService.getRelayStatus().subscribe(
(response) => {
if (Array.isArray(response.data) && response.data.length > 0) {
this.relayStatus = response.data.map((relay) => ({
id: relay.id,
number: relay.number,
enabled_at: relay.enabled_at,
disabled_at: relay.disabled_at,
created_at: relay.created_at,
current_status: relay.current_status
}));
this.relayStatus.sort((a, b) => b.number - a.number);
this.noData = false;
} else {
this.handleError({ message: 'No available relay data.' });
this.noData = true;
}
this.isLoaded = false;
},
(error) => {
this.isLoaded = false;
this.handleError(error);
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 });
} else if (this.selectedButton === 'npk1') {
this.toast.error('Error fetching NPK1 sensor data. Please try again.', 'Error', { timeOut: 3000 });
} else if (this.selectedButton === 'npk2') {
this.toast.error('Error fetching NPK2 sensor data. Please try again.', 'Error', { timeOut: 3000 });
} else if (this.selectedButton === 'relay') {
this.toast.error('Error fetching relay status. Please try again.', 'Error', { timeOut: 3000 });
}
this.hasError = true;
}
}

View File

@ -0,0 +1,11 @@
<div class="container-fluid">
<div class="row">
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 background sidebar-fixed">
<app-sidebar></app-sidebar>
</div>
<div class="col py-3 content-area">
<router-outlet></router-outlet>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
.background {
background-color: #16423C;
border-radius: 0px 15px 15px 0px;
}
.sidebar-fixed {
position: fixed;
top: 0;
bottom: 0;
left: 0;
width: 16.67%;
z-index: 1;
overflow-y: auto;
}
.content-area {
z-index: 0;
margin-left: 16.67%;
overflow-y: auto;
padding-left: 1rem;
padding-right: 1rem;
height: 100vh;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LayoutsComponent } from './layouts.component';
describe('LayoutsComponent', () => {
let component: LayoutsComponent;
let fixture: ComponentFixture<LayoutsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LayoutsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LayoutsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,14 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { SidebarComponent } from './sidebar/sidebar.component';
@Component({
selector: 'app-layouts',
standalone: true,
imports: [RouterOutlet, SidebarComponent],
templateUrl: './layouts.component.html',
styleUrl: './layouts.component.scss'
})
export class LayoutsComponent {
}

View File

@ -0,0 +1,30 @@
<div class="d-flex flex-column container align-items-center align-items-sm-start px-3 pt-2 text-white min-vh-100">
<a href="/" class="d-flex align-items-center pb-3 mb-md-0 me-md-auto text-white text-decoration-none">
<span class="fs-5 d-none d-sm-inline space">Agrilink Vocpro</span>
</a>
<ul class="nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start" id="menu">
<li>
<a routerLink='/dashboard' data-bs-toggle="collapse" class="nav-link px-0 align-middle">
<i class="bi bi-graph-up title"></i> <span class="ms-1 d-none d-sm-inline menu">Dashboard</span> </a>
</li>
<li>
<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 Data</span> </a>
</li>
</ul>
<hr>
<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">
<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 -->
<span class="d-none d-sm-inline mx-1">{{ fullName }}</span>
</a>
<ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1">
<li><a class="dropdown-item" (click)="onLogout()">Sign out</a></li>
</ul>
</div>
</div>

View File

@ -0,0 +1,44 @@
.background{
background-color: #16423C;
border-radius: 0px 15px 15px 0px;
}
.container{
font-family: 'Onest', sans-serif;
}
.title{
color: white;
}
.space{
margin-bottom: 15px;
margin-top: 15px;
font-weight: 900;
}
.menu{
color: white;
font-size: 15px;
padding-left: 10px;
z-index: 9999;
}
.container-sidebar{
display: block;
}
@media (max-width: 430px) {
.dropdown-menu{
position: absolute;
z-index: 9999;
overflow: visible;
}
.dropdown-menu{
a{
font-size: 11.5px;
padding: 0px 0px 0px 10px
}
}
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SidebarComponent } from './sidebar.component';
describe('SidebarComponent', () => {
let component: SidebarComponent;
let fixture: ComponentFixture<SidebarComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SidebarComponent]
})
.compileComponents();
fixture = TestBed.createComponent(SidebarComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,34 @@
import { Component } from '@angular/core';
import { AuthService } from '../../../../cores/services/auth.service';
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']
})
export class SidebarComponent {
fullName: string | null = null;
avatar: string | null = null;
constructor(private authService: AuthService, private router: Router, private toast: ToastrService) {
this.fullName = this.authService.getUserFullName();
this.avatar = this.authService.getAvatar();
}
onLogout(): void {
this.authService.logout().subscribe(
() => {
this.router.navigate(['/auth']);
this.toast.success('Logout successful');
},
(error: any) => {
this.toast.error(error.error.message);
}
);
}
}

View File

@ -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">
<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 #barChart></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,132 @@
.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 {
position: relative;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 35px;
border: 1px solid #E5E5E5;
padding: 0px 20px 0px 20px;
background-color: white;
border-radius: 10px;
margin-top: 20px;
canvas {
position: relative;
width: 100%;
height: auto;
aspect-ratio: 2 / 1;
max-height: 300px;
}
}
.table th,
.table td {
color: #49473C;
}
.title-graph {
color: #49473C;
font-size: 20px;
font-weight: 400;
margin-top: 15px;
position: relative;
}
.graph-content {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
padding-top: 20px;
color: #49473C;
}
.spinner {
font-size: 30px;
color: #16423C;
position: absolute;
}
@media (max-width: 768px) {
.graph {
height: 300px;
padding: 10px;
}
.title-graph {
font-size: 15px;
}
.table {
font-size: 10px;
}
canvas {
max-width: 300px;
}
.active-button {
background-color: #cad849;
color: white;
}
}
@media (max-width: 344px) {
button {
color: #49473C;
margin-right: 20px;
margin-bottom: 20px;
}
.title-graph {
font-size: 15px;
}
.table {
font-size: 10px;
}
canvas {
max-width: 250px;
}
.active-button {
background-color: #cad849;
color: white;
}
}
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,152 @@
import { Component, OnInit, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
import { SensorService } from '../../../../cores/services/sensor.service';
import { ApiResponse, ParameterSensor } from '../../../../cores/interface/sensor-data';
import { CommonModule } from '@angular/common';
import { Chart } from 'chart.js';
import { max } from 'date-fns';
@Component({
selector: 'app-actualgraph',
standalone: true,
imports: [CommonModule],
templateUrl: './actualgraph.component.html',
styleUrls: ['./actualgraph.component.scss']
})
export class ActualgraphComponent implements OnInit {
@ViewChild('barChart') barChart!: ElementRef;
selectedButton: string = 'npk1';
greeting = '';
chart: any;
actualDataValues = { nitrogen: 0, phosphorus: 0, kalium: 0 };
standardDataValues = {
nitrogen: { min: 100, max: 200 },
phosphorus: { min: 90, max: 125 },
kalium: { min: 220, max: 240 }
};
constructor(
private sensorService: SensorService,
private cdr: ChangeDetectorRef
) {}
ngOnInit(): void {
this.updateGreeting();
this.loadData('npk1');
}
ngAfterViewInit(): void {
this.initChart();
}
initChart(): void {
const minStandard = [
this.standardDataValues.nitrogen.min,
this.standardDataValues.phosphorus.min,
this.standardDataValues.kalium.min
];
const maxStandard = [
this.standardDataValues.nitrogen.max,
this.standardDataValues.phosphorus.max,
this.standardDataValues.kalium.max
];
this.chart = new Chart(this.barChart.nativeElement, {
type: 'bar',
data: {
labels: ['Nitrogen', 'Phosphorus', 'Kalium'],
datasets: [
{
label: 'Min Standard',
backgroundColor: 'rgba(0, 0, 0, 0)',
data: minStandard,
borderWidth: 0
},
{
label: 'Max Standard',
backgroundColor: 'rgba(212, 231, 197)',
data: maxStandard,
borderWidth: 1
},
{
label: 'Actual Data',
type: 'scatter',
backgroundColor: 'rgba(18, 55, 42)',
borderColor: 'rgba(18, 55, 10)',
pointRadius: 5,
pointHoverRadius: 7,
data: [
this.actualDataValues.nitrogen,
this.actualDataValues.phosphorus,
this.actualDataValues.kalium
],
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { stacked: true },
y: {
stacked: true,
beginAtZero: false,
ticks: {
callback: (value: string | number, index: number, values: any) => {
return `${value} mg/L`;
}
}
}
}
}
});
}
loadData(sensorType: string): void {
this.selectedButton = sensorType;
this.sensorService.getLatestData().subscribe(
(response: ApiResponse) => {
if (response.statusCode === 200) {
const selectedData = (response.data as { [key: string]: any[] })[sensorType];
if (selectedData && selectedData.length > 0) {
const npkData: ParameterSensor = selectedData[0];
this.actualDataValues = {
nitrogen: npkData.soilnitrogen ?? 0,
phosphorus: npkData.soilphosphorus ?? 0,
kalium: npkData.soilpotassium ?? 0
};
this.updateChart();
} else {
}
} else {
console.error('Error loading data: ', response.message);
}
},
(error) => {
console.error('Error loading data:', error);
this.cdr.detectChanges();
}
);
}
updateChart(): void {
if (this.chart) {
this.chart.data.datasets[2].data = [
{ x: 'Nitrogen', y: this.actualDataValues.nitrogen },
{ x: 'Phosphorus', y: this.actualDataValues.phosphorus },
{ x: 'Kalium', y: this.actualDataValues.kalium }
];
this.chart.update();
}
}
updateGreeting(): void {
const hour = new Date().getHours();
this.greeting = hour < 12 ? 'Good Morning' : hour < 18 ? 'Good Afternoon' : 'Good Evening';
}
}

View File

@ -0,0 +1,93 @@
<div class="container-graph">
<ng-container *ngIf="allNoData; else graphContent">
<div class="no-data">No available data</div>
</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 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">
<div class="d-flex align-items-center" style="padding: 50px 0px 50px 0px">
<i class="fa fa-spinner fa-spin"></i>
</div>
</ng-container>
<ng-template #dhtData>
<canvas #myChartDHT id="myChartDHT" width="600" height="300" style="height: 300px;"></canvas>
<p *ngIf="isNoDataDHT" class="no-data">No available data</p>
</ng-template>
</div>
<div class="sensor-wrapper">
<div class="title-with-dropdown">
<div class="title">Sensor NPK1</div>
<select class="form-select" style="margin-top: 10px" [(ngModel)]="selectedNPK1" (change)="onResize()">
<option value="npk">NPK</option>
<option value="temperature">Temperatur Tanah</option>
<option value="humidity">Kelembaban Tanah</option>
<option value="ph">PH Tanah</option>
<option value="conductivity">Conductivity</option>
</select>
</div>
<ng-container *ngIf="isLoadingNPK1; else npk1Data">
<div class="d-flex align-items-center" style="padding: 50px 0px 50px 0px">
<i class="fa fa-spinner fa-spin"></i>
</div>
</ng-container>
<ng-template #npk1Data>
<canvas #myChartNPK1 id="myChartNPK1"></canvas>
<p *ngIf="isNoDataNPK1" class="no-data">No available data</p>
</ng-template>
</div>
<div class="sensor-wrapper">
<div class="title-with-dropdown">
<div class="title">Sensor NPK2</div>
<select class="form-select" style="margin-top: 10px" [(ngModel)]="selectedNPK2" (change)=" onResize()">
<option value="npk">NPK</option>
<option value="temperature">Temperatur Tanah</option>
<option value="humidity">Kelembaban Tanah</option>
<option value="ph">PH Tanah</option>
<option value="conductivity">Conductivity</option>
</select>
</div>
<ng-container *ngIf="isLoadingNPK2; else npk2Data">
<div class="d-flex align-items-center" style="padding: 50px 0px 50px 0px;">
<i class="fa fa-spinner fa-spin"></i>
</div>
</ng-container>
<ng-template #npk2Data>
<canvas #myChartNPK2 id="myChartNPK2"></canvas>
<p *ngIf="isNoDataNPK2" class="no-data">No available data</p>
</ng-template>
</div>
</ng-template>
</div>

View File

@ -0,0 +1,178 @@
.container-graph {
display: flex;
flex-direction: column;
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;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 35px;
border: 1px solid #E5E5E5;
padding: 0px 20px 0px 20px;
background-color: white;
border-radius: 10px;
h2 {
font-size: 18px;
margin-bottom: 10px;
}
canvas {
position: relative;
width: 100%;
height: auto;
aspect-ratio: 2 / 1;
max-height: 300px;
}
}
.title-with-dropdown {
display: flex;
align-items: center;
justify-content: center;
width: 50%;
.title {
flex: 0 0 25%;
text-align: center;
}
.form-select {
flex: 0 0 25%;
margin: 18px 0px 0px 15px;
}
}
}
.title {
text-align: center;
font-size: 20px;
margin: 18px 0px 18px 0px;
}
@media (max-width: 768px) {
canvas {
height: auto;
width: 100%;
max-width: 100%;
}
.date-picker{
display: flex;
justify-content: center;
padding: 10px 20px;
}
mat-hint{
font-size: 10px;
align-items: center;
}
.button-param {
flex-direction: column;
align-items: center;
}
button {
width: 80%;
max-width: 300px;
text-align: center;
}
}
@media (max-width: 344px) {
canvas {
height: auto;
width: 100%;
max-width: 100%;
}
.date-picker{
display: flex;
justify-content: center;
padding: 10px 20px;
}
mat-hint{
font-size: 10px;
align-items: center;
}
.button-param {
flex-direction: column;
align-items: center;
}
button {
width: 80%;
max-width: 300px;
text-align: center;
}
}
.loading {
font-size: 18px;
text-align: center;
}
.spinner {
color: #16423C;
}
.no-data {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
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

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { GraphComponent } from './graph.component';
describe('GraphComponent', () => {
let component: GraphComponent;
let fixture: ComponentFixture<GraphComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [GraphComponent]
})
.compileComponents();
fixture = TestBed.createComponent(GraphComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,590 @@
import { Component, OnInit, ElementRef, ViewChild, AfterViewInit, OnDestroy, OnChanges, Input, SimpleChanges, ChangeDetectorRef } from '@angular/core';
import { Chart, registerables, Tooltip } from 'chart.js';
import { SensorService } from '../../../../cores/services/sensor.service';
import { ApiResponse, ParameterSensor } from '../../../../cores/interface/sensor-data';
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);
const parameterColors: { [key: string]: string } = {
vicitemperature: '#8F5A62',
vicihumidity: '#16423C',
viciluminosity: '#DF9B55',
soiltemperature: '#8c2d1c',
soilhumidity: '#3d7b8f',
soilconductivity: '#f2b8b8',
soilph: '#459645',
soilnitrogen: '#fece48',
soilphosphorus: '#B80F0A',
soilpotassium: '#4c1f74',
};
@Component({
selector: 'app-graph',
standalone: true,
imports: [CommonModule, FormsModule, MatDatepickerModule, MatFormFieldModule],
providers: [provideNativeDateAdapter()],
templateUrl: './graph.component.html',
styleUrls: ['./graph.component.scss']
})
export class GraphComponent implements OnInit, AfterViewInit, OnDestroy, OnChanges{
@ViewChild('myChartDHT', { static: false }) dhtChartElement!: ElementRef<HTMLCanvasElement>;
@ViewChild('myChartNPK1', { static: false }) npk1ChartElement!: ElementRef<HTMLCanvasElement>;
@ViewChild('myChartNPK2', { static: false }) npk2ChartElement!: ElementRef<HTMLCanvasElement>;
@Input() interval: string = 'hourly';
selectedInterval: string = 'HOURLY';
startDate: Date | null = null;
endDate: Date | null = null;
isLoadingDHT: boolean = true;
isLoadingNPK1: boolean = true;
isLoadingNPK2: boolean = true;
isNoDataDHT: boolean = false;
isNoDataNPK1: boolean = false;
isNoDataNPK2: boolean = false;
allNoData: boolean = false;
activeButton: string = 'vicitemperature';
sensorParameters: { [key: string]: string[] } = {
dht: ['vicitemperature', 'vicihumidity', 'viciluminosity'],
npk1: ['soiltemperature', 'soilhumidity', 'soilconductivity', 'soilph', 'soilnitrogen', 'soilphosphorus', 'soilpotassium'],
npk2: ['soiltemperature', 'soilhumidity', 'soilconductivity', 'soilph', 'soilnitrogen', 'soilphosphorus', 'soilpotassium'],
};
parameterDisplayNames: { [key: string]: string } = {
vicitemperature: 'Temperatur Udara (°C)',
vicihumidity: 'Kelembaban Udara (%)',
viciluminosity: 'Intensitas Cahaya (lux)',
soiltemperature: 'Temperatur Tanah (°C)',
soilhumidity: 'Kelembaban Tanah (%)',
soilconductivity: 'Conductivity (μS/cm)',
soilph: 'pH',
soilnitrogen: 'Nitrogen (mg/l)',
soilphosphorus: 'Phosphorus (mg/l)',
soilpotassium: 'Kalium (mg/l)',
};
charts: { [key: string]: Chart | undefined } = {
dht: undefined,
npk1: undefined,
npk2: undefined,
};
selectedNPK1: string = 'npk';
selectedNPK2: string = 'npk';
private resizeListener!: () => void;
private updateTimeout: any;
constructor(private sensorService: SensorService, private toast: ToastrService) {}
ngOnInit(): void {
this.resizeListener = this.onResize.bind(this);
window.addEventListener('resize', this.resizeListener);
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['interval'] && changes['interval'].previousValue !== changes['interval'].currentValue) {
this.selectedInterval = changes['interval'].currentValue;
}
}
ngAfterViewInit(): void {
setTimeout(() => {
this.updateCharts();
}, 0);
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);
}
onResize(): void {
Object.values(this.charts).forEach(chart => {
if (chart) {
chart.destroy();
}
});
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');
const hasil = `${year}-${month}-${day}`;
return hasil;
}
getDateAgo():string{
const today = new Date();
today.setDate(today.getDate() - 7);
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}`;
}
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') {
this.sensorService.getSensorDataHourly('dht', '', hStart, hEnd, 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;
}
},
error: () => {
this.isLoadingDHT = false;
this.isNoDataDHT = true;
}
});
} else if (timeRange === 'DAILY') {
this.sensorService.getSensorDataDaily('dht', 'npk', dStart, dEnd, 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;
}
},
error: () => {
this.isLoadingDHT = false;
this.isNoDataDHT = true;
}
});
}
}
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, hStart, hEnd, timeRange).subscribe({
next: (response) => {
this.isLoadingNPK1 = false;
if (response.statusCode === 200 && response.data.npk1?.length > 0) {
this.createChart(this.npk1ChartElement.nativeElement, response, 'npk1', this.selectedNPK1);
this.isNoDataNPK1 = false;
} else {
this.isNoDataNPK1 = true;
}
},
error: () => {
this.isLoadingNPK1 = false;
this.isNoDataNPK1 = true;
}
});
}else if(timeRange === 'DAILY'){
this.sensorService.getSensorDataDaily('npk1', this.selectedNPK1, dStart, dEnd, timeRange).subscribe({
next: (response) => {
this.isLoadingNPK1 = false;
if (response.statusCode === 200 && response.data.npk1?.length > 0) {
this.createChart(this.npk1ChartElement.nativeElement, response, 'npk1', this.selectedNPK1);
this.isNoDataNPK1 = false;
} else {
this.isNoDataNPK1 = true;
}
},
error: () => {
this.isLoadingNPK1 = false;
this.isNoDataNPK1 = true;
}
});
}
}
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) {
this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2);
this.isNoDataNPK2 = false;
} else {
this.isNoDataNPK2 = true;
}
},
error: () => {
this.isLoadingNPK2 = false;
this.isNoDataNPK2 = true;
}
});
} else if(savedTimeRange === 'DAILY'){
this.sensorService.getSensorDataDaily('npk2', this.selectedNPK2, dStart, dEnd, savedTimeRange).subscribe({
next: (response) => {
this.isLoadingNPK2 = false;
if (response.statusCode === 200 && response.data.npk2?.length > 0) {
this.createChart(this.npk2ChartElement.nativeElement, response, 'npk2', this.selectedNPK2);
this.isNoDataNPK2 = false;
} else {
this.isNoDataNPK2 = true;
}
},
error: () => {
this.isLoadingNPK2 = false;
this.isNoDataNPK2 = true;
}
});
}
}
updateCharts(): void {
const interval = this.selectedInterval;
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;
}
});
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);
}
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();
}
}
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;
}
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`;
const pointRadius = data.length === 1 ? 5 : 0;
const pointHoverRadius = data.length === 1 ? 7 : 0;
const isHidden = parameter !== this.activeButton;
return {
label: displayName,
data,
borderColor,
borderWidth: 1.5,
fill: true,
backgroundColor,
tension: 0.5,
pointRadius,
pointHoverRadius,
hidden : isHidden
};
}).filter(dataset => dataset !== null);
} else {
datasets = parameters
.filter(parameter => {
if (selectedOption === 'npk') {
return ['soilphosphorus', 'soilnitrogen', 'soilpotassium'].includes(parameter);
} else if (selectedOption === 'temperature') {
return ['soiltemperature'].includes(parameter);
} else if (selectedOption === 'humidity') {
return ['soilhumidity'].includes(parameter);
} else if (selectedOption === 'conductivity') {
return ['soilconductivity'].includes(parameter);
} else if (selectedOption === 'ph') {
return ['soilph'].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;
}
if (this.charts[sensor]) {
this.charts[sensor]?.destroy();
}
if(sensor === 'dht'){
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: false,
},
},
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 (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;
}
},
},
}
}
});
}
}
getDataFromResponse(response: ApiResponse, sensor: string, parameter: string): { data: number[], labels: string[] } {
const sensorData = response.data[sensor as keyof typeof response.data];
const data: number[] = [];
const labels: string[] = [];
if (sensorData) {
sensorData.forEach(item => {
data.push(item[parameter as keyof ParameterSensor] ?? 0);
if(this.interval === 'HOURLY') {
labels.push(item.date);
labels.push(item.hour);
}
else if (this.interval === 'DAILY') {
labels.push(item.day);
}
});
} else {
console.error('No data found for sensor:', sensor);
}
return { data, labels };
}
getLabels(response: ApiResponse, sensor: string): string[] {
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 `${formatDate}, ${item.hour}.00`;
} else if (this.interval === 'DAILY') {
const day = item.date;
const convertDateToDay = this.convertDateToDay(day);
return `${convertDateToDay}, ${formatDate}`;
} else {
return '';
}
});
}
convertDateToDay(dateString: string): string {
const date = new Date(dateString);
const days = ['Sun', 'Mon', 'Tues', 'Wed', 'Thrus', 'Fri', 'Sat'];
return days[date.getDay()];
}
};

View File

@ -0,0 +1,14 @@
<div class="container">
<div>
<h1 class="title">{{ greeting }}</h1>
<h3 class="description">View your historical data with customizable time ranges</h3>
</div>
<div>
<button (click)="updateInterval('HOURLY')" [class.active-button]="selectedButton === 'hourly'">Hourly</button>
<button (click)="updateInterval('DAILY')" [class.active-button]="selectedButton === 'daily'">Daily</button>
</div>
<div class="graph">
<app-graph [interval]="selectedInterval"></app-graph>
</div>
</div>

View File

@ -0,0 +1,134 @@
.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;
}
.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 (min-width: 344px) {
.card-parameter{
flex: 1 1 100%;
}
button {
color: #49473C;
margin-right: 20px;
margin-bottom: 20px;
}
}
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
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HistorygraphComponent } from './historygraph.component';
describe('HistorygraphComponent', () => {
let component: HistorygraphComponent;
let fixture: ComponentFixture<HistorygraphComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HistorygraphComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HistorygraphComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,61 @@
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 = '';
intervalId: any;
greeting: string = '';
constructor(private cdr: ChangeDetectorRef) {}
@ViewChild(GraphComponent) graphComponent!: GraphComponent;
ngOnInit(): void {
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);
}
}
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';
}
}
}

View File

@ -0,0 +1,64 @@
<div class="container-fluid ps-md-0">
<div class="row g-0">
<div class="d-none d-md-flex col-md-4 col-lg-6 bg-image"></div>
<div class="col-md-8 col-lg-6">
<div class="login d-flex align-items-center py-5">
<div class="container">
<div class="row">
<div class="col-md-9 col-lg-8 mx-auto">
<h3 class="login-heading mb-4">Create an Account!</h3>
<form (ngSubmit)="onSubmit()">
<div class="form-floating mb-3">
<input type="text" class="form-control" [(ngModel)]="fullname" name="fullname" id="floatingFullname" placeholder="Full Name" required autocomplete="off">
<label for="floatingFullname">Full Name</label>
</div>
<div class="form-floating mb-3">
<input type="text" class="form-control" [(ngModel)]="username" name="username" id="floatingUsername" placeholder="Username" required autocomplete="off">
<label for="floatingUsername">Username</label>
</div>
<div class="form-floating mb-3">
<input type="email" class="form-control" [(ngModel)]="email" name="email" id="floatingInput" placeholder="name@example.com" required autocomplete="off">
<label for="floatingInput">Email address</label>
</div>
<div class="form-floating mb-3">
<input [type]="passwordVisible ? 'text' : 'password'"
class="form-control"
[(ngModel)]="password"
name="password"
id="floatingPassword"
placeholder="Password"
required autocomplete="off">
<label for="floatingPassword">Password</label>
<button type="button"
class="btn btn-link position-absolute end-0 me-3"
(click)="togglePasswordVisibility()">
<i class="fa" [ngClass]="passwordVisible ? 'fa-eye' : 'fa-eye-slash'"></i>
</button>
</div>
<div class="d-grid">
<button class="btn btn-lg color-btn btn-login text-uppercase fw-bold mb-2"
type="submit"
[disabled]="loading">
<span *ngIf="!loading">Sign up</span>
<span *ngIf="loading">
<i class="fa fa-spinner fa-spin"></i>
</span>
</button>
<div class="text-center">
<a class="small forgot" routerLink='/auth'>Sign In here</a>
</div>
</div>
</form>
</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>

View File

@ -0,0 +1,60 @@
.login {
min-height: 100vh;
font-family: "Onest", sans-serif;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.bg-image {
background-image: url('../../../assets/images/bg-auth.jpg');
background-size: cover;
background-position: right;
height: 100vh;
border-radius: 0px 10px 10px 0px;
}
.login-heading {
font-weight: 300;
}
.btn-login {
font-size: 0.9rem;
letter-spacing: 0.05rem;
padding: 0.75rem 1rem;
}
.color-btn{
background-color: #16423C;
color: white;
}
.forgot{
color: #16423C;
}
.form-floating {
position: relative;
.btn-link {
position: absolute;
top: 50%;
right: 10px;
transform: translateY(-50%);
border: none;
background: transparent;
cursor: pointer;
color: #16423C;
&:hover {
color: #16423C;
}
}
}
.text-center-copyright {
text-align: center;
font-size: 0.85rem;
color: #636363;
padding: 15px 0;
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RegisterComponent } from './register.component';
describe('RegisterComponent', () => {
let component: RegisterComponent;
let fixture: ComponentFixture<RegisterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RegisterComponent]
})
.compileComponents();
fixture = TestBed.createComponent(RegisterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,77 @@
import { Component, OnInit } from '@angular/core'; // Import OnInit
import { Router, RouterModule } from '@angular/router';
import { AuthService } from '../../cores/services/auth.service';
import { FormsModule } from '@angular/forms';
import { RegistrationData } from '../../cores/interface/auth';
import { ToastrService } from 'ngx-toastr';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-register',
standalone: true,
imports: [FormsModule, RouterModule, CommonModule],
templateUrl: './register.component.html',
styleUrls: ['./register.component.scss']
})
export class RegisterComponent implements OnInit {
username: string = '';
password: string = '';
email: string = '';
fullname: string = '';
loading: boolean = false;
passwordVisible: boolean = false;
constructor(private authService: AuthService, private router: Router, private toast: ToastrService) {}
ngOnInit(): void {
this.username = '';
this.password = '';
this.email = '';
this.fullname = '';
}
togglePasswordVisibility() {
this.passwordVisible = !this.passwordVisible;
}
onSubmit() {
this.loading = true;
if (!this.username || !this.password || !this.email || !this.fullname) {
this.loading = false;
this.toast.error('Please fill in all fields.');
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 = {
username: this.username,
pwd: this.password,
email: this.email,
google_id: '1',
fullname: this.fullname,
avatar: ''
};
this.authService.register(registrationData).subscribe(
(response) => {
this.loading = false;
this.toast.success('Registration successful');
this.router.navigate(['/auth']);
},
(error) => {
this.loading = false;
this.toast.error(error.error.message);
}
);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AgrilinkVocpro</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Onest:wght@400;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy" crossorigin="anonymous"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
<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>
</body>
</html>

View File

@ -0,0 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View File

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

View File

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts"
],
"include": [
"src/**/*.d.ts"
]
}

View File

@ -0,0 +1,33 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"isolatedModules": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@ -0,0 +1,15 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}