-
Notifications
You must be signed in to change notification settings - Fork 2
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
Changes from all commits
02415ac
dffdebd
e948d5e
520a325
3a5405a
c3005ea
9c84a81
78e64c6
45ab6d2
3c10703
fcbd239
240c986
0c5d05b
1dd0c7b
f5ed638
de8d379
013917e
c61195b
7d7a057
353da7e
b4a6fc9
06b9423
245fc71
1ad4f4f
f7b42e5
5f2d869
b6157aa
c7fafc7
b3ca16c
a45618d
71b70d1
4ae3ac8
6f9488e
0cfed3e
6b0e503
a00447d
3ff5559
30f377f
283070e
e75b174
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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()), | ||
], | ||
}; |
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], | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
/** 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 }); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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...