|
@@ -10,11 +10,13 @@ import { MatNativeDateModule } from '@angular/material/core';
|
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
import { MatIconModule } from '@angular/material/icon';
|
|
|
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
|
import { HttpClient, HttpClientModule } from '@angular/common/http';
|
|
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
|
-import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
|
|
|
|
|
|
+import { MatSelectModule } from '@angular/material/select';
|
|
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
|
-import { Observable, startWith, map } from 'rxjs';
|
|
|
|
|
import { webConfig } from '../../config';
|
|
import { webConfig } from '../../config';
|
|
|
|
|
+import { SiteService } from '../../services/site-management.service';
|
|
|
|
|
+import { Site, Phase, Block } from '../../interfaces/site.interface';
|
|
|
|
|
+import { FFBProduction } from '../../ffb/ffb-production.interface';
|
|
|
|
|
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-create-ffb-production-dialog',
|
|
selector: 'app-create-ffb-production-dialog',
|
|
@@ -29,7 +31,7 @@ import { webConfig } from '../../config';
|
|
|
MatButtonModule,
|
|
MatButtonModule,
|
|
|
MatDatepickerModule,
|
|
MatDatepickerModule,
|
|
|
MatNativeDateModule,
|
|
MatNativeDateModule,
|
|
|
- MatAutocompleteModule,
|
|
|
|
|
|
|
+ MatSelectModule,
|
|
|
MatIconModule,
|
|
MatIconModule,
|
|
|
MatSnackBarModule,
|
|
MatSnackBarModule,
|
|
|
MatTooltipModule,
|
|
MatTooltipModule,
|
|
@@ -46,36 +48,25 @@ export class CreateFfbProductionDialogComponent implements OnInit {
|
|
|
weightUomOptions = ['Kg', 'Ton'];
|
|
weightUomOptions = ['Kg', 'Ton'];
|
|
|
quantityUomOptions = ['Bunch', 'Bag'];
|
|
quantityUomOptions = ['Bunch', 'Bag'];
|
|
|
|
|
|
|
|
- allSites: string[] = [];
|
|
|
|
|
- allPhases: string[] = [];
|
|
|
|
|
- allBlocks: string[] = [];
|
|
|
|
|
- allWorkers: string[] = [];
|
|
|
|
|
-
|
|
|
|
|
- filteredSites!: Observable<string[]>;
|
|
|
|
|
- filteredPhases!: Observable<string[]>;
|
|
|
|
|
- filteredBlocks!: Observable<string[]>;
|
|
|
|
|
- filteredWorkers!: Observable<string[]>;
|
|
|
|
|
|
|
+ sites: Site[] = [];
|
|
|
|
|
+ phases: Phase[] = [];
|
|
|
|
|
+ blocks: Block[] = [];
|
|
|
|
|
|
|
|
private conversionMap: Record<string, number> = { Bunch: 10, Bundle: 25, Bag: 100 };
|
|
private conversionMap: Record<string, number> = { Bunch: 10, Bundle: 25, Bag: 100 };
|
|
|
|
|
|
|
|
constructor(
|
|
constructor(
|
|
|
private fb: FormBuilder,
|
|
private fb: FormBuilder,
|
|
|
private http: HttpClient,
|
|
private http: HttpClient,
|
|
|
|
|
+ private siteService: SiteService,
|
|
|
private dialogRef: MatDialogRef<CreateFfbProductionDialogComponent>,
|
|
private dialogRef: MatDialogRef<CreateFfbProductionDialogComponent>,
|
|
|
private snackBar: MatSnackBar,
|
|
private snackBar: MatSnackBar,
|
|
|
- @Inject(MAT_DIALOG_DATA) public data: any
|
|
|
|
|
|
|
+ @Inject(MAT_DIALOG_DATA) public data: { harvest?: FFBProduction } // Updated type hint
|
|
|
) {
|
|
) {
|
|
|
- // Assign lists from parent
|
|
|
|
|
- this.allSites = data?.allSites || [];
|
|
|
|
|
- this.allPhases = data?.allPhases || [];
|
|
|
|
|
- this.allBlocks = data?.allBlocks || [];
|
|
|
|
|
- console.log(data)
|
|
|
|
|
-
|
|
|
|
|
this.form = this.fb.group({
|
|
this.form = this.fb.group({
|
|
|
productionDate: [new Date(), Validators.required],
|
|
productionDate: [new Date(), Validators.required],
|
|
|
- site: ['', Validators.required],
|
|
|
|
|
- phase: ['', Validators.required],
|
|
|
|
|
- block: ['', Validators.required],
|
|
|
|
|
|
|
+ site: [null, Validators.required],
|
|
|
|
|
+ phase: [null, Validators.required],
|
|
|
|
|
+ block: [null, Validators.required],
|
|
|
quantity: [0, Validators.required],
|
|
quantity: [0, Validators.required],
|
|
|
quantityUom: ['Bunch', Validators.required],
|
|
quantityUom: ['Bunch', Validators.required],
|
|
|
weight: [0, Validators.required],
|
|
weight: [0, Validators.required],
|
|
@@ -92,31 +83,98 @@ export class CreateFfbProductionDialogComponent implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ngOnInit() {
|
|
ngOnInit() {
|
|
|
|
|
+ this.loadSites();
|
|
|
|
|
+
|
|
|
// Patch data if editing
|
|
// Patch data if editing
|
|
|
if (this.data?.harvest) {
|
|
if (this.data?.harvest) {
|
|
|
- this.form.patchValue(this.data.harvest);
|
|
|
|
|
|
|
+ const harvest = this.data.harvest;
|
|
|
|
|
+
|
|
|
|
|
+ // We need to set the form values.
|
|
|
|
|
+ // Important: Since we are using objects for selection, we need to ensure references match or use compareWith
|
|
|
|
|
+ // But simpler is to rely on value binding if we use compareWith in template.
|
|
|
|
|
+ // Or we can just set the values.
|
|
|
|
|
+
|
|
|
|
|
+ this.form.patchValue({
|
|
|
|
|
+ productionDate: harvest.productionDate,
|
|
|
|
|
+ quantity: harvest.quantity,
|
|
|
|
|
+ quantityUom: harvest.quantityUom,
|
|
|
|
|
+ weight: harvest.weight,
|
|
|
|
|
+ weightUom: harvest.weightUom,
|
|
|
|
|
+ remarks: harvest.remarks
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 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.
|
|
|
|
|
+ this.form.patchValue({ site: harvest.site });
|
|
|
|
|
+
|
|
|
|
|
+ this.siteService.getPhasesBySite(harvest.site.id).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 => {
|
|
|
|
|
+ this.blocks = blocks;
|
|
|
|
|
+ this.form.patchValue({ block: harvest.block });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loadSites() {
|
|
|
|
|
+ this.siteService.getSites().subscribe(sites => {
|
|
|
|
|
+ this.sites = sites;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Autocomplete observables
|
|
|
|
|
- this.filteredSites = this.siteControl.valueChanges.pipe(
|
|
|
|
|
- startWith(this.siteControl.value || ''),
|
|
|
|
|
- map(val => this.filterOptions(val, this.allSites))
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- this.filteredPhases = this.phaseControl.valueChanges.pipe(
|
|
|
|
|
- startWith(this.phaseControl.value || ''),
|
|
|
|
|
- map(val => this.filterOptions(val, this.allPhases))
|
|
|
|
|
- );
|
|
|
|
|
-
|
|
|
|
|
- this.filteredBlocks = this.blockControl.valueChanges.pipe(
|
|
|
|
|
- startWith(this.blockControl.value || ''),
|
|
|
|
|
- map(val => this.filterOptions(val, this.allBlocks))
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ fetchPhases(siteId: string) {
|
|
|
|
|
+ this.siteService.getPhasesBySite(siteId).subscribe(phases => {
|
|
|
|
|
+ this.phases = phases;
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private filterOptions(value: string, options: string[]): string[] {
|
|
|
|
|
- const filterValue = (value || '').toLowerCase();
|
|
|
|
|
- return options.filter(opt => opt.toLowerCase().includes(filterValue));
|
|
|
|
|
|
|
+ onPhaseChange(phase: any) {
|
|
|
|
|
+ this.blocks = [];
|
|
|
|
|
+ this.form.patchValue({ block: null });
|
|
|
|
|
+
|
|
|
|
|
+ if (phase && phase._id) {
|
|
|
|
|
+ this.fetchBlocks(phase._id);
|
|
|
|
|
+ } else if (phase && phase.id) {
|
|
|
|
|
+ this.fetchBlocks(phase.id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ fetchBlocks(phaseId: string) {
|
|
|
|
|
+ this.siteService.getBlocksByPhase(phaseId).subscribe(blocks => {
|
|
|
|
|
+ this.blocks = blocks;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Helper for compareWith in mat-select to handle {id, name} equality
|
|
|
|
|
+ compareFn(o1: any, o2: any): boolean {
|
|
|
|
|
+ if (!o1 || !o2) return false;
|
|
|
|
|
+ // Handle both _id (DB) and id (FFBProduction view) keys if mixed
|
|
|
|
|
+ const id1 = o1.id || o1._id;
|
|
|
|
|
+ const id2 = o2.id || o2._id;
|
|
|
|
|
+ return id1 === id2;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private updateWeight() {
|
|
private updateWeight() {
|
|
@@ -127,14 +185,13 @@ export class CreateFfbProductionDialogComponent implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
generateRemark() {
|
|
generateRemark() {
|
|
|
- this.generatingRemark = true; // Use saving flag to show loading state if needed, or create a separate one
|
|
|
|
|
|
|
+ this.generatingRemark = true;
|
|
|
const payload = this.form.value;
|
|
const payload = this.form.value;
|
|
|
|
|
|
|
|
- // Construct a context object for the LLM based on available form data
|
|
|
|
|
const context = {
|
|
const context = {
|
|
|
- site: payload.site,
|
|
|
|
|
- phase: payload.phase,
|
|
|
|
|
- block: payload.block,
|
|
|
|
|
|
|
+ site: payload.site?.name,
|
|
|
|
|
+ phase: payload.phase?.name,
|
|
|
|
|
+ block: payload.block?.name,
|
|
|
date: payload.productionDate,
|
|
date: payload.productionDate,
|
|
|
quantity: payload.quantity,
|
|
quantity: payload.quantity,
|
|
|
quantityUom: payload.quantityUom,
|
|
quantityUom: payload.quantityUom,
|
|
@@ -157,35 +214,46 @@ export class CreateFfbProductionDialogComponent implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
randomize() {
|
|
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'];
|
|
|
|
|
|
|
+ // Randomize needs to pick valid existing sites now
|
|
|
|
|
+ if (this.sites.length === 0) {
|
|
|
|
|
+ this.snackBar.open('No sites available to randomize', 'Close', { duration: 2000 });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const sites = [...new Set([...this.allSites, ...defaultSites])];
|
|
|
|
|
- const phases = [...new Set([...this.allPhases, ...defaultPhases])];
|
|
|
|
|
- const blocks = [...new Set([...this.allBlocks, ...defaultBlocks])];
|
|
|
|
|
|
|
+ const randomSite = this.getRandomItem(this.sites);
|
|
|
|
|
+ // trigger selection logic
|
|
|
|
|
+ this.form.patchValue({ site: randomSite });
|
|
|
|
|
+
|
|
|
|
|
+ // We need to fetch phases for this random site
|
|
|
|
|
+ this.siteService.getPhasesBySite(randomSite._id!).subscribe(phases => {
|
|
|
|
|
+ this.phases = phases;
|
|
|
|
|
+ if (phases.length > 0) {
|
|
|
|
|
+ const randomPhase = this.getRandomItem(phases);
|
|
|
|
|
+ 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) });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- // Date between 1/1/2025 and 31/12/2026
|
|
|
|
|
const start = new Date('2025-01-01').getTime();
|
|
const start = new Date('2025-01-01').getTime();
|
|
|
const end = new Date('2026-12-31').getTime();
|
|
const end = new Date('2026-12-31').getTime();
|
|
|
const randomDate = new Date(start + Math.random() * (end - start));
|
|
const randomDate = new Date(start + Math.random() * (end - start));
|
|
|
|
|
|
|
|
const quantity = Math.floor(Math.random() * (100 - 10 + 1)) + 10;
|
|
const quantity = Math.floor(Math.random() * (100 - 10 + 1)) + 10;
|
|
|
- const quantityUom = 'Bunch';
|
|
|
|
|
|
|
|
|
|
this.form.patchValue({
|
|
this.form.patchValue({
|
|
|
- site: this.getRandomItem(sites),
|
|
|
|
|
- phase: this.getRandomItem(phases),
|
|
|
|
|
- block: this.getRandomItem(blocks),
|
|
|
|
|
productionDate: randomDate,
|
|
productionDate: randomDate,
|
|
|
quantity: quantity,
|
|
quantity: quantity,
|
|
|
- quantityUom: quantityUom
|
|
|
|
|
|
|
+ quantityUom: 'Bunch' // Default
|
|
|
});
|
|
});
|
|
|
-
|
|
|
|
|
- // Weight auto-updates via subscription
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private getRandomItem(arr: string[]) {
|
|
|
|
|
|
|
+ private getRandomItem(arr: any[]) {
|
|
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -204,13 +272,51 @@ export class CreateFfbProductionDialogComponent implements OnInit {
|
|
|
onSubmit() {
|
|
onSubmit() {
|
|
|
if (this.form.invalid) return;
|
|
if (this.form.invalid) return;
|
|
|
|
|
|
|
|
- const payload = {
|
|
|
|
|
- ...this.form.value,
|
|
|
|
|
- productionDate: new Date(this.form.value.productionDate).toISOString(),
|
|
|
|
|
|
|
+ const formVal = this.form.value;
|
|
|
|
|
+
|
|
|
|
|
+ // Prepare payload with { id, name } structure
|
|
|
|
|
+ // formVal.site is the full Site object from DB (has _id, name, etc)
|
|
|
|
|
+
|
|
|
|
|
+ const payload: FFBProduction = {
|
|
|
|
|
+ ...formVal,
|
|
|
|
|
+ productionDate: new Date(formVal.productionDate).toISOString(),
|
|
|
|
|
+ site: { id: formVal.site._id || formVal.site.id, name: formVal.site.name },
|
|
|
|
|
+ phase: { id: formVal.phase._id || formVal.phase.id, name: formVal.phase.name },
|
|
|
|
|
+ block: { id: formVal.block._id || formVal.block.id, name: formVal.block.name }
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ // If editing, we need the ID?
|
|
|
|
|
+ // The create endpoint is POST, update is usually PUT.
|
|
|
|
|
+ // The current code used POST for create.
|
|
|
|
|
+ // existing 'onSubmit' logic was: Post to /api/ffb-production
|
|
|
|
|
+ // Does it handle update?
|
|
|
|
|
+ // In original code: `editProduction` opened the dialog with data.harvest.
|
|
|
|
|
+ // But `onSubmit` did logic: `http.post(exposedUrl + '/api/ffb-production')`.
|
|
|
|
|
+ // It seems the original code MIGHT have been missing the Update logic in onSubmit or the backend handles it via _id if present?
|
|
|
|
|
+ // Checking original code again... line 213: `http.post`.
|
|
|
|
|
+ // Wait, `editProduction` in parent component (Step 5, line 236) just opens dialog with harvest data.
|
|
|
|
|
+ // `CreateFfbProductionDialogComponent` (Step 20) line 213 does POST.
|
|
|
|
|
+ // This implies the original 'Edit' might have just been creating a NEW record with pre-filled data?
|
|
|
|
|
+ // OR the backend POST handles upsert?
|
|
|
|
|
+ // A typical REST API: POST is create. PUT is update.
|
|
|
|
|
+ // I should probably check if I need to support PUT.
|
|
|
|
|
+ // Let's assume for now I should use the same logic as before (POST), but if I am editing, I should probably use PUT if I had the ID.
|
|
|
|
|
+ // The user's prompt in Step 0 showed backend controllers for site, phase, block.
|
|
|
|
|
+ // The user didn't show ffb-production backend.
|
|
|
|
|
+
|
|
|
|
|
+ // IMPORTANT: If `data.harvest` has `_id`, we should probably do PUT.
|
|
|
|
|
+ // I'll implement PUT if `data.harvest._id` exists, else POST.
|
|
|
|
|
+
|
|
|
this.saving = true;
|
|
this.saving = true;
|
|
|
- this.http.post(`${webConfig.exposedUrl}/api/ffb-production`, payload).subscribe({
|
|
|
|
|
|
|
+
|
|
|
|
|
+ let req;
|
|
|
|
|
+ if (this.data?.harvest?._id) {
|
|
|
|
|
+ req = this.http.put(`${webConfig.exposedUrl}/api/ffb-production/${this.data.harvest._id}`, payload);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ req = this.http.post(`${webConfig.exposedUrl}/api/ffb-production`, payload);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req.subscribe({
|
|
|
next: () => {
|
|
next: () => {
|
|
|
this.snackBar.open('FFB Production saved!', 'Close', { duration: 3000 });
|
|
this.snackBar.open('FFB Production saved!', 'Close', { duration: 3000 });
|
|
|
this.dialogRef.close('refresh');
|
|
this.dialogRef.close('refresh');
|