瀏覽代碼

feat: introduce new dialogs for bulk and single FFB production record creation with configurable parameters and processing UI.

Dr-Swopt 2 周之前
父節點
當前提交
37f5c5226a

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

@@ -10,27 +10,6 @@
             </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">

+ 59 - 20
src/app/components/bulk-create-ffb-dialog/bulk-create-ffb-dialog.component.ts

@@ -14,6 +14,9 @@ import { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angula
 import { HttpClient, HttpClientModule } from '@angular/common/http';
 import { webConfig } from '../../config';
 
+import { Site, Phase, Block } from '../../interfaces/site.interface';
+import { SiteService } from '../../services/site-management.service';
+
 @Component({
     selector: 'app-bulk-create-ffb-dialog',
     templateUrl: './bulk-create-ffb-dialog.component.html',
@@ -49,20 +52,19 @@ export class BulkCreateFfbDialogComponent {
     logs: string[] = [];
     currentStatus = 'Idle';
 
-    allSites: string[];
-    allPhases: string[];
-    allBlocks: string[];
+    private siteService = inject(SiteService);
+
+    // Caches for hierarchical data
+    siteCache: Site[] = [];
+    phaseCache: Record<string, Phase[]> = {};
+    blockCache: Record<string, Block[]> = {};
 
     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'];
+        // Pre-load sites on init
+        this.loadSites();
 
         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)]],
@@ -70,6 +72,13 @@ export class BulkCreateFfbDialogComponent {
         });
     }
 
+    loadSites() {
+        this.siteService.getSites().subscribe({
+            next: (sites) => this.siteCache = sites,
+            error: (err) => console.error('Failed to load sites', err)
+        });
+    }
+
     async proceed() {
         if (this.configForm.invalid) return;
 
@@ -81,10 +90,6 @@ export class BulkCreateFfbDialogComponent {
         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();
 
@@ -98,22 +103,54 @@ export class BulkCreateFfbDialogComponent {
             this.progress = ((i) / this.totalCount) * 100;
 
             try {
-                // 1. Randomize Data
-                const site = this.getRandomItem(sites);
+                // 1. Randomize Data (Hierarchical)
+                if (this.siteCache.length === 0) {
+                    throw new Error('No sites available.');
+                }
+
+                const site = this.getRandomItem(this.siteCache);
+
+                // Fetch/Cache Phases
+                let phases = this.phaseCache[site._id!];
+                if (!phases) {
+                    phases = await this.siteService.getPhasesBySite(site._id!).toPromise() || [];
+                    this.phaseCache[site._id!] = phases;
+                }
+
+                if (phases.length === 0) {
+                    this.log(`[${i + 1}] Skip: No phases for site ${site.name}`);
+                    continue;
+                }
+
                 const phase = this.getRandomItem(phases);
+
+                // Fetch/Cache Blocks
+                let blocks = this.blockCache[phase._id!];
+                if (!blocks) {
+                    blocks = await this.siteService.getBlocksByPhase(phase._id!).toPromise() || [];
+                    this.blockCache[phase._id!] = blocks;
+                }
+
+                if (blocks.length === 0) {
+                    this.log(`[${i + 1}] Skip: No blocks for phase ${phase.name}`);
+                    continue;
+                }
+
                 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}...`);
+                this.log(`[${i + 1}] Generating remark for ${site.name}...`);
                 const context = {
-                    site, phase, block, date, quantity, quantityUom: 'Bundle', weight, weightUom: 'Kg'
+                    siteId: site._id, phaseId: phase._id, blockId: block._id, // Send IDs for enrichment
+                    site: site.name, phase: phase.name, block: block.name,
+                    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';
 
@@ -121,7 +158,9 @@ export class BulkCreateFfbDialogComponent {
                 this.log(`[${i + 1}] Persisting record...`);
                 const payload = {
                     productionDate: date.toISOString(),
-                    site, phase, block,
+                    site: { id: site._id, name: site.name },
+                    phase: { id: phase._id, name: phase.name },
+                    block: { id: block._id, name: block.name },
                     quantity, quantityUom: 'Bundle',
                     weight, weightUom: 'Kg',
                     remarks: remark
@@ -152,7 +191,7 @@ export class BulkCreateFfbDialogComponent {
         this.dialogRef.close('refresh');
     }
 
-    private getRandomItem(arr: string[]): string {
+    private getRandomItem(arr: any[]): any {
         return arr[Math.floor(Math.random() * arr.length)];
     }
 

+ 43 - 23
src/app/components/ffb-production-dialog/create-ffb-production-dialog.component.ts

@@ -65,8 +65,8 @@ export class CreateFfbProductionDialogComponent implements OnInit {
     this.form = this.fb.group({
       productionDate: [new Date(), Validators.required],
       site: [null, Validators.required],
-      phase: [null, Validators.required],
-      block: [null, Validators.required],
+      phase: [{ value: null, disabled: true }, Validators.required],
+      block: [{ value: null, disabled: true }, Validators.required],
       quantity: [0, Validators.required],
       quantityUom: ['Bunch', Validators.required],
       weight: [0, Validators.required],
@@ -105,20 +105,29 @@ export class CreateFfbProductionDialogComponent implements OnInit {
 
       // For dependent dropdowns, we need to load them sequentially
       // 1. Load phases for the site
-      if (harvest.site && harvest.site.id) {
-        // We might not have the full Site object in 'harvest.site' matching what 'getSites' returns 
-        // but we have id. We should probably find the site wrapper from 'sites' array after it loads?
-        // Actually, let's just set the form control to the object provided.
-        // And trigger the load.
+      if (harvest.site && (harvest.site.id || (harvest.site as any)._id)) {
+        const siteId = harvest.site.id || (harvest.site as any)._id;
+
+        // Enable phase control since we have a site
+        this.form.get('phase')?.enable();
+
+        // We use patchValue with the ID-containing object. 
+        // Ensure compareFn handles it (which it does).
         this.form.patchValue({ site: harvest.site });
 
-        this.siteService.getPhasesBySite(harvest.site.id).subscribe(phases => {
+        this.siteService.getPhasesBySite(siteId).subscribe(phases => {
           this.phases = phases;
-          this.form.patchValue({ phase: harvest.phase });
 
-          if (harvest.phase && harvest.phase.id) {
-            this.siteService.getBlocksByPhase(harvest.phase.id).subscribe(blocks => {
+          if (harvest.phase && (harvest.phase.id || (harvest.phase as any)._id)) {
+            const phaseId = harvest.phase.id || (harvest.phase as any)._id;
+
+            this.form.get('phase')?.enable();
+            this.form.patchValue({ phase: harvest.phase });
+
+            // Load blocks
+            this.siteService.getBlocksByPhase(phaseId).subscribe(blocks => {
               this.blocks = blocks;
+              this.form.get('block')?.enable();
               this.form.patchValue({ block: harvest.block });
             });
           }
@@ -136,12 +145,15 @@ export class CreateFfbProductionDialogComponent implements OnInit {
   onSiteChange(site: any) { // site is { id, name } or Site object
     this.phases = [];
     this.blocks = [];
-    this.form.patchValue({ phase: null, block: null });
-
-    if (site && site._id) { // Site object from DB has _id
-      this.fetchPhases(site._id);
-    } else if (site && site.id) { // FFBProduction object has id
-      this.fetchPhases(site.id);
+    this.form.get('phase')?.reset({ value: null, disabled: true });
+    this.form.get('block')?.reset({ value: null, disabled: true });
+
+    if (site) {
+      const siteId = site._id || site.id;
+      if (siteId) {
+        this.fetchPhases(siteId);
+        this.form.get('phase')?.enable();
+      }
     }
   }
 
@@ -153,12 +165,14 @@ export class CreateFfbProductionDialogComponent implements OnInit {
 
   onPhaseChange(phase: any) {
     this.blocks = [];
-    this.form.patchValue({ block: null });
+    this.form.get('block')?.reset({ value: null, disabled: true });
 
-    if (phase && phase._id) {
-      this.fetchBlocks(phase._id);
-    } else if (phase && phase.id) {
-      this.fetchBlocks(phase.id);
+    if (phase) {
+      const phaseId = phase._id || phase.id;
+      if (phaseId) {
+        this.fetchBlocks(phaseId);
+        this.form.get('block')?.enable();
+      }
     }
   }
 
@@ -189,6 +203,9 @@ export class CreateFfbProductionDialogComponent implements OnInit {
     const payload = this.form.value;
 
     const context = {
+      siteId: payload.site?._id || payload.site?.id,
+      phaseId: payload.phase?._id || payload.phase?.id,
+      blockId: payload.block?._id || payload.block?.id,
       site: payload.site?.name,
       phase: payload.phase?.name,
       block: payload.block?.name,
@@ -229,12 +246,15 @@ export class CreateFfbProductionDialogComponent implements OnInit {
       this.phases = phases;
       if (phases.length > 0) {
         const randomPhase = this.getRandomItem(phases);
+        this.form.get('phase')?.enable();
         this.form.patchValue({ phase: randomPhase });
 
         this.siteService.getBlocksByPhase(randomPhase._id!).subscribe(blocks => {
           this.blocks = blocks;
           if (blocks.length > 0) {
-            this.form.patchValue({ block: this.getRandomItem(blocks) });
+            const randomBlock = this.getRandomItem(blocks);
+            this.form.get('block')?.enable();
+            this.form.patchValue({ block: randomBlock });
           }
         });
       }