|
|
@@ -1,23 +1,22 @@
|
|
|
-import { Component, OnInit } from '@angular/core';
|
|
|
+import { Component, OnInit, inject } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
|
-import { MatTableModule } from '@angular/material/table';
|
|
|
+import { HttpClient, HttpClientModule } from '@angular/common/http';
|
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
|
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
|
+import { MatTableModule } from '@angular/material/table';
|
|
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
|
import { MatInputModule } from '@angular/material/input';
|
|
|
import { MatSelectModule } from '@angular/material/select';
|
|
|
-import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
|
-import { MatCardModule } from '@angular/material/card';
|
|
|
-import { FFBHarvestService } from './ffb-harvest.service';
|
|
|
-import { FFBHarvest } from './ffb-harvest.interface';
|
|
|
-import { NgChartsModule } from 'ng2-charts';
|
|
|
-import { ChartData, ChartOptions } from 'chart.js';
|
|
|
-import { HttpClientModule } from '@angular/common/http';
|
|
|
-import { map } from 'rxjs/operators';
|
|
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
|
|
import { MatNativeDateModule } from '@angular/material/core';
|
|
|
-import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
|
|
+import { ReactiveFormsModule, FormControl } from '@angular/forms';
|
|
|
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
|
+import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
|
|
+import { combineLatest, startWith } from 'rxjs';
|
|
|
+import { FFBHarvest } from './ffb-harvest.interface';
|
|
|
+import { webConfig } from '../config';
|
|
|
+import { CreateFfbHarvestDialogComponent } from '../components/ffb-harvest-dialog/create-ffb-harvest-dialog.component';
|
|
|
|
|
|
@Component({
|
|
|
selector: 'app-ffb-harvest',
|
|
|
@@ -26,158 +25,158 @@ import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
|
|
standalone: true,
|
|
|
imports: [
|
|
|
CommonModule,
|
|
|
- ReactiveFormsModule,
|
|
|
HttpClientModule,
|
|
|
- MatTableModule,
|
|
|
MatButtonModule,
|
|
|
MatIconModule,
|
|
|
+ MatProgressSpinnerModule,
|
|
|
+ MatTableModule,
|
|
|
MatFormFieldModule,
|
|
|
MatInputModule,
|
|
|
MatSelectModule,
|
|
|
- MatSnackBarModule,
|
|
|
MatDatepickerModule,
|
|
|
MatNativeDateModule,
|
|
|
- MatAutocompleteModule,
|
|
|
- MatCardModule,
|
|
|
- NgChartsModule
|
|
|
+ ReactiveFormsModule,
|
|
|
+ MatSnackBarModule,
|
|
|
+ MatDialogModule
|
|
|
],
|
|
|
})
|
|
|
export class FfbHarvestComponent implements OnInit {
|
|
|
- weightUomOptions: string[] = ['kg', 'ton'];
|
|
|
- quantityUomOptions: string[] = ['bunch', 'kg'];
|
|
|
+ private http = inject(HttpClient);
|
|
|
+ private snack = inject(MatSnackBar);
|
|
|
+ private dialog = inject(MatDialog);
|
|
|
+
|
|
|
+ searchControl = new FormControl('');
|
|
|
+ siteControl = new FormControl('');
|
|
|
+ phaseControl = new FormControl('');
|
|
|
+ startDateControl = new FormControl<Date | null>(null);
|
|
|
+ endDateControl = new FormControl<Date | null>(null);
|
|
|
+
|
|
|
harvests: FFBHarvest[] = [];
|
|
|
- displayedColumns: string[] = ['harvestDate', 'site', 'phase', 'block', 'weight', 'quantity', 'actions'];
|
|
|
- form: FormGroup;
|
|
|
- editing: boolean = false;
|
|
|
+ filteredHarvests: FFBHarvest[] = [];
|
|
|
+ uniqueSites: string[] = [];
|
|
|
+ uniquePhases: string[] = [];
|
|
|
+
|
|
|
+ displayedColumns: string[] = [
|
|
|
+ 'harvestDate',
|
|
|
+ 'harvester',
|
|
|
+ 'site',
|
|
|
+ 'phase',
|
|
|
+ 'block',
|
|
|
+ 'weight',
|
|
|
+ 'quantity',
|
|
|
+ 'actions',
|
|
|
+ ];
|
|
|
+
|
|
|
loading = false;
|
|
|
|
|
|
- // Chart related
|
|
|
- public chartData: ChartData<'line'> = { labels: [], datasets: [] };
|
|
|
- public chartOptions: ChartOptions<'line'> = {
|
|
|
- responsive: true,
|
|
|
- scales: {
|
|
|
- x: { type: 'time', time: { unit: 'day' } as any },
|
|
|
- y: { beginAtZero: true }
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- constructor(
|
|
|
- private harvestSvc: FFBHarvestService,
|
|
|
- private fb: FormBuilder,
|
|
|
- private snack: MatSnackBar
|
|
|
- ) {
|
|
|
- this.form = this.fb.group({
|
|
|
- harvestDate: [null, Validators.required],
|
|
|
- site: ['', Validators.required],
|
|
|
- phase: [''],
|
|
|
- block: [''],
|
|
|
- harvester: [''],
|
|
|
- daysOfWork: [0, [Validators.required, Validators.min(0)]],
|
|
|
- weight: [0, [Validators.required, Validators.min(0)]],
|
|
|
- weightUom: ['kg', Validators.required],
|
|
|
- quantity: [0, [Validators.required, Validators.min(0)]],
|
|
|
- quantityUom: ['bunch', Validators.required]
|
|
|
- });
|
|
|
- }
|
|
|
+ ngOnInit() {
|
|
|
+ this.loadHarvests();
|
|
|
|
|
|
- ngOnInit(): void {
|
|
|
- this.load();
|
|
|
+ combineLatest([
|
|
|
+ this.searchControl.valueChanges.pipe(startWith(this.searchControl.value)),
|
|
|
+ this.siteControl.valueChanges.pipe(startWith(this.siteControl.value)),
|
|
|
+ this.phaseControl.valueChanges.pipe(startWith(this.phaseControl.value)),
|
|
|
+ this.startDateControl.valueChanges.pipe(startWith(this.startDateControl.value)),
|
|
|
+ this.endDateControl.valueChanges.pipe(startWith(this.endDateControl.value)),
|
|
|
+ ]).subscribe(() => this.applyFilters());
|
|
|
}
|
|
|
|
|
|
- load(query?: any) {
|
|
|
+ loadHarvests() {
|
|
|
this.loading = true;
|
|
|
- this.harvestSvc.findAll(query).pipe(
|
|
|
- map(arr => arr || [])
|
|
|
- ).subscribe({
|
|
|
- next: (res) => {
|
|
|
- this.harvests = res.map(r => ({ ...r, harvestDate: new Date(r.harvestDate) }));
|
|
|
- this.updateChart();
|
|
|
+ this.http.get<FFBHarvest[]>(`${webConfig.exposedUrl}/api/ffb-harvest`).subscribe({
|
|
|
+ next: (data) => {
|
|
|
+ this.harvests = data.map(h => ({ ...h, harvestDate: new Date(h.harvestDate) }));
|
|
|
+ this.filteredHarvests = [...this.harvests];
|
|
|
+ this.uniqueSites = [...new Set(this.harvests.map(h => h.site))];
|
|
|
+ this.uniquePhases = [...new Set(this.harvests.map(h => h.phase))];
|
|
|
this.loading = false;
|
|
|
},
|
|
|
error: (err) => {
|
|
|
console.error(err);
|
|
|
this.snack.open('Failed to load harvests', 'Close', { duration: 3000 });
|
|
|
this.loading = false;
|
|
|
- }
|
|
|
+ },
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private updateChart() {
|
|
|
- const sorted = [...this.harvests].sort((a, b) => (new Date(a.harvestDate)).getTime() - (new Date(b.harvestDate)).getTime());
|
|
|
- const labels = sorted.map(s => (new Date(s.harvestDate)).toISOString());
|
|
|
- const data = sorted.map(s => s.weight);
|
|
|
- this.chartData = {
|
|
|
- labels,
|
|
|
- datasets: [
|
|
|
- { label: 'Weight', data, tension: 0.2 }
|
|
|
- ]
|
|
|
- };
|
|
|
- }
|
|
|
+ applyFilters() {
|
|
|
+ const keyword = (this.searchControl.value || '').toLowerCase().trim();
|
|
|
+ const site = this.siteControl.value;
|
|
|
+ const phase = this.phaseControl.value;
|
|
|
+ const startDate = this.startDateControl.value;
|
|
|
+ const endDate = this.endDateControl.value;
|
|
|
+
|
|
|
+ this.filteredHarvests = this.harvests.filter(h => {
|
|
|
+ const matchesKeyword =
|
|
|
+ !keyword ||
|
|
|
+ h.site.toLowerCase().includes(keyword) ||
|
|
|
+ h.phase.toLowerCase().includes(keyword) ||
|
|
|
+ h.block.toLowerCase().includes(keyword);
|
|
|
|
|
|
- startCreate() {
|
|
|
- this.editing = true;
|
|
|
- this.form.reset({
|
|
|
- harvestDate: null, site: '', phase: '', block: '', harvester: '',
|
|
|
- daysOfWork: 0, weight: 0, weightUom: 'kg', quantity: 0, quantityUom: 'bunch'
|
|
|
+ const matchesSite = !site || h.site === site;
|
|
|
+ const matchesPhase = !phase || h.phase === phase;
|
|
|
+
|
|
|
+ const hDate = new Date(h.harvestDate);
|
|
|
+ const matchesDate =
|
|
|
+ (!startDate || hDate >= startDate) && (!endDate || hDate <= endDate);
|
|
|
+
|
|
|
+ return matchesKeyword && matchesSite && matchesPhase && matchesDate;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- cancel() {
|
|
|
- this.editing = false;
|
|
|
+ resetFilters() {
|
|
|
+ this.searchControl.setValue('');
|
|
|
+ this.siteControl.setValue('');
|
|
|
+ this.phaseControl.setValue('');
|
|
|
+ this.startDateControl.setValue(null);
|
|
|
+ this.endDateControl.setValue(null);
|
|
|
+ this.filteredHarvests = [...this.harvests];
|
|
|
}
|
|
|
|
|
|
- submit() {
|
|
|
- if (this.form.invalid) {
|
|
|
- this.snack.open('Please fill required fields', 'Close', { duration: 2000 });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- const payload: FFBHarvest = {
|
|
|
- ...this.form.value,
|
|
|
- harvestDate: this.form.value.harvestDate
|
|
|
- };
|
|
|
-
|
|
|
- this.harvestSvc.create(payload).subscribe({
|
|
|
- next: () => {
|
|
|
- this.snack.open('Created', 'Close', { duration: 2000 });
|
|
|
- this.load();
|
|
|
- this.cancel();
|
|
|
- },
|
|
|
- error: (err) => {
|
|
|
- console.error(err);
|
|
|
- this.snack.open('Create failed', 'Close', { duration: 3000 });
|
|
|
- }
|
|
|
- });
|
|
|
+ refresh() {
|
|
|
+ this.loadHarvests();
|
|
|
}
|
|
|
|
|
|
- remove(id?: string) {
|
|
|
- if (!id) return;
|
|
|
- if (!confirm('Delete this record?')) return;
|
|
|
- this.harvestSvc.delete(id).subscribe({
|
|
|
+ deleteHarvest(id?: string) {
|
|
|
+ if (!id || !confirm('Are you sure you want to delete this record?')) return;
|
|
|
+
|
|
|
+ this.http.delete(`${webConfig.exposedUrl}/api/ffb-harvest/${id}`).subscribe({
|
|
|
next: () => {
|
|
|
this.snack.open('Deleted', 'Close', { duration: 2000 });
|
|
|
- this.load();
|
|
|
+ this.loadHarvests();
|
|
|
},
|
|
|
error: (err) => {
|
|
|
console.error(err);
|
|
|
this.snack.open('Delete failed', 'Close', { duration: 3000 });
|
|
|
- }
|
|
|
+ },
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- showDetails(id?: string) {
|
|
|
- if (!id) { this.snack.open('No id', 'Close', { duration: 1500 }); return; }
|
|
|
- this.harvestSvc.findById(id).subscribe({
|
|
|
- next: (res) => alert(JSON.stringify(res, null, 2)),
|
|
|
- error: (err) => {
|
|
|
- console.error(err);
|
|
|
- this.snack.open('Failed to fetch details', 'Close', { duration: 2000 });
|
|
|
- }
|
|
|
+ formatDate(date: Date | string) {
|
|
|
+ const d = new Date(date);
|
|
|
+ return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: '2-digit' });
|
|
|
+ }
|
|
|
+
|
|
|
+ // ================== DIALOG HANDLERS ==================
|
|
|
+
|
|
|
+ createHarvest() {
|
|
|
+ const dialogRef = this.dialog.open(CreateFfbHarvestDialogComponent, {
|
|
|
+ width: '800px',
|
|
|
+ });
|
|
|
+
|
|
|
+ dialogRef.afterClosed().subscribe(result => {
|
|
|
+ if (result === 'refresh') this.loadHarvests();
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- filterBySite(site: string) {
|
|
|
- if (!site) this.load();
|
|
|
- else this.load({ site });
|
|
|
+ editHarvest(harvest: FFBHarvest) {
|
|
|
+ const dialogRef = this.dialog.open(CreateFfbHarvestDialogComponent, {
|
|
|
+ width: '800px',
|
|
|
+ data: harvest
|
|
|
+ });
|
|
|
+
|
|
|
+ dialogRef.afterClosed().subscribe(result => {
|
|
|
+ if (result === 'refresh') this.loadHarvests();
|
|
|
+ });
|
|
|
}
|
|
|
}
|