瀏覽代碼

working components

Dr-Swopt 2 周之前
父節點
當前提交
184db3ebb1

+ 63 - 0
src/app/components/ffb-harvest-dialog/create-ffb-harvest-dialog.component.css

@@ -0,0 +1,63 @@
+.dialog-form {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+  padding: 16px;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+
+.form-section {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.flex-1 {
+  flex: 1 1 0;
+}
+
+.accordion {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.array-container {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  margin-top: 12px;
+}
+
+.form-array-item {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+  padding: 12px;
+  background: #fafafa;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+  margin-top: 16px;
+}
+
+button[color='warn'] {
+  background-color: #d32f2f;
+  color: #fff;
+}
+
+.snackbar-success {
+  background-color: #4caf50;
+  color: white;
+}
+
+.snackbar-error {
+  background-color: #f44336;
+  color: white;
+}

+ 82 - 0
src/app/components/ffb-harvest-dialog/create-ffb-harvest-dialog.component.html

@@ -0,0 +1,82 @@
+<h2 mat-dialog-title>
+  {{ data ? 'View FFB Harvest' : 'Create FFB Harvest' }}
+</h2>
+
+<form [formGroup]="form" (ngSubmit)="onSubmit()" class="dialog-form">
+  <div class="form-section">
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Harvest Date</mat-label>
+      <input matInput [matDatepicker]="picker" formControlName="harvestDate" [readonly]="!!data" />
+      <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
+      <mat-datepicker #picker></mat-datepicker>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Site</mat-label>
+      <input matInput formControlName="site" [readonly]="!!data" />
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Phase</mat-label>
+      <input matInput formControlName="phase" [readonly]="!!data" />
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Block</mat-label>
+      <input matInput formControlName="block" [readonly]="!!data" />
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Quantity</mat-label>
+      <input matInput type="number" formControlName="quantity" [readonly]="!!data" />
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Quantity UOM</mat-label>
+      <input
+        type="text"
+        matInput
+        formControlName="quantityUom"
+        [matAutocomplete]="quantityUomAuto"
+        [readonly]="!!data"
+      />
+      <mat-autocomplete #quantityUomAuto="matAutocomplete">
+        <mat-option *ngFor="let u of quantityUomOptions" [value]="u">{{ u }}</mat-option>
+      </mat-autocomplete>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Weight</mat-label>
+      <input matInput type="number" formControlName="weight" [readonly]="!!data" />
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Weight UOM</mat-label>
+      <input
+        type="text"
+        matInput
+        formControlName="weightUom"
+        [matAutocomplete]="weightUomAuto"
+        [readonly]="!!data"
+      />
+      <mat-autocomplete #weightUomAuto="matAutocomplete">
+        <mat-option *ngFor="let u of weightUomOptions" [value]="u">{{ u }}</mat-option>
+      </mat-autocomplete>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Harvester</mat-label>
+      <input matInput formControlName="harvester" [readonly]="!!data" />
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="flex-1">
+      <mat-label>Days of Work</mat-label>
+      <input matInput type="number" formControlName="daysOfWork" [readonly]="!!data" />
+    </mat-form-field>
+  </div>
+
+  <div class="dialog-footer">
+    <button mat-stroked-button type="button" (click)="cancel()">Close</button>
+    <button *ngIf="!data" mat-flat-button color="primary" type="submit">Save</button>
+  </div>
+</form>

+ 133 - 0
src/app/components/ffb-harvest-dialog/create-ffb-harvest-dialog.component.ts

@@ -0,0 +1,133 @@
+import { CommonModule } from '@angular/common';
+import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
+import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatNativeDateModule } from '@angular/material/core';
+import { MatIconModule } from '@angular/material/icon';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { webConfig } from '../../config';
+import { Component, Inject } from '@angular/core';
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
+import { MatAutocompleteModule } from '@angular/material/autocomplete';
+
+@Component({
+  selector: 'app-create-ffb-harvest-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    ReactiveFormsModule,
+    HttpClientModule,
+    MatDialogModule,
+    MatFormFieldModule,
+    MatInputModule,
+    MatButtonModule,
+    MatDatepickerModule,
+    MatNativeDateModule,
+    MatAutocompleteModule,
+    MatIconModule,
+    MatSnackBarModule,
+  ],
+  templateUrl: './create-ffb-harvest-dialog.component.html',
+  styleUrls: ['./create-ffb-harvest-dialog.component.css'],
+})
+export class CreateFfbHarvestDialogComponent {
+  form: FormGroup;
+  saving = false;
+
+  // Options for UOMs
+  weightUomOptions = ['Kg', 'Ton',];
+  quantityUomOptions = ['Bunch', 'Bundle', 'Bag'];
+
+  constructor(
+    private fb: FormBuilder,
+    private http: HttpClient,
+    private dialogRef: MatDialogRef<CreateFfbHarvestDialogComponent>,
+    private snackBar: MatSnackBar,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {
+    this.form = this.fb.group({
+      harvestDate: [new Date(), Validators.required],
+      site: ['', Validators.required],
+      phase: ['', Validators.required],
+      block: ['', Validators.required],
+      quantity: [0, Validators.required],
+      quantityUom: ['Bunch', Validators.required],
+      weight: [0, Validators.required],
+      weightUom: ['Kg', Validators.required],
+      harvester: ['', Validators.required],
+      daysOfWork: [0, Validators.required],
+    });
+
+    // Watch for changes on quantity or quantityUom to auto-calculate weight
+    this.form.get('quantity')!.valueChanges.subscribe(() => this.updateWeight());
+    this.form.get('quantityUom')!.valueChanges.subscribe(() => this.updateWeight());
+  }
+
+  ngOnInit() {
+    if (this.data) {
+      this.form.patchValue({
+        harvestDate: this.data.harvestDate,
+        site: this.data.site,
+        phase: this.data.phase,
+        block: this.data.block,
+        quantity: this.data.quantity,
+        quantityUom: this.data.quantityUom || 'Bunch',
+        weight: this.data.weight,
+        weightUom: this.data.weightUom || 'Kg',
+        harvester: this.data.harvester,
+        daysOfWork: this.data.daysOfWork,
+      });
+    }
+  }
+
+  // Conversion map (quantityUom -> weight in kg)
+  private conversionMap: Record<string, number> = {
+    Bunch: 10,
+    Bundle: 25,
+    Bag: 100,
+  };
+
+  /** Automatically update weight based on quantity and quantityUom */
+  private updateWeight() {
+    const quantity = this.form.get('quantity')!.value || 0;
+    const quantityUom = this.form.get('quantityUom')!.value;
+    const baseWeight = this.conversionMap[quantityUom] || 0;
+
+    const totalWeightKg = quantity * baseWeight;
+
+    this.form.patchValue(
+      { weight: totalWeightKg },
+      { emitEvent: false } // avoid infinite loop
+    );
+  }
+
+  onSubmit() {
+    if (this.form.invalid) return;
+
+    const payload = {
+      ...this.form.value,
+      harvestDate: new Date(this.form.value.harvestDate).toISOString(),
+    };
+
+    this.saving = true;
+    this.http.post(`${webConfig.exposedUrl}/api/ffb-harvest`, payload).subscribe({
+      next: () => {
+        this.snackBar.open('FFB Harvest created!', 'Close', { duration: 3000 });
+        this.dialogRef.close('refresh');
+      },
+      error: (err) => {
+        console.error(err);
+        this.snackBar.open('Failed to save FFB Harvest.', 'Close', { duration: 5000 });
+        this.saving = false;
+      },
+    });
+  }
+
+  cancel() {
+    this.dialogRef.close();
+  }
+}
+

+ 36 - 23
src/app/ffb/ffb-harvest.component.css

@@ -1,3 +1,19 @@
+/* --- Container padding --- */
+.ffb-harvest-container {
+  padding: 1.5rem;
+}
+
+/* --- Table styles --- */
+table {
+  width: 100%;
+  margin-top: 10px;
+}
+
+mat-progress-spinner {
+  display: block;
+  margin: 2rem auto;
+}
+
 .clickable-row {
   cursor: pointer;
   transition: background-color 0.2s;
@@ -7,6 +23,16 @@
   background-color: #f5f5f5;
 }
 
+.spin {
+  animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+/* --- Toolbar layout --- */
 .toolbar {
   display: flex;
   justify-content: space-between;
@@ -15,6 +41,7 @@
   gap: 0.5rem;
 }
 
+/* --- Left section layout --- */
 .left-section {
   display: flex;
   align-items: flex-end;
@@ -25,52 +52,38 @@
 
 .left-section mat-form-field {
   margin-bottom: 0 !important;
-  transform: translateY(4px);
+  transform: translateY(4px); /* Align with buttons */
 }
 
+/* --- Search bar width --- */
 .search-field {
   width: 250px;
   max-width: 400px;
   margin-top: 0 !important;
 }
 
+/* --- Buttons --- */
 .left-section button {
   margin-top: 0 !important;
-  height: 56px;
+  height: 56px; /* Match Material form-field height */
   display: flex;
   align-items: center;
   justify-content: center;
 }
 
+/* --- Warning buttons (Reset Filters / Delete) --- */
 button[color="warn"] {
   color: white !important;
   background-color: #d32f2f !important;
+  margin-bottom: 10px;
 }
 
-button[color="primary"] {
+button[color="primary"], button[color="accent"] {
   margin-bottom: 10px;
 }
 
+/* --- Title alignment --- */
 .left-section h2 {
   margin: 0;
-  line-height: 56px;
-}
-
-table {
-  width: 100%;
-  margin-top: 10px;
-}
-
-mat-progress-spinner {
-  display: block;
-  margin: 2rem auto;
-}
-
-.spin {
-  animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-  from { transform: rotate(0deg); }
-  to { transform: rotate(360deg); }
+  line-height: 56px; /* Align with form fields and buttons */
 }

+ 106 - 156
src/app/ffb/ffb-harvest.component.html

@@ -1,158 +1,108 @@
-<mat-card>
-  <mat-card-title>FFB Harvests</mat-card-title>
-
-  <mat-card-content>
-    <div class="controls">
-      <button mat-flat-button color="primary" (click)="startCreate()">
-        <mat-icon>add</mat-icon> New
+<div class="toolbar">
+  <div class="left-section">
+    <h2 style="margin-bottom: 15px;">FFB Harvests</h2>
+
+    <mat-form-field appearance="outline" class="search-field">
+      <mat-label>Search</mat-label>
+      <input matInput [formControl]="searchControl" placeholder="Search by site/phase/block..." />
+      <button *ngIf="searchControl.value" mat-icon-button matSuffix (click)="searchControl.setValue('')">
+        <mat-icon>close</mat-icon>
+      </button>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline">
+      <mat-label>Site</mat-label>
+      <mat-select [formControl]="siteControl">
+        <mat-option value="">All</mat-option>
+        <mat-option *ngFor="let s of uniqueSites" [value]="s">{{ s }}</mat-option>
+      </mat-select>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline">
+      <mat-label>Phase</mat-label>
+      <mat-select [formControl]="phaseControl">
+        <mat-option value="">All</mat-option>
+        <mat-option *ngFor="let p of uniquePhases" [value]="p">{{ p }}</mat-option>
+      </mat-select>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline">
+      <mat-label>Start Date</mat-label>
+      <input matInput [matDatepicker]="startPicker" [formControl]="startDateControl">
+      <mat-datepicker-toggle matSuffix [for]="startPicker"></mat-datepicker-toggle>
+      <mat-datepicker #startPicker></mat-datepicker>
+    </mat-form-field>
+
+    <mat-form-field appearance="outline">
+      <mat-label>End Date</mat-label>
+      <input matInput [matDatepicker]="endPicker" [formControl]="endDateControl">
+      <mat-datepicker-toggle matSuffix [for]="endPicker"></mat-datepicker-toggle>
+      <mat-datepicker #endPicker></mat-datepicker>
+    </mat-form-field>
+
+    <button mat-icon-button color="primary" (click)="refresh()" [disabled]="loading" [class.spin]="loading">
+      <mat-icon>refresh</mat-icon>
+    </button>
+
+    <button mat-flat-button color="primary" (click)="createHarvest()">
+      + Add FFB Harvest
+    </button>
+
+    <button mat-stroked-button color="warn" (click)="resetFilters()">
+      <mat-icon>clear_all</mat-icon>
+      Reset Filters
+    </button>
+  </div>
+</div>
+
+<mat-progress-spinner *ngIf="loading" mode="indeterminate" diameter="48"></mat-progress-spinner>
+
+<table mat-table [dataSource]="filteredHarvests" class="mat-elevation-z8" *ngIf="!loading">
+  <ng-container matColumnDef="harvestDate">
+    <th mat-header-cell *matHeaderCellDef>Date</th>
+    <td mat-cell *matCellDef="let h">{{ formatDate(h.harvestDate) }}</td>
+  </ng-container>
+
+  <ng-container matColumnDef="harvester">
+    <th mat-header-cell *matHeaderCellDef>Harvester</th>
+    <td mat-cell *matCellDef="let h">{{ h.harvester }}</td>
+  </ng-container>
+
+  <ng-container matColumnDef="site">
+    <th mat-header-cell *matHeaderCellDef>Site</th>
+    <td mat-cell *matCellDef="let h">{{ h.site }}</td>
+  </ng-container>
+
+  <ng-container matColumnDef="phase">
+    <th mat-header-cell *matHeaderCellDef>Phase</th>
+    <td mat-cell *matCellDef="let h">{{ h.phase }}</td>
+  </ng-container>
+
+  <ng-container matColumnDef="block">
+    <th mat-header-cell *matHeaderCellDef>Block</th>
+    <td mat-cell *matCellDef="let h">{{ h.block }}</td>
+  </ng-container>
+
+  <ng-container matColumnDef="weight">
+    <th mat-header-cell *matHeaderCellDef>Weight</th>
+    <td mat-cell *matCellDef="let h">{{ h.weight }} {{ h.weightUom }}</td>
+  </ng-container>
+
+  <ng-container matColumnDef="quantity">
+    <th mat-header-cell *matHeaderCellDef>Quantity</th>
+    <td mat-cell *matCellDef="let h">{{ h.quantity }} {{ h.quantityUom }}</td>
+  </ng-container>
+
+  <ng-container matColumnDef="actions">
+    <th mat-header-cell *matHeaderCellDef></th>
+    <td mat-cell *matCellDef="let h">
+      <button mat-icon-button color="warn" (click)="deleteHarvest(h._id); $event.stopPropagation()">
+        <mat-icon>delete</mat-icon>
       </button>
+    </td>
+  </ng-container>
 
-      <mat-form-field appearance="outline" class="small">
-        <mat-label>Filter site</mat-label>
-        <input matInput (keyup.enter)="filterBySite($any($event.target).value)" placeholder="type site then Enter">
-      </mat-form-field>
-
-      <button mat-button color="accent" (click)="load()">Refresh</button>
-    </div>
-
-    <div class="table-wrap" *ngIf="harvests?.length; else noData">
-      <table mat-table [dataSource]="harvests" class="mat-elevation-z2">
-        <ng-container matColumnDef="harvestDate">
-          <th mat-header-cell *matHeaderCellDef>Date</th>
-          <td mat-cell *matCellDef="let el">{{ el.harvestDate | date: 'yyyy-MM-dd' }}</td>
-        </ng-container>
-
-        <ng-container matColumnDef="site">
-          <th mat-header-cell *matHeaderCellDef>Site</th>
-          <td mat-cell *matCellDef="let el">{{ el.site }}</td>
-        </ng-container>
-
-        <ng-container matColumnDef="phase">
-          <th mat-header-cell *matHeaderCellDef>Phase</th>
-          <td mat-cell *matCellDef="let el">{{ el.phase }}</td>
-        </ng-container>
-
-        <ng-container matColumnDef="block">
-          <th mat-header-cell *matHeaderCellDef>Block</th>
-          <td mat-cell *matCellDef="let el">{{ el.block }}</td>
-        </ng-container>
-
-        <ng-container matColumnDef="weight">
-          <th mat-header-cell *matHeaderCellDef>Weight</th>
-          <td mat-cell *matCellDef="let el">{{ el.weight }} {{ el.weightUom }}</td>
-        </ng-container>
-
-        <ng-container matColumnDef="quantity">
-          <th mat-header-cell *matHeaderCellDef>Qty</th>
-          <td mat-cell *matCellDef="let el">{{ el.quantity }} {{ el.quantityUom }}</td>
-        </ng-container>
-
-        <ng-container matColumnDef="actions">
-          <th mat-header-cell *matHeaderCellDef>Actions</th>
-          <td mat-cell *matCellDef="let el">
-            <button mat-icon-button color="accent" (click)="showDetails(el._id)" title="Details">
-              <mat-icon>visibility</mat-icon>
-            </button>
-            <button mat-icon-button color="warn" (click)="remove(el._id)" title="Delete">
-              <mat-icon>delete</mat-icon>
-            </button>
-          </td>
-        </ng-container>
-
-        <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
-        <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
-      </table>
-    </div>
-
-    <ng-template #noData>
-      <p>No harvest data found.</p>
-    </ng-template>
-
-    <div class="chart" *ngIf="harvests && harvests.length">
-      <canvas baseChart [data]="chartData" [options]="chartOptions" chartType="line"></canvas>
-    </div>
-
-    <div id="ffb-form" class="form-area" *ngIf="editing">
-      <h3>Create Harvest</h3>
-      <form [formGroup]="form" (ngSubmit)="submit()">
-
-        <div class="row">
-          <mat-form-field appearance="outline">
-            <mat-label>Harvest Date</mat-label>
-            <input matInput [matDatepicker]="picker" formControlName="harvestDate" placeholder="Choose a date" />
-            <mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
-            <mat-datepicker #picker></mat-datepicker>
-          </mat-form-field>
-
-          <mat-form-field appearance="outline">
-            <mat-label>Site</mat-label>
-            <input matInput formControlName="site">
-          </mat-form-field>
-        </div>
-
-        <div class="row">
-          <mat-form-field appearance="outline">
-            <mat-label>Phase</mat-label>
-            <input matInput formControlName="phase">
-          </mat-form-field>
-
-          <mat-form-field appearance="outline">
-            <mat-label>Block</mat-label>
-            <input matInput formControlName="block">
-          </mat-form-field>
-        </div>
-
-        <div class="row">
-          <mat-form-field appearance="outline">
-            <mat-label>Harvester</mat-label>
-            <input matInput formControlName="harvester">
-          </mat-form-field>
-
-          <mat-form-field appearance="outline">
-            <mat-label>Days of Work</mat-label>
-            <input matInput type="number" formControlName="daysOfWork">
-          </mat-form-field>
-        </div>
-
-        <div class="row">
-          <mat-form-field appearance="outline">
-            <mat-label>Weight</mat-label>
-            <input matInput type="number" formControlName="weight">
-          </mat-form-field>
-          <mat-form-field appearance="outline">
-            <mat-label>Weight UOM</mat-label>
-            <input type="text" matInput [matAutocomplete]="weightUomAuto" formControlName="weightUOM">
-            <mat-autocomplete #weightUomAuto="matAutocomplete">
-              <mat-option *ngFor="let uom of weightUomOptions" [value]="uom">
-                {{ uom }}
-              </mat-option>
-            </mat-autocomplete>
-          </mat-form-field>
-        </div>
-
-        <div class="row">
-          <mat-form-field appearance="outline">
-            <mat-label>Quantity</mat-label>
-            <input matInput type="number" formControlName="quantity">
-          </mat-form-field>
-
-          <mat-form-field appearance="outline">
-            <mat-label>Quantity UOM</mat-label>
-            <input type="text" matInput [matAutocomplete]="quantityUomAuto" formControlName="quantityUOM">
-            <mat-autocomplete #quantityUomAuto="matAutocomplete">
-              <mat-option *ngFor="let uom of quantityUomOptions" [value]="uom">
-                {{ uom }}
-              </mat-option>
-            </mat-autocomplete>
-            </mat-form-field>
-        </div>
-
-        <div class="form-actions">
-          <button mat-flat-button color="primary" type="submit">Create</button>
-          <button mat-button type="button" (click)="cancel()">Cancel</button>
-        </div>
-      </form>
-    </div>
-
-  </mat-card-content>
-</mat-card>
+  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+  <tr mat-row *matRowDef="let row; columns: displayedColumns" class="clickable-row" (click)="editHarvest(row)">
+  </tr>
+</table>

+ 117 - 118
src/app/ffb/ffb-harvest.component.ts

@@ -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();
+    });
   }
 }

+ 0 - 38
src/app/ffb/ffb-harvest.service.ts

@@ -1,38 +0,0 @@
-import { Injectable } from '@angular/core';
-import { HttpClient, HttpParams } from '@angular/common/http';
-import { Observable } from 'rxjs';
-import { webConfig } from '../config';
-import { FFBHarvest } from './ffb-harvest.interface';
-
-@Injectable({
-  providedIn: 'root'
-})
-export class FFBHarvestService {
-  private base = `${webConfig.exposedUrl}/api/ffb-harvest`;
-
-  constructor(private http: HttpClient) {}
-
-  create(item: FFBHarvest): Observable<FFBHarvest> {
-    return this.http.post<FFBHarvest>(this.base, item);
-  }
-
-  findAll(query?: any): Observable<FFBHarvest[]> {
-    let params = new HttpParams();
-    if (query) {
-      Object.keys(query).forEach(k => {
-        if (query[k] !== null && query[k] !== undefined && query[k] !== '') {
-          params = params.set(k, query[k]);
-        }
-      });
-    }
-    return this.http.get<FFBHarvest[]>(this.base, { params });
-  }
-
-  findById(id: string): Observable<FFBHarvest> {
-    return this.http.get<FFBHarvest>(`${this.base}/${id}`);
-  }
-
-  delete(id: string): Observable<any> {
-    return this.http.delete(`${this.base}/${id}`);
-  }
-}