Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Dashboard] Crop Probability Module #232

Merged
merged 40 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
02415ac
chore: app build
FaithDaka Jan 18, 2024
dffdebd
feat: crop-information module
FaithDaka Jan 18, 2024
e948d5e
feat: sidebar link and routing
FaithDaka Jan 18, 2024
520a325
feat: v1 crop information dashboard module
FaithDaka Jan 18, 2024
3a5405a
Merge branch 'main' of https://github.com/e-picsa/picsa-apps into ft-…
FaithDaka Jan 31, 2024
c3005ea
chore: updating file structure
FaithDaka Jan 31, 2024
9c84a81
Merge branch 'main' of https://github.com/e-picsa/picsa-apps into ft-…
FaithDaka Feb 12, 2024
78e64c6
fix: file paths
FaithDaka Feb 12, 2024
45ab6d2
server: station crop information table
FaithDaka Feb 12, 2024
3c10703
server: station data table
FaithDaka Feb 12, 2024
fcbd239
server: crop data table
FaithDaka Feb 12, 2024
240c986
types: crop probability type setting
FaithDaka Feb 12, 2024
0c5d05b
feat: crop probability dashboard module
FaithDaka Feb 12, 2024
1dd0c7b
fix: update crop probability module routes
FaithDaka Feb 13, 2024
f5ed638
feat: crop probability dashboard module
FaithDaka Feb 13, 2024
de8d379
feat: new crop probability entry module
FaithDaka Feb 13, 2024
013917e
fix: schema typo
chrismclarke Feb 15, 2024
c61195b
refactor: crop_data table columns
chrismclarke Feb 15, 2024
7d7a057
chore: remove station_data_table
chrismclarke Feb 15, 2024
353da7e
refactor: station_crop_data table
chrismclarke Feb 15, 2024
b4a6fc9
chore: export updated types
chrismclarke Feb 15, 2024
06b9423
chore: rename routes
chrismclarke Feb 15, 2024
245fc71
chore: update table links
chrismclarke Feb 15, 2024
1ad4f4f
chore: code tidying
chrismclarke Feb 15, 2024
f7b42e5
Merge branch 'main' into ft-dashboard-crop-information
chrismclarke Feb 15, 2024
5f2d869
Merge branch 'main' of https://github.com/e-picsa/picsa-apps into ft-…
FaithDaka Mar 1, 2024
b6157aa
fix: Merge conflicts
FaithDaka Mar 1, 2024
c7fafc7
fix: resolve merge conflicts
FaithDaka Mar 1, 2024
b3ca16c
fix: update links to match
FaithDaka Mar 1, 2024
a45618d
fix: updated table functions
FaithDaka Mar 10, 2024
71b70d1
refactor: use picsa data table service
FaithDaka Mar 10, 2024
4ae3ac8
refactor: entry component uses form builder
FaithDaka Mar 10, 2024
6f9488e
file formatted
FaithDaka Mar 10, 2024
0cfed3e
feat: entry input validators
FaithDaka Mar 10, 2024
6b0e503
fix: import paths
FaithDaka Mar 10, 2024
a00447d
chore: navigate back after crop add
chrismclarke Mar 11, 2024
3ff5559
chore: add form types
chrismclarke Mar 12, 2024
30f377f
chore: code tidying
chrismclarke Mar 12, 2024
283070e
feat: crop info edit
chrismclarke Mar 12, 2024
e75b174
feat: enable shared form components in dashboard
chrismclarke Mar 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/picsa-apps/dashboard/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@
"polyfills": ["zone.js"],
"tsConfig": "apps/picsa-apps/dashboard/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["apps/picsa-apps/dashboard/src/favicon.ico", "apps/picsa-apps/dashboard/src/assets"],
"assets": [
"apps/picsa-apps/dashboard/src/favicon.ico",
"apps/picsa-apps/dashboard/src/assets",
{ "glob": "*.svg", "input": "libs/data/crop_activity/svgs", "output": "assets/svgs/crop_activity" },
{ "glob": "*.svg", "input": "libs/data/crops/svgs", "output": "assets/svgs/crops" },
{ "glob": "*.svg", "input": "libs/data/weather/svgs", "output": "assets/svgs/weather" }
],
"styles": ["apps/picsa-apps/dashboard/src/styles.scss", "node_modules/leaflet/dist/leaflet.css"],
"stylePreprocessorOptions": {
"includePaths": ["libs/theme/src"]
Expand Down
14 changes: 12 additions & 2 deletions apps/picsa-apps/dashboard/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter } from '@angular/router';
import { PicsaFormsModule } from '@picsa/forms';
import { PicsaTranslateModule } from '@picsa/shared/modules';

import { appRoutes } from './app.routes';

export const appConfig: ApplicationConfig = {
providers: [provideRouter(appRoutes), provideAnimations()],
providers: [
provideRouter(appRoutes),
provideAnimations(),
provideHttpClient(),
// Enable picsa forms and (global) translate module for lazy-loaded standalone components
// https://angular.io/guide/standalone-components#configuring-dependency-injection
importProvidersFrom(PicsaFormsModule, PicsaTranslateModule.forRoot()),
],
};
5 changes: 5 additions & 0 deletions apps/picsa-apps/dashboard/src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export const appRoutes: Route[] = [
},

// unmatched routes fallback to home
{
path: 'crop-information',
loadChildren: () =>
import('./modules/crop-information/crop-information.module').then((m) => m.CropInformationModule),
},
{
path: 'monitoring',
loadChildren: () => import('./modules/monitoring/monitoring-forms.module').then((m) => m.MonitoringFormsPageModule),
Expand Down
9 changes: 5 additions & 4 deletions apps/picsa-apps/dashboard/src/app/data/navLinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ export const DASHBOARD_NAV_LINKS = [
},
],
},
// {
// label: 'Crop Information',
// href: '/crop-information',
// },
{
label: 'Crop Information',
href: '/crop-information',
matIcon: 'spa',
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(+) Impressed you found an icon that looks like a crop, so weird mat-icons call it the spa icon...

{
label: 'Monitoring Forms',
href: '/monitoring',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="page-content">
<div style="display: flex; align-items: center">
<h2 style="flex: 1">Crop Probabilities</h2>
<button mat-stroked-button color="primary" routerLink="entry"><mat-icon>add</mat-icon>Add New Entry</button>
</div>
@if(service.cropProbabilities){
<picsa-data-table [data]="service.cropProbabilities" [options]="tableOptions"></picsa-data-table>
}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { CropInformationPageComponent } from './crop-information.page';
import { NewEntryPageComponent } from './pages/new_entry/new_entry.page';

@NgModule({
declarations: [],
imports: [
CommonModule,
RouterModule.forChild([
{
path: '',
component: CropInformationPageComponent,
},
// new entry
{
path: 'entry',
component: NewEntryPageComponent,
},
// editable entry
{
path: ':id',
component: NewEntryPageComponent,
},
]),
],
})
export class CropInformationModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';

import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { PicsaDataTableComponent } from '@picsa/shared/features';
import { PicsaNotificationService } from '@picsa/shared/services/core/notification.service';

import { DashboardMaterialModule } from '../../material.module';
import { CropProbabilityDashboardService, ICropInformationRow } from './crop-information.service';

@Component({
selector: 'dashboard-resources-page',
standalone: true,
imports: [CommonModule, DashboardMaterialModule, PicsaDataTableComponent, RouterModule],
templateUrl: '../crop-information/crop-information.component.html',
styleUrls: ['../crop-information/crop-information.component.scss'],
})
export class CropInformationPageComponent implements OnInit {
constructor(
public service: CropProbabilityDashboardService,
private notificationService: PicsaNotificationService,
private route: ActivatedRoute,
private router: Router
) {}

displayedColumns: string[] = [
'crop',
'variety',
'water_lower',
'water_upper',
'length_lower',
'length_upper',
'label',
];

tableOptions = {
displayColumns: this.displayedColumns,
handleRowClick: (row: ICropInformationRow) => {
this.router.navigate([row.id], { relativeTo: this.route });
},
};

async ngOnInit() {
this.service.ready();
chrismclarke marked this conversation as resolved.
Show resolved Hide resolved
this.refreshCropInformation();
}

refreshCropInformation() {
this.service.listCropProbabilities().catch((error) => {
this.notificationService.showUserNotification({
matIcon: 'error',
message: 'Error fetching crop probabilities:' + error.message,
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable } from '@angular/core';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { Database } from '@picsa/server-types';
import { PicsaAsyncService } from '@picsa/shared/services/asyncService.service';
import { SupabaseService } from '@picsa/shared/services/core/supabase';
import { IStorageEntry } from '@picsa/shared/services/core/supabase/services/supabase-storage.service';

export type ICropInformationRow = Database['public']['Tables']['crop_data']['Row'];
export type ICropInformationInsert = Database['public']['Tables']['crop_data']['Insert'];

export interface IResourceStorageEntry extends IStorageEntry {
/** Url generated when upload to public bucket (will always be populated, even if bucket not public) */
publicUrl: string;
}

@Injectable({ providedIn: 'root' })
export class CropProbabilityDashboardService extends PicsaAsyncService {
public cropProbabilities: ICropInformationRow[] = [];

public get table() {
return this.supabaseService.db.table('crop_data');
}

constructor(private supabaseService: SupabaseService) {
super();
}

public override async init() {
await this.supabaseService.ready();
await this.listCropProbabilities();
}

public async listCropProbabilities() {
const { data, error } = await this.supabaseService.db.table('crop_data').select<'*', ICropInformationRow>('*');
if (error) {
throw error;
}
this.cropProbabilities = data || [];
}

public async addCropProbability(cropProbability: ICropInformationInsert) {
const { data, error } = await this.supabaseService.db.table('crop_data').insert(cropProbability);
if (error) {
throw error;
}
return data;
}
public async updateCropProbability(cropProbability: ICropInformationInsert) {
const { data, error } = await this.supabaseService.db
.table('crop_data')
.update(cropProbability)
.eq('id', cropProbability.id);
if (error) {
throw error;
}
return data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<div class="page-content">
<h2>New Crop Probability Entry</h2>
<form class="form-content" [formGroup]="entryForm" (ngSubmit)="submitForm()">
<div class="form-data">
<picsa-form-crop-select formControlName="crop"></picsa-form-crop-select>
</div>
<div class="form-data">
<label for="variety">Crop Variety:</label>
<input id="variety" type="text" formControlName="variety" />
</div>
<div class="form-data">
<label for="water_upper">Water Upper:</label>
<input id="water_upper" type="number" formControlName="water_upper" />
</div>
<div class="form-data">
<label for="water_lower">Water Lower:</label>
<input id="water_lower" type="number" formControlName="water_lower" />
</div>
<div class="form-data">
<label for="length_upper">Length Upper:</label>
<input id="length_upper" type="number" formControlName="length_upper" />
</div>
<div class="form-data">
<label for="length_lower">Length Lower:</label>
<input id="lenght_lower" type="number" formControlName="length_lower" />
</div>
<button mat-raised-button color="primary" class="submitButton" type="submit" [disabled]="!entryForm.valid">
Submit
</button>
</form>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.form-content{
border: 0.6px solid #eeeeee;
display: flex;
flex-direction: column;
.form-data{
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-evenly;
flex-wrap: wrap;
padding: 12px 0;
border:0.6px solid #eeeeee ;
label{
font-weight: 500;
}
input{
border: 1px solid rgb(119, 127, 122);
height:30px;
border-radius: 6px;
width: 180px;
padding: 0 12px;
}
}
button{
margin: 20px 0;
display: flex;
align-self: center;
border: 0px;
width: 180px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { NewEntryPageComponent } from './new_entry.page';

describe('NewEntryComponent', () => {
let component: NewEntryPageComponent;
let fixture: ComponentFixture<NewEntryPageComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NewEntryPageComponent],
}).compileComponents();

fixture = TestBed.createComponent(NewEntryPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { PicsaFormsModule } from '@picsa/forms';
import { PicsaNotificationService } from '@picsa/shared/services/core/notification.service';

import { DashboardMaterialModule } from '../../../../material.module';
import {
CropProbabilityDashboardService,
ICropInformationInsert,
ICropInformationRow,
} from '../../crop-information.service';

@Component({
selector: 'dashboard-new-entry',
standalone: true,
imports: [CommonModule, DashboardMaterialModule, RouterModule, FormsModule, PicsaFormsModule, ReactiveFormsModule],
templateUrl: './new_entry.component.html',
styleUrls: ['./new_entry.component.scss'],
})
export class NewEntryPageComponent implements OnInit {
entryForm = this.formBuilder.nonNullable.group({
id: new FormControl(), // populated by server or on edit
crop: ['', Validators.required],
variety: ['', Validators.required],
water_lower: [0],
water_upper: [0],
length_lower: [0],
length_upper: [0],
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(+1) thanks for adding the form and validation methods, looks good to me both on db and UI validation.
I still haven't found a super clean way to generate angular forms from our existing db schemas, but will add a small follow-up just to validate the form output against the expected db input


/** Utility method, retained to ensure rawValue corresponds to expected CaledarDataEntry type */
private get formValue() {
const entry: ICropInformationInsert = this.entryForm.getRawValue();
return entry;
}

constructor(
private service: CropProbabilityDashboardService,
private formBuilder: FormBuilder,
private router: Router,
private route: ActivatedRoute,
private notificationService: PicsaNotificationService
) {
this.service.ready();
}

ngOnInit(): void {
this.service.ready();
const { id } = this.route.snapshot.params;
if (id) {
this.loadEditableEntry(id);
}
}

async submitForm() {
try {
if (this.formValue.id) {
await this.service.updateCropProbability(this.formValue);
} else {
// remove null id when adding crop probability
const { id, ...data } = this.formValue;
await this.service.addCropProbability(data);
}
// navigate back after successful addition
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
} catch (error: any) {
this.notificationService.showUserNotification({ matIcon: 'error', message: error.message });
}
}

/** Load an existing db record for editing */
private async loadEditableEntry(id: string) {
const { data, error } = await this.service.table.select<'*', ICropInformationRow>('*').eq('id', id).single();
if (data) {
this.entryForm.patchValue(data);
}
if (error) {
this.notificationService.showUserNotification({ matIcon: 'error', message: error.message });
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
}
}
}
Loading
Loading