Jelajahi Sumber

ffb compont creation

Dr-Swopt 2 minggu lalu
induk
melakukan
abd0206edf

+ 45 - 0
package-lock.json

@@ -24,6 +24,8 @@
         "@capgo/capacitor-native-biometric": "^7.1.7",
         "@simplewebauthn/browser": "^13.1.0",
         "angularx-qrcode": "^20.0.0",
+        "chart.js": "^4.5.1",
+        "ng2-charts": "^5.0.4",
         "ngx-socket-io": "^4.9.1",
         "rxjs": "~7.8.2",
         "socket.io-client": "^4.8.1",
@@ -4043,6 +4045,12 @@
         "tslib": "2"
       }
     },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+      "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+      "license": "MIT"
+    },
     "node_modules/@leichtgewicht/ip-codec": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -7024,6 +7032,19 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/chart.js": {
+      "version": "4.5.1",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+      "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=8"
+      }
+    },
     "node_modules/chokidar": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -10538,6 +10559,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+      "license": "MIT"
+    },
     "node_modules/lodash.debounce": {
       "version": "4.0.8",
       "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -11297,6 +11324,24 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/ng2-charts": {
+      "version": "5.0.4",
+      "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-5.0.4.tgz",
+      "integrity": "sha512-AnOZ2KSRw7QjiMMNtXz9tdnO+XrIKP/2MX1TfqEEo2fwFU5c8LFJIYqmkMPkIzAEm/U9y/1psA5TDNmxxjEdgA==",
+      "license": "MIT",
+      "dependencies": {
+        "lodash-es": "^4.17.15",
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/cdk": ">=16.0.0",
+        "@angular/common": ">=16.0.0",
+        "@angular/core": ">=16.0.0",
+        "@angular/platform-browser": ">=16.0.0",
+        "chart.js": "^3.4.0 || ^4.0.0",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
     "node_modules/ngx-socket-io": {
       "version": "4.9.1",
       "resolved": "https://registry.npmjs.org/ngx-socket-io/-/ngx-socket-io-4.9.1.tgz",

+ 2 - 0
package.json

@@ -28,6 +28,8 @@
     "@capgo/capacitor-native-biometric": "^7.1.7",
     "@simplewebauthn/browser": "^13.1.0",
     "angularx-qrcode": "^20.0.0",
+    "chart.js": "^4.5.1",
+    "ng2-charts": "^5.0.4",
     "ngx-socket-io": "^4.9.1",
     "rxjs": "~7.8.2",
     "socket.io-client": "^4.8.1",

+ 27 - 0
src/app/activity/activity.component.html

@@ -21,6 +21,33 @@
       </mat-select>
     </mat-form-field>
 
+    <!-- 🔽 Filter by Resource Type -->
+    <mat-form-field appearance="outline">
+      <mat-label>Resource Type</mat-label>
+      <mat-select [formControl]="resourceTypeControl">
+        <mat-option value="">All</mat-option>
+        <mat-option *ngFor="let t of uniqueResourceTypes" [value]="t">{{ t }}</mat-option>
+      </mat-select>
+    </mat-form-field>
+
+    <!-- 🔽 Filter by Output Type -->
+    <mat-form-field appearance="outline">
+      <mat-label>Output Type</mat-label>
+      <mat-select [formControl]="outputTypeControl">
+        <mat-option value="">All</mat-option>
+        <mat-option *ngFor="let t of uniqueOutputTypes" [value]="t">{{ t }}</mat-option>
+      </mat-select>
+    </mat-form-field>
+
+    <!-- 🔽 Filter by Target Type -->
+    <mat-form-field appearance="outline">
+      <mat-label>Target Type</mat-label>
+      <mat-select [formControl]="targetTypeControl">
+        <mat-option value="">All</mat-option>
+        <mat-option *ngFor="let t of uniqueTargetTypes" [value]="t">{{ t }}</mat-option>
+      </mat-select>
+    </mat-form-field>
+
     <!-- 📅 Start Date -->
     <mat-form-field appearance="outline">
       <mat-label>Start Date</mat-label>

+ 77 - 17
src/app/activity/activity.component.ts

@@ -16,7 +16,7 @@ import { MatNativeDateModule } from '@angular/material/core'; // 🧩 For native
 import { MatAutocompleteModule } from '@angular/material/autocomplete';
 import { ReactiveFormsModule, FormControl } from '@angular/forms';
 import { combineLatest, startWith } from 'rxjs';
-import { Activity } from './activity.interface';
+import { Activity, Output, Resource, Target } from './activity.interface';
 import { CalculateDialogComponent } from '../components/calculate-dialog/calculate-dialog.component';
 
 @Component({
@@ -46,11 +46,18 @@ export class ActivityComponent implements OnInit {
   private http = inject(HttpClient);
   private dialog = inject(MatDialog);
   searchControl = new FormControl('');
+  typeControl = new FormControl('');
   dataSource: Activity[] = [];
   filteredActivities: Activity[] = [];
+
   uniqueActivityNames: string[] = [];
+  uniqueResourceTypes: string[] = [];
+  uniqueOutputTypes: string[] = [];
+  uniqueTargetTypes: string[] = [];
   uniqueTypes: string[] = [];
-  typeControl = new FormControl('');
+  resourceTypeControl = new FormControl('');
+  outputTypeControl = new FormControl('');
+  targetTypeControl = new FormControl('');
   startDateControl = new FormControl<Date | null>(null);
   endDateControl = new FormControl<Date | null>(null);
 
@@ -73,9 +80,13 @@ export class ActivityComponent implements OnInit {
     combineLatest([
       this.searchControl.valueChanges.pipe(startWith(this.searchControl.value)),
       this.typeControl.valueChanges.pipe(startWith(this.typeControl.value)),
+      this.resourceTypeControl.valueChanges.pipe(startWith(this.resourceTypeControl.value)),
+      this.outputTypeControl.valueChanges.pipe(startWith(this.outputTypeControl.value)),
+      this.targetTypeControl.valueChanges.pipe(startWith(this.targetTypeControl.value)),
       this.startDateControl.valueChanges.pipe(startWith(this.startDateControl.value)),
       this.endDateControl.valueChanges.pipe(startWith(this.endDateControl.value)),
     ]).subscribe(() => this.applyCombinedFilters());
+
   }
 
   refresh() {
@@ -114,6 +125,29 @@ export class ActivityComponent implements OnInit {
 
         this.filteredActivities = [...this.dataSource];
         this.uniqueTypes = [...new Set(data.map((a) => a.type))];
+        this.uniqueResourceTypes = [
+          ...new Set(
+            data.flatMap((a: Activity) =>
+              a.resources?.map((r: Resource) => r.type) || []
+            )
+          ),
+        ];
+
+        this.uniqueOutputTypes = [
+          ...new Set(
+            data.flatMap((a: Activity) =>
+              a.outputs?.map((o: Output) => o.type) || []
+            )
+          ),
+        ];
+
+        this.uniqueTargetTypes = [
+          ...new Set(
+            data.flatMap((a: Activity) =>
+              a.targets?.map((t: Target) => t.type) || []
+            )
+          ),
+        ];
         this.loading = false;
         this.applyCombinedFilters();
       },
@@ -127,46 +161,72 @@ export class ActivityComponent implements OnInit {
 
 
   applyCombinedFilters() {
-    const keyword = (this.searchControl.value || '').toLowerCase();
-    const selectedType = this.typeControl.value;
+    const keyword = (this.searchControl.value || '').toLowerCase().trim();
+    const selectedActivityType = this.typeControl.value;
+    const selectedResourceType = this.resourceTypeControl.value;
+    const selectedOutputType = this.outputTypeControl.value;
+    const selectedTargetType = this.targetTypeControl.value;
     const startDate = this.startDateControl.value;
     const endDate = this.endDateControl.value;
 
     this.filteredActivities = this.dataSource.filter((activity) => {
-      // 1️⃣ Check activity name
-      const matchesName = activity.name.toLowerCase().includes(keyword);
-
-      // 2️⃣ Check resource names
+      // 1️⃣ Keyword matches
+      const matchesName = activity.name?.toLowerCase().includes(keyword);
       const matchesResource = activity.resources?.some(res =>
-        res.name.toLowerCase().includes(keyword)
+        res.name?.toLowerCase().includes(keyword)
+      ) ?? false;
+      const matchesOutput = activity.outputs?.some(out =>
+        out.name?.toLowerCase().includes(keyword)
+      ) ?? false;
+      const matchesTarget = activity.targets?.some(tgt =>
+        tgt.name?.toLowerCase().includes(keyword)
       ) ?? false;
 
-      const matchesKeyword = matchesName || matchesResource;
-
-      // 3️⃣ Type filter
-      const matchesType = selectedType ? activity.type === selectedType : true;
-
-      // 4️⃣ Date range filter
+      const matchesKeyword = !keyword || matchesName || matchesResource || matchesOutput || matchesTarget;
+
+      // 2️⃣ Type filters
+      const matchesActivityType = selectedActivityType ? activity.type === selectedActivityType : true;
+      const matchesResourceType = selectedResourceType
+        ? activity.resources?.some(res => res.type === selectedResourceType)
+        : true;
+      const matchesOutputType = selectedOutputType
+        ? activity.outputs?.some(out => out.type === selectedOutputType)
+        : true;
+      const matchesTargetType = selectedTargetType
+        ? activity.targets?.some(tgt => tgt.type === selectedTargetType)
+        : true;
+
+      // 3️⃣ Date range filter
       const activityStart = new Date(activity.dateStart);
       const activityEnd = new Date(activity.dateEnd);
       const matchesDateRange =
         (!startDate || activityStart >= startDate) &&
         (!endDate || activityEnd <= endDate);
 
-      return matchesKeyword && matchesType && matchesDateRange;
+      return (
+        matchesKeyword &&
+        matchesActivityType &&
+        matchesResourceType &&
+        matchesOutputType &&
+        matchesTargetType &&
+        matchesDateRange
+      );
     });
   }
 
-
   resetFilters() {
     this.searchControl.setValue('');
     this.typeControl.setValue('');
+    this.resourceTypeControl.setValue('');
+    this.outputTypeControl.setValue('');
+    this.targetTypeControl.setValue('');
     this.startDateControl.setValue(null);
     this.endDateControl.setValue(null);
     this.filteredActivities = [...this.dataSource];
   }
 
 
+
   applyFilter(value: string) {
     const filterValue = value.toLowerCase();
     this.filteredActivities = this.dataSource.filter((activity) =>

+ 14 - 0
src/app/components/calculate-dialog/calculate-dialog.component.html

@@ -87,9 +87,14 @@
         <p>{{ totalDuration }} {{ durationUom }}</p>
     </section>
 
+    <!-- MONTHLY AVERAGES -->
     <!-- MONTHLY AVERAGES -->
     <section *ngIf="averageOutputsPerMonth && (averageOutputsPerMonth | keyvalue).length">
         <h3>📦 Average Outputs per Month</h3>
+        <p *ngIf="overallStart && overallEnd">
+            📅 Period: {{ overallStart | date:'mediumDate' }} – {{ overallEnd | date:'mediumDate' }}
+            ({{ totalMonths | number:'1.0-1' }} months)
+        </p>
         <ul>
             <li *ngFor="let uom of (averageOutputsPerMonth | keyvalue)">
                 {{ uom.value | number:'1.0-2' }} {{ totalOutputUoms[uom.key] }}
@@ -99,6 +104,10 @@
 
     <section *ngIf="averageTargetsPerMonth && (averageTargetsPerMonth | keyvalue).length">
         <h3>🎯 Average Targets per Month</h3>
+        <p *ngIf="overallStart && overallEnd">
+            📅 Period: {{ overallStart | date:'mediumDate' }} – {{ overallEnd | date:'mediumDate' }}
+            ({{ totalMonths | number:'1.0-1' }} months)
+        </p>
         <ul>
             <li *ngFor="let uom of (averageTargetsPerMonth | keyvalue)">
                 {{ uom.value | number:'1.0-2' }} {{ totalTargetUoms[uom.key] }}
@@ -108,8 +117,13 @@
 
     <section *ngIf="averageDurationPerMonth && averageDurationPerMonth > 0">
         <h3>⏱️ Average Duration per Month</h3>
+        <p *ngIf="overallStart && overallEnd">
+            📅 Period: {{ overallStart | date:'mediumDate' }} – {{ overallEnd | date:'mediumDate' }}
+            ({{ totalMonths | number:'1.0-1' }} months)
+        </p>
         <p>{{ averageDurationPerMonth | number:'1.0-2' }} {{ durationUom }}</p>
     </section>
+
 </mat-dialog-content>
 
 <mat-dialog-actions align="end">

+ 2 - 0
src/app/components/calculate-dialog/calculate-dialog.component.ts

@@ -57,6 +57,7 @@ export class CalculateDialogComponent implements OnInit {
     totalTargetUoms: Record<string, string> = {};
     totalDuration: number = 0;
     durationUom: string = '';
+    totalMonths: number = 0
 
     averageOutputsPerMonth: Record<string, number> = {};
     averageTargetsPerMonth: Record<string, number> = {};
@@ -204,6 +205,7 @@ export class CalculateDialogComponent implements OnInit {
         this.totalTargetUoms = targetUoms;
         this.totalDuration = totalDurationQuantity;
         this.durationUom = durationUom;
+        this.totalMonths = totalMonths
 
         this.averageOutputsPerMonth = averageOutputs;
         this.averageTargetsPerMonth = averageTargets;

+ 1 - 1
src/app/config.ts

@@ -1,3 +1,3 @@
 export const webConfig = {
-    exposedUrl: `https://192.168.100.100:3000`,
+    exposedUrl: `https://192.168.100.100:4000`,
 }

+ 5 - 1
src/app/dashboard/dashboard.component.html

@@ -18,7 +18,11 @@
   <mat-tab label="Activities">
     <app-activity></app-activity>
   </mat-tab>
-
+  
+  <mat-tab label="FFB Harvest">
+    <app-ffb-harvest></app-ffb-harvest>
+  </mat-tab>
+    
   <mat-tab label="Plantation">
     <!-- Integrate PlantationTreeComponent here -->
     <app-plantation-tree></app-plantation-tree>

+ 4 - 2
src/app/dashboard/dashboard.component.ts

@@ -9,6 +9,7 @@ import { DashboardHomeComponent } from './dashboard.home.component';
 import { PaymentComponent } from '../payment/payment.component';
 import { PlantationTreeComponent } from "../plantation/plantation-tree.component";
 import { ActivityComponent } from '../activity/activity.component';
+import { FfbHarvestComponent } from "../ffb/ffb-harvest.component";
 
 @Component({
   selector: 'app-dashboard',
@@ -22,8 +23,9 @@ import { ActivityComponent } from '../activity/activity.component';
     PaymentComponent,
     AttendanceComponent,
     PlantationTreeComponent,
-    ActivityComponent
-  ],
+    ActivityComponent,
+    FfbHarvestComponent
+],
   templateUrl: './dashboard.component.html',
   styleUrls: ['./dashboard.component.css']
 })

+ 55 - 0
src/app/ffb/ffb-harvest.component.css

@@ -0,0 +1,55 @@
+:host {
+  display: block;
+  padding: 12px;
+}
+
+.controls {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.small {
+  width: 220px;
+}
+
+.table-wrap {
+  overflow: auto;
+  max-height: 360px;
+  margin-bottom: 12px;
+}
+
+.table-wrap table {
+  width: 100%;
+}
+
+.chart {
+  margin: 12px 0;
+  height: 260px;
+}
+
+.form-area {
+  margin-top: 16px;
+  padding: 12px;
+  border: 1px solid rgba(0,0,0,0.08);
+  border-radius: 8px;
+}
+
+.row {
+  display: flex;
+  gap: 12px;
+  flex-wrap: wrap;
+  margin-bottom: 12px;
+}
+
+.row mat-form-field {
+  flex: 1 1 220px;
+}
+
+.form-actions {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  margin-top: 10px;
+}

+ 155 - 0
src/app/ffb/ffb-harvest.component.html

@@ -0,0 +1,155 @@
+<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
+      </button>
+
+      <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>
+            <mat-select formControlName="weightUOM">
+              <mat-option value="kg">kg</mat-option>
+              <mat-option value="ton">ton</mat-option>
+            </mat-select>
+          </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>
+            <mat-select formControlName="quantityUOM">
+              <mat-option value="bunch">bunch</mat-option>
+              <mat-option value="kg">kg</mat-option>
+            </mat-select>
+          </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>

+ 179 - 0
src/app/ffb/ffb-harvest.component.ts

@@ -0,0 +1,179 @@
+import { Component, OnInit } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { MatTableModule } from '@angular/material/table';
+import { MatButtonModule } from '@angular/material/button';
+import { MatIconModule } from '@angular/material/icon';
+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.model';
+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';
+
+@Component({
+  selector: 'app-ffb-harvest',
+  templateUrl: './ffb-harvest.component.html',
+  styleUrls: ['./ffb-harvest.component.css'],
+  standalone: true,
+  imports: [
+    CommonModule,
+    ReactiveFormsModule,
+    HttpClientModule,
+    MatTableModule,
+    MatButtonModule,
+    MatIconModule,
+    MatFormFieldModule,
+    MatInputModule,
+    MatSelectModule,
+    MatSnackBarModule,
+    MatDatepickerModule,
+    MatNativeDateModule,
+    MatCardModule,
+    NgChartsModule
+  ],
+})
+export class FfbHarvestComponent implements OnInit {
+  harvests: FFBHarvest[] = [];
+  displayedColumns: string[] = ['harvestDate', 'site', 'phase', 'block', 'weight', 'quantity', 'actions'];
+  form: FormGroup;
+  editing: boolean = false;
+  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(): void {
+    this.load();
+  }
+
+  load(query?: any) {
+    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.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 }
+      ]
+    };
+  }
+
+  startCreate() {
+    this.editing = true;
+    this.form.reset({
+      harvestDate: null, site: '', phase: '', block: '', harvester: '',
+      daysOfWork: 0, weight: 0, weightUom: 'kg', quantity: 0, quantityUom: 'bunch'
+    });
+  }
+
+  cancel() {
+    this.editing = false;
+  }
+
+  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 });
+      }
+    });
+  }
+
+  remove(id?: string) {
+    if (!id) return;
+    if (!confirm('Delete this record?')) return;
+    this.harvestSvc.delete(id).subscribe({
+      next: () => {
+        this.snack.open('Deleted', 'Close', { duration: 2000 });
+        this.load();
+      },
+      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 });
+      }
+    });
+  }
+
+  filterBySite(site: string) {
+    if (!site) this.load();
+    else this.load({ site });
+  }
+}

+ 13 - 0
src/app/ffb/ffb-harvest.model.ts

@@ -0,0 +1,13 @@
+export interface FFBHarvest {
+  _id?: string;
+  harvestDate: Date | string; // backend returns ISO string; convert to Date where needed
+  site: string;
+  phase: string;
+  block: string;
+  harvester: string;
+  daysOfWork: number;
+  weight: number;
+  weightUom: string;
+  quantity: number;
+  quantityUom: string;
+}

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

@@ -0,0 +1,38 @@
+import { Injectable } from '@angular/core';
+import { HttpClient, HttpParams } from '@angular/common/http';
+import { Observable } from 'rxjs';
+import { FFBHarvest } from './ffb-harvest.model';
+import { webConfig } from '../config';
+
+@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}`);
+  }
+}

+ 1 - 1
src/app/socket.config.ts

@@ -3,6 +3,6 @@
 import { SocketIoConfig } from 'ngx-socket-io';
 
 export const socketConfig: SocketIoConfig = {
-    url: 'https://localhost:3000/ws', // change this to your backend URL
+    url: 'https://localhost:4000/ws', // change this to your backend URL
     options: {}
 };