Compare commits
54 Commits
fbba089059
...
a649caccc0
| Author | SHA1 | Date | |
|---|---|---|---|
| a649caccc0 | |||
|
|
a4ab48ab20 | ||
|
|
98ca03b55e | ||
|
|
944a9fa5cb | ||
|
|
4377bb493d | ||
|
|
7c44188d6e | ||
|
|
a5fbb018c4 | ||
|
|
d6e7c7696c | ||
|
|
ae43ca880c | ||
|
|
cfbc25fd66 | ||
|
|
da65fdf819 | ||
|
|
703c522927 | ||
|
|
a5318426f4 | ||
|
|
bac7a0a36c | ||
|
|
c405a9d815 | ||
|
|
9ed5499042 | ||
|
|
e770c97f91 | ||
|
|
40b0e21e18 | ||
|
|
47b90ec2ca | ||
|
|
10dff91778 | ||
|
|
5ffd7fc241 | ||
|
|
a68c0f045b | ||
|
|
6ad5951413 | ||
|
|
2c6123c54c | ||
|
|
50c3ce7497 | ||
|
|
cff1e79837 | ||
|
|
4bb2c107d3 | ||
|
|
2864560c56 | ||
|
|
948899a5ef | ||
|
|
a2befa2950 | ||
|
|
cc4f40b05e | ||
|
|
50bd5c0ebf | ||
|
|
8e0de242b0 | ||
|
|
e0b74fd135 | ||
|
|
ee463ec174 | ||
|
|
fac32f6978 | ||
|
|
11698f0abb | ||
|
|
879151ea6c | ||
|
|
879ad3dcde | ||
|
|
141fc9bb54 | ||
|
|
581ca5f35c | ||
|
|
59e89aee72 | ||
|
|
2c7a2a4a01 | ||
|
|
d1d0c5498a | ||
|
|
6aab9e1b71 | ||
|
|
e478095235 | ||
|
|
991da7be77 | ||
|
|
cd0e9e3e85 | ||
|
|
f6d3ef022c | ||
|
|
1b99366200 | ||
|
|
30a4eae0d3 | ||
|
|
4a77bab8f2 | ||
|
|
a3412a36d5 | ||
|
|
78b48ce344 |
136
README.md
136
README.md
|
|
@ -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.
|
|
||||||
|
|
|
||||||
16
agrilink_vocpro/.editorconfig
Normal file
16
agrilink_vocpro/.editorconfig
Normal 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
42
agrilink_vocpro/.gitignore
vendored
Normal 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
|
||||||
4
agrilink_vocpro/.vscode/extensions.json
vendored
Normal file
4
agrilink_vocpro/.vscode/extensions.json
vendored
Normal 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
20
agrilink_vocpro/.vscode/launch.json
vendored
Normal 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
42
agrilink_vocpro/.vscode/tasks.json
vendored
Normal 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
27
agrilink_vocpro/README.md
Normal 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.
|
||||||
115
agrilink_vocpro/angular.json
Normal file
115
agrilink_vocpro/angular.json
Normal 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
15440
agrilink_vocpro/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
47
agrilink_vocpro/package.json
Normal file
47
agrilink_vocpro/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
agrilink_vocpro/public/favicon.ico
Normal file
BIN
agrilink_vocpro/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
agrilink_vocpro/src/app/app.component.html
Normal file
1
agrilink_vocpro/src/app/app.component.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<router-outlet></router-outlet>
|
||||||
0
agrilink_vocpro/src/app/app.component.scss
Normal file
0
agrilink_vocpro/src/app/app.component.scss
Normal file
29
agrilink_vocpro/src/app/app.component.spec.ts
Normal file
29
agrilink_vocpro/src/app/app.component.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
13
agrilink_vocpro/src/app/app.component.ts
Normal file
13
agrilink_vocpro/src/app/app.component.ts
Normal 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';
|
||||||
|
}
|
||||||
18
agrilink_vocpro/src/app/app.config.ts
Normal file
18
agrilink_vocpro/src/app/app.config.ts
Normal 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
|
||||||
|
]
|
||||||
|
};
|
||||||
45
agrilink_vocpro/src/app/app.routes.ts
Normal file
45
agrilink_vocpro/src/app/app.routes.ts
Normal 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]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
25
agrilink_vocpro/src/app/cores/guards/auth.guard.ts
Normal file
25
agrilink_vocpro/src/app/cores/guards/auth.guard.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
14
agrilink_vocpro/src/app/cores/interface/auth.ts
Normal file
14
agrilink_vocpro/src/app/cores/interface/auth.ts
Normal 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;
|
||||||
|
}
|
||||||
56
agrilink_vocpro/src/app/cores/interface/sensor-data.ts
Normal file
56
agrilink_vocpro/src/app/cores/interface/sensor-data.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
11
agrilink_vocpro/src/app/cores/services/api.service.ts
Normal file
11
agrilink_vocpro/src/app/cores/services/api.service.ts
Normal 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) {}
|
||||||
|
}
|
||||||
77
agrilink_vocpro/src/app/cores/services/auth.service.ts
Normal file
77
agrilink_vocpro/src/app/cores/services/auth.service.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
agrilink_vocpro/src/app/cores/services/sensor.service.ts
Normal file
87
agrilink_vocpro/src/app/cores/services/sensor.service.ts
Normal 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);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
agrilink_vocpro/src/app/cores/services/storage.service.ts
Normal file
44
agrilink_vocpro/src/app/cores/services/storage.service.ts
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
71
agrilink_vocpro/src/app/pages/auth/auth.component.html
Normal file
71
agrilink_vocpro/src/app/pages/auth/auth.component.html
Normal 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>©2024 Agrilink</div>
|
||||||
|
<div>Powered by <strong>Politeknik Negeri Malang</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
77
agrilink_vocpro/src/app/pages/auth/auth.component.scss
Normal file
77
agrilink_vocpro/src/app/pages/auth/auth.component.scss
Normal 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;
|
||||||
|
}
|
||||||
23
agrilink_vocpro/src/app/pages/auth/auth.component.spec.ts
Normal file
23
agrilink_vocpro/src/app/pages/auth/auth.component.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
67
agrilink_vocpro/src/app/pages/auth/auth.component.ts
Normal file
67
agrilink_vocpro/src/app/pages/auth/auth.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
184
agrilink_vocpro/src/app/pages/dashboard/dashboard.component.html
Normal file
184
agrilink_vocpro/src/app/pages/dashboard/dashboard.component.html
Normal 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>
|
||||||
|
|
||||||
157
agrilink_vocpro/src/app/pages/dashboard/dashboard.component.scss
Normal file
157
agrilink_vocpro/src/app/pages/dashboard/dashboard.component.scss
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
166
agrilink_vocpro/src/app/pages/dashboard/dashboard.component.ts
Normal file
166
agrilink_vocpro/src/app/pages/dashboard/dashboard.component.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ActualgraphComponent } from './actualgraph.component';
|
||||||
|
|
||||||
|
describe('ActualgraphComponent', () => {
|
||||||
|
let component: ActualgraphComponent;
|
||||||
|
let fixture: ComponentFixture<ActualgraphComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ActualgraphComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ActualgraphComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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()];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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>©2024 Agrilink</div>
|
||||||
|
<div>Powered by <strong>Politeknik Negeri Malang</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
77
agrilink_vocpro/src/app/pages/register/register.component.ts
Normal file
77
agrilink_vocpro/src/app/pages/register/register.component.ts
Normal 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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
agrilink_vocpro/src/assets/images/auth.png
Normal file
BIN
agrilink_vocpro/src/assets/images/auth.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
BIN
agrilink_vocpro/src/assets/images/bg-auth.jpg
Normal file
BIN
agrilink_vocpro/src/assets/images/bg-auth.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 724 KiB |
BIN
agrilink_vocpro/src/assets/images/profile.png
Normal file
BIN
agrilink_vocpro/src/assets/images/profile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
26
agrilink_vocpro/src/index.html
Normal file
26
agrilink_vocpro/src/index.html
Normal 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>
|
||||||
6
agrilink_vocpro/src/main.ts
Normal file
6
agrilink_vocpro/src/main.ts
Normal 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));
|
||||||
30
agrilink_vocpro/src/styles.scss
Normal file
30
agrilink_vocpro/src/styles.scss
Normal 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;
|
||||||
|
}
|
||||||
15
agrilink_vocpro/tsconfig.app.json
Normal file
15
agrilink_vocpro/tsconfig.app.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
33
agrilink_vocpro/tsconfig.json
Normal file
33
agrilink_vocpro/tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
15
agrilink_vocpro/tsconfig.spec.json
Normal file
15
agrilink_vocpro/tsconfig.spec.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user