Преглед изворни кода

Enhanced for updated schema

Dr-Swopt пре 5 дана
родитељ
комит
665c6f7189

+ 89 - 0
src/app/components/bulk-create-ffb-dialog/bulk-create-ffb-dialog.component.css

@@ -0,0 +1,89 @@
+.dialog-content {
+    max-height: 70vh;
+    overflow-y: auto;
+    padding: 20px;
+    /* Increased padding */
+}
+
+.row {
+    display: flex;
+    gap: 15px;
+    margin-bottom: 15px;
+    /* Added margin bottom */
+}
+
+.flex-1 {
+    flex: 1;
+}
+
+.info-box {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    background-color: #e3f2fd;
+    padding: 10px;
+    border-radius: 4px;
+    margin-bottom: 15px;
+    color: #0d47a1;
+}
+
+.info-box p {
+    margin: 0;
+    font-size: 0.9rem;
+}
+
+.dialog-footer {
+    display: flex;
+    justify-content: flex-end;
+    gap: 10px;
+    margin-top: 20px;
+}
+
+/* Processing View */
+.processing-view {
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
+.status-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+}
+
+.status-header h3 {
+    margin: 0;
+    color: #1976d2;
+}
+
+.count {
+    font-weight: bold;
+    font-size: 1.1rem;
+}
+
+.progress-bar {
+    height: 10px;
+    border-radius: 5px;
+}
+
+.logs-container {
+    background: #f5f5f5;
+    border: 1px solid #e0e0e0;
+    border-radius: 4px;
+    height: 200px;
+    overflow-y: auto;
+    padding: 10px;
+    font-family: monospace;
+    font-size: 0.85rem;
+}
+
+.log-entry {
+    padding: 2px 0;
+    border-bottom: 1px solid #eee;
+}
+
+.log-entry:first-child {
+    font-weight: bold;
+    color: #2e7d32;
+}

+ 92 - 0
src/app/components/bulk-create-ffb-dialog/bulk-create-ffb-dialog.component.html

@@ -0,0 +1,92 @@
+<h2 mat-dialog-title>Bulk Create FFB Production</h2>
+
+<div class="dialog-content">
+    <!-- Configuration Form -->
+    <form [formGroup]="configForm" *ngIf="!isProcessing && processedCount === 0" class="config-form">
+        <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+                <mat-label>Number of Records</mat-label>
+                <input matInput type="number" formControlName="count" min="1">
+            </mat-form-field>
+        </div>
+
+        <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+                <mat-label>Sites (comma separated)</mat-label>
+                <input matInput formControlName="sites">
+                <mat-hint>Randomly selected from this list</mat-hint>
+            </mat-form-field>
+        </div>
+
+        <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+                <mat-label>Phases (comma separated)</mat-label>
+                <input matInput formControlName="phases">
+            </mat-form-field>
+        </div>
+
+        <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+                <mat-label>Blocks (comma separated)</mat-label>
+                <input matInput formControlName="blocks">
+            </mat-form-field>
+        </div>
+
+        <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+                <mat-label>Start Date</mat-label>
+                <input matInput [matDatepicker]="startPicker" formControlName="startDate">
+                <mat-datepicker-toggle matSuffix [for]="startPicker"></mat-datepicker-toggle>
+                <mat-datepicker #startPicker></mat-datepicker>
+            </mat-form-field>
+
+            <mat-form-field appearance="outline" class="flex-1">
+                <mat-label>End Date</mat-label>
+                <input matInput [matDatepicker]="endPicker" formControlName="endDate">
+                <mat-datepicker-toggle matSuffix [for]="endPicker"></mat-datepicker-toggle>
+                <mat-datepicker #endPicker></mat-datepicker>
+            </mat-form-field>
+        </div>
+
+        <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+                <mat-label>Min Weight (Kg)</mat-label>
+                <input matInput type="number" formControlName="minWeight">
+            </mat-form-field>
+            <mat-form-field appearance="outline" class="flex-1">
+                <mat-label>Max Weight (Kg)</mat-label>
+                <input matInput type="number" formControlName="maxWeight">
+            </mat-form-field>
+        </div>
+
+        <div class="info-box">
+            <mat-icon color="primary">info</mat-icon>
+            <p>Quantity (Bundles) will be calculated as Weight / 10.</p>
+        </div>
+
+    </form>
+
+    <!-- Processing View -->
+    <div *ngIf="isProcessing || processedCount > 0" class="processing-view">
+        <div class="status-header">
+            <h3>{{ currentStatus }}</h3>
+            <span class="count">{{ processedCount }} / {{ totalCount }}</span>
+        </div>
+
+        <mat-progress-bar mode="determinate" [value]="progress" class="progress-bar"></mat-progress-bar>
+
+        <div class="logs-container">
+            <div *ngFor="let log of logs" class="log-entry">{{ log }}</div>
+        </div>
+    </div>
+</div>
+
+<div class="dialog-footer">
+    <button *ngIf="!isProcessing" mat-stroked-button (click)="close()">{{ processedCount > 0 ? 'Close' : 'Cancel'
+        }}</button>
+    <button *ngIf="!isProcessing && processedCount === 0" mat-flat-button color="primary" (click)="proceed()"
+        [disabled]="configForm.invalid">Proceed</button>
+    <button *ngIf="isProcessing" mat-flat-button color="warn" (click)="stop()">
+        <mat-icon>stop</mat-icon> Emergency Stop
+    </button>
+</div>

+ 164 - 0
src/app/components/bulk-create-ffb-dialog/bulk-create-ffb-dialog.component.ts

@@ -0,0 +1,164 @@
+
+import { Component, Inject, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+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 { MatIconModule } from '@angular/material/icon';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatProgressBarModule } from '@angular/material/progress-bar';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatNativeDateModule } from '@angular/material/core';
+import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { webConfig } from '../../config';
+
+@Component({
+    selector: 'app-bulk-create-ffb-dialog',
+    templateUrl: './bulk-create-ffb-dialog.component.html',
+    styleUrls: ['./bulk-create-ffb-dialog.component.css'],
+    standalone: true,
+    imports: [
+        CommonModule,
+        MatDialogModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatButtonModule,
+        MatIconModule,
+        MatProgressSpinnerModule,
+        MatProgressBarModule,
+        MatDatepickerModule,
+        MatNativeDateModule,
+        ReactiveFormsModule,
+        HttpClientModule
+    ]
+})
+export class BulkCreateFfbDialogComponent {
+    private http = inject(HttpClient);
+    dialogRef = inject(MatDialogRef<BulkCreateFfbDialogComponent>);
+
+    configForm: FormGroup;
+
+    isProcessing = false;
+    stopSignal = false;
+    progress = 0;
+    totalCount = 0;
+    processedCount = 0;
+
+    logs: string[] = [];
+    currentStatus = 'Idle';
+
+    allSites: string[];
+    allPhases: string[];
+    allBlocks: string[];
+
+    constructor(private fb: FormBuilder, @Inject(MAT_DIALOG_DATA) public data: any) {
+        this.allSites = data?.allSites || ['Site A', 'Site B', 'Site C', 'Site D'];
+        this.allPhases = data?.allPhases || ['Phase 1', 'Phase 2', 'Phase 3'];
+        this.allBlocks = data?.allBlocks || ['Block 1', 'Block 2', 'Block 3'];
+
+        this.configForm = this.fb.group({
+            count: [10, [Validators.required, Validators.min(1)]],
+            sites: [this.allSites.join(', '), Validators.required],
+            phases: [this.allPhases.join(', '), Validators.required],
+            blocks: [this.allBlocks.join(', '), Validators.required],
+            startDate: [new Date('2025-01-01'), Validators.required],
+            endDate: [new Date('2026-12-31'), Validators.required],
+            minWeight: [100, [Validators.required, Validators.min(1)]],
+            maxWeight: [200, [Validators.required, Validators.min(1)]]
+        });
+    }
+
+    async proceed() {
+        if (this.configForm.invalid) return;
+
+        this.isProcessing = true;
+        this.stopSignal = false;
+        this.logs = [];
+        this.processedCount = 0;
+
+        const config = this.configForm.value;
+        this.totalCount = config.count;
+
+        const sites = config.sites.split(',').map((s: string) => s.trim()).filter((s: string) => s);
+        const phases = config.phases.split(',').map((s: string) => s.trim()).filter((s: string) => s);
+        const blocks = config.blocks.split(',').map((s: string) => s.trim()).filter((s: string) => s);
+
+        const start = new Date(config.startDate).getTime();
+        const end = new Date(config.endDate).getTime();
+
+        for (let i = 0; i < this.totalCount; i++) {
+            if (this.stopSignal) {
+                this.log('Operation stopped by user.');
+                break;
+            }
+
+            this.currentStatus = `Processing record ${i + 1} of ${this.totalCount}...`;
+            this.progress = ((i) / this.totalCount) * 100;
+
+            try {
+                // 1. Randomize Data
+                const site = this.getRandomItem(sites);
+                const phase = this.getRandomItem(phases);
+                const block = this.getRandomItem(blocks);
+                const date = new Date(start + Math.random() * (end - start));
+                const weight = Math.floor(Math.random() * (config.maxWeight - config.minWeight + 1)) + config.minWeight;
+                const quantity = Math.floor(weight / 10); // 1 bundle = 10kg assumption
+
+                // 2. Generate Remark
+                this.log(`[${i + 1}] Generating remark for ${site}...`);
+                const context = {
+                    site, phase, block, date, quantity, quantityUom: 'Bundle', weight, weightUom: 'Kg'
+                };
+
+                // We await these calls to ensure sequential processing as requested ("loop until finished")
+                // Note: In a real app we might want to run some in parallel, but requirement implies sequential/controlled loop
+                const remarkRes = await this.http.post<{ remark: string }>(`${webConfig.exposedUrl}/api/ffb-production/generate-remark`, context).toPromise();
+                const remark = remarkRes?.remark || 'No remark generated';
+
+                // 3. Save Record
+                this.log(`[${i + 1}] Persisting record...`);
+                const payload = {
+                    productionDate: date.toISOString(),
+                    site, phase, block,
+                    quantity, quantityUom: 'Bundle',
+                    weight, weightUom: 'Kg',
+                    remarks: remark
+                };
+
+                await this.http.post(`${webConfig.exposedUrl}/api/ffb-production`, payload).toPromise();
+                this.log(`[${i + 1}] Success.`);
+
+            } catch (err) {
+                console.error(err);
+                this.log(`[${i + 1}] Error: Failed to process record.`);
+            }
+
+            this.processedCount++;
+            this.progress = (this.processedCount / this.totalCount) * 100;
+        }
+
+        this.isProcessing = false;
+        this.currentStatus = this.stopSignal ? 'Stopped.' : 'Completed.';
+    }
+
+    stop() {
+        this.stopSignal = true;
+        this.log('Stopping operation...');
+    }
+
+    close() {
+        this.dialogRef.close('refresh');
+    }
+
+    private getRandomItem(arr: string[]): string {
+        return arr[Math.floor(Math.random() * arr.length)];
+    }
+
+    private log(message: string) {
+        this.logs.unshift(message); // Add to top
+        // Limit log size
+        if (this.logs.length > 50) this.logs.pop();
+    }
+}

+ 54 - 0
src/app/components/ffb-chat-dialog/ffb-chat-dialog.component.css

@@ -0,0 +1,54 @@
+
+.dialog-container {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  max-height: 80vh;
+}
+
+.content {
+  flex: 1;
+  overflow-y: auto;
+  padding-top: 10px;
+}
+
+.chat-form {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.full-width {
+  width: 100%;
+}
+
+.actions {
+  display: flex;
+  justify-content: flex-end;
+  margin-bottom: 20px;
+}
+
+.spinner-container {
+  display: flex;
+  justify-content: center;
+  margin: 20px 0;
+}
+
+.response-container {
+  margin-top: 20px;
+  padding: 15px;
+  background-color: #f5f5f5;
+  border-radius: 4px;
+  border-left: 4px solid #3f51b5;
+}
+
+.response-text {
+  white-space: pre-wrap;
+  line-height: 1.5;
+}
+
+h3 {
+  margin-top: 0;
+  color: #3f51b5;
+  font-size: 1rem;
+}

+ 41 - 0
src/app/components/ffb-chat-dialog/ffb-chat-dialog.component.html

@@ -0,0 +1,41 @@
+
+<div class="dialog-container">
+  <h2 mat-dialog-title>Chat with FFB Data</h2>
+  
+  <mat-dialog-content class="content">
+    <form [formGroup]="chatForm" (ngSubmit)="sendMessage()" class="chat-form">
+      <mat-form-field appearance="outline" class="full-width">
+        <mat-label>Provider</mat-label>
+        <mat-select formControlName="provider">
+          <mat-option *ngFor="let p of providers" [value]="p.value">
+            {{p.viewValue}}
+          </mat-option>
+        </mat-select>
+      </mat-form-field>
+
+      <mat-form-field appearance="outline" class="full-width">
+        <mat-label>Your Message</mat-label>
+        <textarea matInput formControlName="message" rows="4" placeholder="Ask something about FFB production..."></textarea>
+      </mat-form-field>
+
+      <div class="actions">
+        <button mat-raised-button color="primary" type="submit" [disabled]="loading || chatForm.invalid">
+          <mat-icon>send</mat-icon> Send
+        </button>
+      </div>
+    </form>
+
+    <div *ngIf="loading" class="spinner-container">
+      <mat-spinner diameter="40"></mat-spinner>
+    </div>
+
+    <div *ngIf="response" class="response-container">
+      <h3>Response:</h3>
+      <div class="response-text">{{ response }}</div>
+    </div>
+  </mat-dialog-content>
+
+  <mat-dialog-actions align="end">
+    <button mat-button (click)="close()">Close</button>
+  </mat-dialog-actions>
+</div>

+ 77 - 0
src/app/components/ffb-chat-dialog/ffb-chat-dialog.component.ts

@@ -0,0 +1,77 @@
+
+import { Component, Inject, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+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 { MatSelectModule } from '@angular/material/select';
+import { MatIconModule } from '@angular/material/icon';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { webConfig } from '../../config';
+
+@Component({
+    selector: 'app-ffb-chat-dialog',
+    templateUrl: './ffb-chat-dialog.component.html',
+    styleUrls: ['./ffb-chat-dialog.component.css'],
+    standalone: true,
+    imports: [
+        CommonModule,
+        MatDialogModule,
+        MatFormFieldModule,
+        MatInputModule,
+        MatButtonModule,
+        MatSelectModule,
+        MatIconModule,
+        MatProgressSpinnerModule,
+        ReactiveFormsModule,
+        HttpClientModule
+    ]
+})
+export class FfbChatDialogComponent {
+    private http = inject(HttpClient);
+    dialogRef = inject(MatDialogRef<FfbChatDialogComponent>);
+
+    chatForm: FormGroup;
+    loading = false;
+    response: string | null = null;
+
+    providers = [
+        { value: 'openai', viewValue: 'OpenAI' },
+        { value: 'gemini', viewValue: 'Gemini' }
+    ];
+
+    constructor(private fb: FormBuilder, @Inject(MAT_DIALOG_DATA) public data: any) {
+        this.chatForm = this.fb.group({
+            message: ['', Validators.required],
+            provider: ['openai', Validators.required]
+        });
+    }
+
+    sendMessage() {
+        if (this.chatForm.invalid) return;
+
+        this.loading = true;
+        this.response = null;
+        const { message, provider } = this.chatForm.value;
+
+        this.http.post<{ response: string }>(`${webConfig.exposedUrl}/api/ffb-production/chat`, { message, provider })
+            .subscribe({
+                next: (res) => {
+                    this.response = res.response;
+                    this.loading = false;
+                },
+                error: (err) => {
+                    console.error(err);
+                    this.response = 'Error: Failed to get response from the server.';
+                    this.loading = false;
+                }
+            });
+    }
+
+    close() {
+        this.dialogRef.close();
+    }
+}

+ 24 - 6
src/app/components/ffb-production-dialog/create-ffb-production-dialog.component.html

@@ -1,5 +1,5 @@
 <h2 mat-dialog-title>
-  {{ data?.production ? 'View/Edit FFB Production' : 'Create FFB Production' }}
+  {{ data?.harvest ? 'View/Edit FFB Production' : 'Create FFB Production' }}
 </h2>
 
 <form [formGroup]="form" (ngSubmit)="onSubmit()" class="dialog-form">
@@ -7,14 +7,14 @@
 
     <mat-form-field appearance="outline" class="flex-1">
       <mat-label>Production Date</mat-label>
-      <input matInput [matDatepicker]="picker" formControlName="productionDate" [readonly]="!!data?.production" />
+      <input matInput [matDatepicker]="picker" formControlName="productionDate" [readonly]="!!data?.harvest" />
       <mat-datepicker-toggle matSuffix [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 [formControl]="siteControl" [matAutocomplete]="siteAuto" [readonly]="!!data?.production" />
+      <input matInput [formControl]="siteControl" [matAutocomplete]="siteAuto" [readonly]="!!data?.harvest" />
       <mat-autocomplete #siteAuto="matAutocomplete">
         <mat-option *ngFor="let option of filteredSites | async" [value]="option">{{ option }}</mat-option>
       </mat-autocomplete>
@@ -22,7 +22,7 @@
 
     <mat-form-field appearance="outline">
       <mat-label>Phase</mat-label>
-      <input matInput [formControl]="phaseControl" [matAutocomplete]="phaseAuto" [readonly]="!!data?.production" />
+      <input matInput [formControl]="phaseControl" [matAutocomplete]="phaseAuto" [readonly]="!!data?.harvest" />
       <mat-autocomplete #phaseAuto="matAutocomplete">
         <mat-option *ngFor="let option of filteredPhases | async" [value]="option">{{ option }}</mat-option>
       </mat-autocomplete>
@@ -30,7 +30,7 @@
 
     <mat-form-field appearance="outline">
       <mat-label>Block</mat-label>
-      <input matInput [formControl]="blockControl" [matAutocomplete]="blockAuto" [readonly]="!!data?.production" />
+      <input matInput [formControl]="blockControl" [matAutocomplete]="blockAuto" [readonly]="!!data?.harvest" />
       <mat-autocomplete #blockAuto="matAutocomplete">
         <mat-option *ngFor="let option of filteredBlocks | async" [value]="option">{{ option }}</mat-option>
       </mat-autocomplete>
@@ -64,10 +64,28 @@
       </mat-autocomplete>
     </mat-form-field>
 
+    <div class="remarks-container" style="width: 100%; display: flex; align-items: flex-start; gap: 10px;">
+      <mat-form-field appearance="outline" style="flex: 1;">
+        <mat-label>Remarks</mat-label>
+        <textarea matInput formControlName="remarks" rows="3" placeholder="Add optional remarks..."></textarea>
+      </mat-form-field>
+      <button mat-mini-fab color="accent" type="button" (click)="generateRemark()" matTooltip="Generate Remark with AI"
+        [disabled]="saving || generatingRemark || form.get('remarks')?.disabled">
+        <mat-progress-spinner *ngIf="generatingRemark" mode="indeterminate" diameter="20"
+          color="primary"></mat-progress-spinner>
+        <mat-icon *ngIf="!generatingRemark">auto_awesome</mat-icon>
+      </button>
+    </div>
+
   </div>
 
   <div class="dialog-footer">
+    <button *ngIf="!data?.harvest" mat-stroked-button color="accent" type="button" (click)="randomize()"
+      style="margin-right: auto;">
+      <mat-icon>shuffle</mat-icon> Randomize
+    </button>
     <button mat-stroked-button type="button" (click)="cancel()">Close</button>
-    <button *ngIf="!data?.harvest" mat-flat-button color="primary" type="submit">Save</button>
+    <button *ngIf="!data?.harvest" mat-flat-button color="primary" type="submit"
+      [disabled]="form.invalid || saving">Save</button>
   </div>
 </form>

+ 85 - 1
src/app/components/ffb-production-dialog/create-ffb-production-dialog.component.ts

@@ -11,6 +11,8 @@ import { MatIconModule } from '@angular/material/icon';
 import { HttpClient, HttpClientModule } from '@angular/common/http';
 import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
 import { MatAutocompleteModule } from '@angular/material/autocomplete';
+import { MatTooltipModule } from '@angular/material/tooltip';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
 import { Observable, startWith, map } from 'rxjs';
 import { webConfig } from '../../config';
 
@@ -30,6 +32,8 @@ import { webConfig } from '../../config';
     MatAutocompleteModule,
     MatIconModule,
     MatSnackBarModule,
+    MatTooltipModule,
+    MatProgressSpinnerModule
   ],
   templateUrl: './create-ffb-production-dialog.component.html',
   styleUrls: ['./create-ffb-production-dialog.component.css'],
@@ -37,6 +41,7 @@ import { webConfig } from '../../config';
 export class CreateFfbProductionDialogComponent implements OnInit {
   form: FormGroup;
   saving = false;
+  generatingRemark = false;
 
   weightUomOptions = ['Kg', 'Ton'];
   quantityUomOptions = ['Bunch', 'Bag'];
@@ -74,9 +79,13 @@ export class CreateFfbProductionDialogComponent implements OnInit {
       quantity: [0, Validators.required],
       quantityUom: ['Bunch', Validators.required],
       weight: [0, Validators.required],
-      weightUom: ['Kg', Validators.required]
+      weightUom: ['Kg', Validators.required],
+      remarks: [{ value: '', disabled: true }]
     });
 
+    // Monitor form changes to enable/disable remarks
+    this.form.valueChanges.subscribe(() => this.checkRemarksStatus());
+
     // Auto-update weight
     this.form.get('quantity')!.valueChanges.subscribe(() => this.updateWeight());
     this.form.get('quantityUom')!.valueChanges.subscribe(() => this.updateWeight());
@@ -117,6 +126,81 @@ export class CreateFfbProductionDialogComponent implements OnInit {
     this.form.patchValue({ weight: quantity * baseWeight }, { emitEvent: false });
   }
 
+  generateRemark() {
+    this.generatingRemark = true; // Use saving flag to show loading state if needed, or create a separate one
+    const payload = this.form.value;
+
+    // Construct a context object for the LLM based on available form data
+    const context = {
+      site: payload.site,
+      phase: payload.phase,
+      block: payload.block,
+      date: payload.productionDate,
+      quantity: payload.quantity,
+      quantityUom: payload.quantityUom,
+      weight: payload.weight,
+      weightUom: payload.weightUom
+    };
+
+    this.http.post<{ remark: string }>(`${webConfig.exposedUrl}/api/ffb-production/generate-remark`, context).subscribe({
+      next: (res) => {
+        this.form.patchValue({ remarks: res.remark });
+        this.snackBar.open('Remark generated!', 'Close', { duration: 2000 });
+        this.generatingRemark = false;
+      },
+      error: (err) => {
+        console.error(err);
+        this.snackBar.open('Failed to generate remark.', 'Close', { duration: 3000 });
+        this.generatingRemark = false;
+      }
+    });
+  }
+
+  randomize() {
+    const defaultSites = ['Site A', 'Site B', 'Site C', 'Site D'];
+    const defaultPhases = ['Phase 1', 'Phase 2', 'Phase 3'];
+    const defaultBlocks = ['Block 1', 'Block 2', 'Block 3'];
+
+    const sites = [...new Set([...this.allSites, ...defaultSites])];
+    const phases = [...new Set([...this.allPhases, ...defaultPhases])];
+    const blocks = [...new Set([...this.allBlocks, ...defaultBlocks])];
+
+    // Date between 1/1/2025 and 31/12/2026
+    const start = new Date('2025-01-01').getTime();
+    const end = new Date('2026-12-31').getTime();
+    const randomDate = new Date(start + Math.random() * (end - start));
+
+    const quantity = Math.floor(Math.random() * (100 - 10 + 1)) + 10;
+    const quantityUom = 'Bunch';
+
+    this.form.patchValue({
+      site: this.getRandomItem(sites),
+      phase: this.getRandomItem(phases),
+      block: this.getRandomItem(blocks),
+      productionDate: randomDate,
+      quantity: quantity,
+      quantityUom: quantityUom
+    });
+
+    // Weight auto-updates via subscription
+  }
+
+  private getRandomItem(arr: string[]) {
+    return arr[Math.floor(Math.random() * arr.length)];
+  }
+
+  private checkRemarksStatus() {
+    const { site, phase, block, productionDate, quantity, weight } = this.form.getRawValue();
+    const allFilled = site && phase && block && productionDate && quantity > 0 && weight > 0;
+
+    const remarksControl = this.form.get('remarks');
+    if (allFilled) {
+      if (remarksControl?.disabled) remarksControl.enable({ emitEvent: false });
+    } else {
+      if (remarksControl?.enabled) remarksControl.disable({ emitEvent: false });
+    }
+  }
+
   onSubmit() {
     if (this.form.invalid) return;
 

+ 17 - 4
src/app/ffb/ffb-production.component.html

@@ -70,11 +70,17 @@
     </button>
 
     <button mat-stroked-button color="primary" (click)="openCalculateDialog()">
-      Calculate Totals & Averages
+      Calculate
     </button>
-
     <button mat-stroked-button color="accent" (click)="openVectorSearchDialog()">
-      <mat-icon>smart_toy</mat-icon> Vector Search
+      <mat-icon>smart_toy</mat-icon> Search
+    </button>
+    <button mat-stroked-button color="accent" (click)="openChatDialog()">
+      <mat-icon>chat</mat-icon> Chat
+    </button>
+
+    <button mat-stroked-button style="background-color: #673ab7; color: white;" (click)="openBulkCreateDialog()">
+      <mat-icon>dynamic_feed</mat-icon> Bulk Create
     </button>
   </div>
 </div>
@@ -112,6 +118,8 @@
     <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">
@@ -124,4 +132,9 @@
   <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
   <tr mat-row *matRowDef="let row; columns: displayedColumns" class="clickable-row" (click)="editProduction(row)">
   </tr>
-</table>
+</table>
+
+<mat-paginator *ngIf="!loading" [length]="totalItems" [pageSize]="pageSize" [pageIndex]="pageIndex"
+  [pageSizeOptions]="pageSizeOptions" (page)="onPageChange($event)" showFirstLastButtons
+  aria-label="Select page of FFB productions">
+</mat-paginator>

+ 97 - 34
src/app/ffb/ffb-production.component.ts

@@ -13,12 +13,16 @@ import { MatNativeDateModule } from '@angular/material/core';
 import { ReactiveFormsModule, FormControl } from '@angular/forms';
 import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
 import { MatDialog, MatDialogModule } from '@angular/material/dialog';
+import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
+import { MatTooltipModule } from '@angular/material/tooltip';
 import { combineLatest, startWith } from 'rxjs';
 import { FFBProduction } from './ffb-production.interface';
 import { webConfig } from '../config';
 import { CreateFfbProductionDialogComponent } from '../components/ffb-production-dialog/create-ffb-production-dialog.component';
 import { FfbHarvestCalculateDialogComponent } from '../components/ffb-calculation/ffb-production-calculate-dialog.component';
 import { FfbVectorSearchDialogComponent } from '../components/ffb-vector-search-dialog/ffb-vector-search-dialog.component';
+import { FfbChatDialogComponent } from '../components/ffb-chat-dialog/ffb-chat-dialog.component';
+import { BulkCreateFfbDialogComponent } from '../components/bulk-create-ffb-dialog/bulk-create-ffb-dialog.component';
 
 @Component({
   selector: 'app-ffb-production',
@@ -39,7 +43,9 @@ import { FfbVectorSearchDialogComponent } from '../components/ffb-vector-search-
     MatNativeDateModule,
     ReactiveFormsModule,
     MatSnackBarModule,
-    MatDialogModule
+    MatDialogModule,
+    MatTooltipModule,
+    MatPaginatorModule
   ],
 })
 export class FfbProductionComponent implements OnInit {
@@ -76,6 +82,12 @@ export class FfbProductionComponent implements OnInit {
     'actions',
   ];
 
+  // Pagination state
+  totalItems = 0;
+  pageSize = 10;
+  pageIndex = 0;
+  pageSizeOptions = [5, 10, 25, 50];
+
   loading = false;
 
   ngOnInit() {
@@ -94,14 +106,58 @@ export class FfbProductionComponent implements OnInit {
 
   loadFFBproduction() {
     this.loading = true;
-    this.http.get<FFBProduction[]>(`${webConfig.exposedUrl}/api/ffb-production`).subscribe({
-      next: (data) => {
+
+    // Prepare query params
+    const params: any = {
+      page: this.pageIndex + 1, // backend is 1-indexed probably
+      limit: this.pageSize,
+    };
+
+    // Add filters if present
+    const keyword = (this.searchControl.value || '').trim();
+    if (keyword) params.keyword = keyword;
+
+    if (this.siteControl.value) params.site = this.siteControl.value;
+    if (this.phaseControl.value) params.phase = this.phaseControl.value;
+
+    const startDate = this.startDateControl.value;
+    if (startDate) params.startDate = startDate.toISOString();
+
+    const endDate = this.endDateControl.value;
+    if (endDate) params.endDate = endDate.toISOString();
+
+    // UOM filters
+    if (this.weightUomControl.value) params.weightUom = this.weightUomControl.value;
+    if (this.quantityUomControl.value) params.quantityUom = this.quantityUomControl.value;
+
+    this.http.get<{ data: FFBProduction[], total: number }>(`${webConfig.exposedUrl}/api/ffb-production`, { params }).subscribe({
+      next: (response) => {
+        // Handle paginated response
+        // If backend returns { data: [], total: 0 }, construct based on that.
+        // If actual structure is just [], we need to adjust.
+        // Assuming user confirmed standard structure or I will use flexible handling.
+
+        let data: FFBProduction[] = [];
+        if (Array.isArray(response)) {
+          // Fallback if backend just returns array
+          data = response;
+          this.totalItems = data.length;
+        } else {
+          data = response.data || [];
+          this.totalItems = response.total || 0;
+        }
+
         this.production = data.map(h => ({ ...h, productionDate: new Date(h.productionDate) }));
+        // filteredProductions is now just the current page data
         this.filteredProductions = [...this.production];
-        // populate unique arrays for filters and autocompletes
+
+        // populate unique arrays for filters and autocompletes - this might need a separate API call if we want ALL unique values
+        // For now, we only get unique values from CURRENT PAGE, which is a limitation of server-side pagination without separate metadata endpoint.
+        // Keeping existing logic for now, but be aware of this limitation.
         this.uniqueSites = [...new Set(this.production.map(h => h.site))];
         this.uniquePhases = [...new Set(this.production.map(h => h.phase))];
         this.uniqueBlocks = [...new Set(this.production.map(h => h.block))];
+
         this.loading = false;
       },
       error: (err) => {
@@ -112,36 +168,16 @@ export class FfbProductionComponent implements OnInit {
     });
   }
 
+  onPageChange(event: PageEvent) {
+    this.pageIndex = event.pageIndex;
+    this.pageSize = event.pageSize;
+    this.loadFFBproduction();
+  }
+
+  // Modified to just trigger reload since filtering is server-side
   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;
-    const weightUom = this.weightUomControl.value;
-    const quantityUom = this.quantityUomControl.value;
-
-    this.filteredProductions = this.production.filter(h => {
-      const matchesKeyword =
-        !keyword ||
-        h.site.toLowerCase().includes(keyword) ||
-        h.phase.toLowerCase().includes(keyword) ||
-        h.block.toLowerCase().includes(keyword) ||
-        h.weight.toString().includes(keyword) ||
-        h.quantity.toString().includes(keyword);
-
-      const matchesSite = !site || h.site === site;
-      const matchesPhase = !phase || h.phase === phase;
-      const matchesWeightUom = !weightUom || h.weightUom === weightUom;
-      const matchesQuantityUom = !quantityUom || h.quantityUom === quantityUom;
-
-      const hDate = new Date(h.productionDate);
-      const matchesDate =
-        (!startDate || hDate >= startDate) && (!endDate || hDate <= endDate);
-
-      return matchesKeyword && matchesSite && matchesPhase &&
-        matchesWeightUom && matchesQuantityUom && matchesDate;
-    });
+    this.pageIndex = 0; // Reset to first page on filter change
+    this.loadFFBproduction();
   }
 
   resetFilters() {
@@ -152,7 +188,8 @@ export class FfbProductionComponent implements OnInit {
     this.endDateControl.setValue(null);
     this.weightUomControl.setValue('');
     this.quantityUomControl.setValue('');
-    this.filteredProductions = [...this.production];
+    this.pageIndex = 0;
+    this.loadFFBproduction();
   }
 
   refresh() {
@@ -237,4 +274,30 @@ export class FfbProductionComponent implements OnInit {
     });
   }
 
+  openChatDialog() {
+    this.dialog.open(FfbChatDialogComponent, {
+      width: '600px',
+      height: '80vh',
+    });
+  }
+
+
+  openBulkCreateDialog() {
+    const dialogRef = this.dialog.open(BulkCreateFfbDialogComponent, {
+      width: '600px',
+      disableClose: true, // Prevent closing while processing
+      data: {
+        allSites: this.uniqueSites,
+        allPhases: this.uniquePhases,
+        allBlocks: this.uniqueBlocks,
+      }
+    });
+
+    dialogRef.afterClosed().subscribe(result => {
+      if (result === 'refresh') this.loadFFBproduction();
+    });
+  }
+
+
+
 }

+ 9 - 1
src/app/ffb/ffb-production.interface.ts

@@ -8,5 +8,13 @@ export interface FFBProduction {
   weightUom: string;
   quantity: number;
   quantityUom: string;
-  score?: number
+  score?: number;
+  remarks?: string;
+}
+
+export interface PaginatedResponse<T> {
+  data: T[];
+  total: number;
+  page: number;
+  limit: number;
 }