Browse Source

Cereated dialog and page enhancements

Dr-Swopt 2 months ago
parent
commit
c9ab09ab4c

+ 19 - 5
package-lock.json

@@ -28,6 +28,7 @@
         "rxjs": "~7.8.2",
         "socket.io-client": "^4.8.1",
         "tslib": "^2.8.1",
+        "uuid": "^13.0.0",
         "zone.js": "~0.15.1"
       },
       "devDependencies": {
@@ -13721,6 +13722,16 @@
         "websocket-driver": "^0.7.4"
       }
     },
+    "node_modules/sockjs/node_modules/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/socks": {
       "version": "2.8.5",
       "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.5.tgz",
@@ -14641,13 +14652,16 @@
       }
     },
     "node_modules/uuid": {
-      "version": "8.3.2",
-      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
-      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
-      "dev": true,
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+      "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
       "license": "MIT",
       "bin": {
-        "uuid": "dist/bin/uuid"
+        "uuid": "dist-node/bin/uuid"
       }
     },
     "node_modules/validate-npm-package-license": {

+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "rxjs": "~7.8.2",
     "socket.io-client": "^4.8.1",
     "tslib": "^2.8.1",
+    "uuid": "^13.0.0",
     "zone.js": "~0.15.1"
   },
   "devDependencies": {

+ 29 - 0
src/app/activity/activity.component.css

@@ -0,0 +1,29 @@
+.activity-container {
+  padding: 1.5rem;
+}
+
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 1.5rem;
+}
+
+table {
+  width: 100%;
+}
+
+mat-progress-spinner {
+  display: block;
+  margin: 2rem auto;
+}
+
+/* activity.component.css */
+.clickable-row {
+  cursor: pointer;
+  transition: background-color 0.2s;
+}
+
+.clickable-row:hover {
+  background-color: #f5f5f5;
+}

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

@@ -0,0 +1,49 @@
+<div class="activity-container">
+  <div class="toolbar">
+    <h2>Activities</h2>
+    <button mat-flat-button color="primary" (click)="openCreateDialog()">
+      <mat-icon>add</mat-icon>
+      New Activity
+    </button>
+  </div>
+
+  <mat-progress-spinner *ngIf="loading" mode="indeterminate" diameter="48"></mat-progress-spinner>
+
+  <table mat-table [dataSource]="dataSource" class="mat-elevation-z8" *ngIf="!loading">
+    <ng-container matColumnDef="name">
+      <th mat-header-cell *matHeaderCellDef>Name</th>
+      <td mat-cell *matCellDef="let row">{{ row.name }}</td>
+    </ng-container>
+
+    <ng-container matColumnDef="type">
+      <th mat-header-cell *matHeaderCellDef>Type</th>
+      <td mat-cell *matCellDef="let row">{{ row.type }}</td>
+    </ng-container>
+
+    <ng-container matColumnDef="dateStart">
+      <th mat-header-cell *matHeaderCellDef>Start</th>
+      <td mat-cell *matCellDef="let row">
+        {{ row.dateStart | date: 'shortDate' }}
+      </td>
+    </ng-container>
+
+    <ng-container matColumnDef="dateEnd">
+      <th mat-header-cell *matHeaderCellDef>End</th>
+      <td mat-cell *matCellDef="let row">
+        {{ row.dateEnd | date: 'shortDate' }}
+      </td>
+    </ng-container>
+
+    <ng-container matColumnDef="actions">
+      <th mat-header-cell *matHeaderCellDef></th>
+      <td mat-cell *matCellDef="let row">
+        <button mat-icon-button color="warn">
+          <mat-icon (click)="deleteActivity(row._id); $event.stopPropagation()">delete</mat-icon>
+        </button>
+      </td>
+    </ng-container>
+
+    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+    <tr mat-row *matRowDef="let row; columns: displayedColumns" (click)="onRowClick(row)" class="clickable-row"></tr>
+  </table>
+</div>

+ 98 - 0
src/app/activity/activity.component.ts

@@ -0,0 +1,98 @@
+import { Component, OnInit, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDialog, MatDialogModule } from '@angular/material/dialog';
+import { MatTableModule } from '@angular/material/table';
+import { MatIconModule } from '@angular/material/icon';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { webConfig } from '../config';
+import { CreateActivityDialogComponent } from '../components/activity-dialog/create-activity-dialog.component';
+
+@Component({
+  selector: 'app-activity',
+  standalone: true,
+  imports: [
+    CommonModule,
+    HttpClientModule,
+    MatButtonModule,
+    MatDialogModule,
+    MatTableModule,
+    MatIconModule,
+    MatProgressSpinnerModule,
+    CreateActivityDialogComponent,
+  ],
+  templateUrl: './activity.component.html',
+  styleUrls: ['./activity.component.css'],
+})
+export class ActivityComponent implements OnInit {
+  private http = inject(HttpClient);
+  private dialog = inject(MatDialog);
+
+  displayedColumns: string[] = [
+    'name',
+    'type',
+    'dateStart',
+    'dateEnd',
+    'actions',
+  ];
+  dataSource: any[] = [];
+  loading = false;
+
+  ngOnInit() {
+    this.loadActivities();
+  }
+
+  loadActivities() {
+    this.loading = true;
+    this.http.get(`${webConfig.exposedUrl}/api/activity`).subscribe({
+      next: (data: any) => {
+        this.dataSource = data;
+        this.loading = false;
+      },
+      error: (err) => {
+        console.error('Failed to fetch activities', err);
+        this.loading = false;
+      },
+    });
+  }
+
+  onRowClick(row: any): void {
+    console.log('Clicked row:', row);
+    this.openEditDialog(row);
+  }
+
+  openCreateDialog() {
+    const dialogRef = this.dialog.open(CreateActivityDialogComponent, {
+      width: '600px',
+    });
+
+    dialogRef.afterClosed().subscribe((result) => {
+      if (result === 'refresh') {
+        this.loadActivities();
+      }
+    });
+  }
+
+  openEditDialog(activity: any) {
+    const dialogRef = this.dialog.open(CreateActivityDialogComponent, {
+      width: '600px',
+      data: activity, // pass existing activity data
+    });
+
+    dialogRef.afterClosed().subscribe((result) => {
+      if (result === 'refresh') {
+        this.loadActivities();
+      }
+    });
+  }
+
+  deleteActivity(id: string) {
+    if (confirm('Are you sure you want to delete this activity?')) {
+      this.http.delete(`${webConfig.exposedUrl}/api/activity/${id}`).subscribe({
+        next: () => this.loadActivities(),
+        error: (err) => console.error('Failed to delete activity', err),
+      });
+    }
+  }
+}

+ 58 - 0
src/app/components/activity-dialog/create-activity-dialog.component.css

@@ -0,0 +1,58 @@
+.dialog-form {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+  padding: 16px;
+  max-height: 80vh;
+  overflow-y: auto;
+}
+
+.form-section {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.row {
+  display: flex;
+  gap: 16px;
+  flex-wrap: wrap;
+}
+
+.flex-1 {
+  flex: 1 1 0;
+}
+
+.full-width {
+  width: 100%;
+}
+
+.accordion {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.array-container {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  margin-top: 12px;
+}
+
+.form-array-item {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+  padding: 12px;
+  background: #fafafa;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+  margin-top: 16px;
+}

+ 184 - 0
src/app/components/activity-dialog/create-activity-dialog.component.html

@@ -0,0 +1,184 @@
+<h2 mat-dialog-title>
+  {{ data ? 'Edit Activity' : 'Create Activity' }}
+</h2>
+
+<form [formGroup]="form" (ngSubmit)="onSubmit()" class="dialog-form">
+  <div class="form-section">
+    <mat-form-field appearance="outline" class="full-width">
+      <mat-label>Activity Name</mat-label>
+      <input matInput formControlName="name" placeholder="e.g. FFB Harvest" />
+    </mat-form-field>
+
+    <mat-form-field appearance="outline" class="full-width">
+      <mat-label>Type</mat-label>
+      <input matInput formControlName="type" placeholder="e.g. actual" />
+    </mat-form-field>
+
+    <div class="row">
+      <mat-form-field appearance="outline" class="flex-1">
+        <mat-label>Start Date</mat-label>
+        <input matInput [matDatepicker]="picker1" formControlName="dateStart" />
+        <mat-datepicker-toggle matIconSuffix [for]="picker1"></mat-datepicker-toggle>
+        <mat-datepicker #picker1></mat-datepicker>
+      </mat-form-field>
+
+      <mat-form-field appearance="outline" class="flex-1">
+        <mat-label>End Date</mat-label>
+        <input matInput [matDatepicker]="picker2" formControlName="dateEnd" />
+        <mat-datepicker-toggle matIconSuffix [for]="picker2"></mat-datepicker-toggle>
+        <mat-datepicker #picker2></mat-datepicker>
+      </mat-form-field>
+    </div>
+  </div>
+
+  <!-- Collapsible Sections -->
+  <mat-accordion multi class="accordion">
+    <!-- Resources -->
+    <mat-expansion-panel>
+      <mat-expansion-panel-header>
+        <mat-panel-title>Resources</mat-panel-title>
+        <mat-panel-description>{{ resources.length }} item(s)</mat-panel-description>
+      </mat-expansion-panel-header>
+
+      <div formArrayName="resources" class="array-container">
+        <button mat-stroked-button color="primary" type="button" (click)="addResource()">
+          + Add Resource
+        </button>
+
+        <div *ngFor="let r of resources.controls; let i = index" [formGroupName]="i" class="form-array-item">
+          <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Type</mat-label>
+              <input matInput formControlName="type" />
+            </mat-form-field>
+
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Name</mat-label>
+              <input matInput formControlName="name" />
+            </mat-form-field>
+          </div>
+
+          <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Quantity</mat-label>
+              <input matInput type="number" formControlName="quantity" />
+            </mat-form-field>
+
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>UOM</mat-label>
+              <input matInput formControlName="uom" />
+            </mat-form-field>
+          </div>
+
+          <button mat-icon-button color="warn" type="button" (click)="removeResource(i)">
+            <mat-icon>delete</mat-icon>
+          </button>
+        </div>
+      </div>
+    </mat-expansion-panel>
+
+    <!-- Outputs -->
+    <mat-expansion-panel>
+      <mat-expansion-panel-header>
+        <mat-panel-title>Outputs</mat-panel-title>
+        <mat-panel-description>{{ outputs.length }} item(s)</mat-panel-description>
+      </mat-expansion-panel-header>
+
+      <div formArrayName="outputs" class="array-container">
+        <button mat-stroked-button color="primary" type="button" (click)="addOutput()">+ Add Output</button>
+
+        <div *ngFor="let o of outputs.controls; let i = index" [formGroupName]="i" class="form-array-item">
+          <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Type</mat-label>
+              <input matInput formControlName="type" />
+            </mat-form-field>
+
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Name</mat-label>
+              <input matInput formControlName="name" />
+            </mat-form-field>
+          </div>
+
+          <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Quantity</mat-label>
+              <input matInput type="number" formControlName="quantity" />
+            </mat-form-field>
+
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>UOM</mat-label>
+              <input matInput formControlName="uom" />
+            </mat-form-field>
+          </div>
+
+          <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Weight</mat-label>
+              <input matInput type="number" formControlName="weight" />
+            </mat-form-field>
+
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Weight UOM</mat-label>
+              <input matInput formControlName="weightUom" />
+            </mat-form-field>
+          </div>
+
+          <button mat-icon-button color="warn" type="button" (click)="removeOutput(i)">
+            <mat-icon>delete</mat-icon>
+          </button>
+        </div>
+      </div>
+    </mat-expansion-panel>
+
+    <!-- Targets -->
+    <mat-expansion-panel>
+      <mat-expansion-panel-header>
+        <mat-panel-title>Targets</mat-panel-title>
+        <mat-panel-description>{{ targets.length }} item(s)</mat-panel-description>
+      </mat-expansion-panel-header>
+
+      <div formArrayName="targets" class="array-container">
+        <button mat-stroked-button color="primary" type="button" (click)="addTarget()">+ Add Target</button>
+
+        <div *ngFor="let t of targets.controls; let i = index" [formGroupName]="i" class="form-array-item">
+          <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Type</mat-label>
+              <input matInput formControlName="type" />
+            </mat-form-field>
+
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Name</mat-label>
+              <input matInput formControlName="name" />
+            </mat-form-field>
+          </div>
+
+          <div class="row">
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>Quantity</mat-label>
+              <input matInput type="number" formControlName="quantity" />
+            </mat-form-field>
+
+            <mat-form-field appearance="outline" class="flex-1">
+              <mat-label>UOM</mat-label>
+              <input matInput formControlName="uom" />
+            </mat-form-field>
+          </div>
+
+          <button mat-icon-button color="warn" type="button" (click)="removeTarget(i)">
+            <mat-icon>delete</mat-icon>
+          </button>
+        </div>
+      </div>
+    </mat-expansion-panel>
+  </mat-accordion>
+
+  <!-- Footer -->
+  <div class="dialog-footer">
+    <button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
+    <button mat-flat-button color="primary" type="submit">
+      {{ data ? 'Update' : 'Save' }}
+    </button>
+  </div>
+</form>

+ 269 - 0
src/app/components/activity-dialog/create-activity-dialog.component.ts

@@ -0,0 +1,269 @@
+
+// create-activity-dialog.component.timport { Component, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {
+  FormBuilder,
+  FormGroup,
+  FormArray,
+  Validators,
+  ReactiveFormsModule,
+} from '@angular/forms';
+import {
+  MatDialogRef,
+  MAT_DIALOG_DATA,
+  MatDialogModule,
+} from '@angular/material/dialog';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatButtonModule } from '@angular/material/button';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatNativeDateModule } from '@angular/material/core';
+import { MatIconModule } from '@angular/material/icon';
+import { MatSelectModule } from '@angular/material/select';
+import { HttpClient, HttpClientModule } from '@angular/common/http';
+import { webConfig } from '../../config'; // adjust path if needed
+import { MatExpansionModule } from '@angular/material/expansion';
+import { Component, Inject } from '@angular/core';
+
+@Component({
+  selector: 'app-create-activity-dialog',
+  standalone: true,
+  imports: [
+    CommonModule,
+    ReactiveFormsModule,
+    HttpClientModule,
+    MatDialogModule,
+    MatFormFieldModule,
+    MatInputModule,
+    MatButtonModule,
+    MatDatepickerModule,
+    MatNativeDateModule,
+    MatIconModule,
+    MatSelectModule,
+    MatExpansionModule,
+  ],
+  templateUrl: './create-activity-dialog.component.html',
+  styleUrls: ['./create-activity-dialog.component.css'],
+})
+export class CreateActivityDialogComponent {
+  form: FormGroup;
+  saving = false;
+
+  constructor(
+    private fb: FormBuilder,
+    private http: HttpClient,
+    private dialogRef: MatDialogRef<CreateActivityDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {
+    this.form = this.fb.group({
+      name: ['', Validators.required],
+      type: ['actual', Validators.required],
+      dateStart: [new Date(), Validators.required],
+      dateEnd: [new Date(), Validators.required],
+      duration: this.fb.group({
+        quantity: [0, Validators.required],
+        uom: ['hours', Validators.required],
+      }),
+      resources: this.fb.array([]),
+      outputs: this.fb.array([]),
+      targets: this.fb.array([]),
+    });
+  }
+  ngOnInit() {
+    if (this.data) {
+      this.form.patchValue({
+        name: this.data.name,
+        type: this.data.type,
+        dateStart: this.data.dateStart,
+        dateEnd: this.data.dateEnd,
+      });
+
+      // ✅ Clear existing arrays just in case
+      this.resources.clear();
+      this.outputs.clear();
+      this.targets.clear();
+
+      // ✅ Populate resources
+      if (this.data.resources && Array.isArray(this.data.resources)) {
+        this.data.resources.forEach((r: any) => {
+          this.resources.push(
+            this.fb.group({
+              type: [r.type, Validators.required],
+              name: [r.name, Validators.required],
+              quantity: [r.value?.quantity || 0, Validators.required],
+              uom: [r.value?.uom || '', Validators.required],
+            })
+          );
+        });
+      }
+
+      // ✅ Populate outputs
+      if (this.data.outputs && Array.isArray(this.data.outputs)) {
+        this.data.outputs.forEach((o: any) => {
+          this.outputs.push(
+            this.fb.group({
+              type: [o.type, Validators.required],
+              name: [o.name, Validators.required],
+              quantity: [o.value?.quantity || 0, Validators.required],
+              uom: [o.value?.uom || '', Validators.required],
+              weight: [o.weightValue?.weight || 0, Validators.required],
+              weightUom: [o.weightValue?.uom || '', Validators.required],
+            })
+          );
+        });
+      }
+
+      // ✅ Populate targets
+      if (this.data.targets && Array.isArray(this.data.targets)) {
+        this.data.targets.forEach((t: any) => {
+          this.targets.push(
+            this.fb.group({
+              type: [t.type, Validators.required],
+              name: [t.name, Validators.required],
+              quantity: [t.value?.quantity || 0, Validators.required],
+              uom: [t.value?.uom || '', Validators.required],
+            })
+          );
+        });
+      }
+    }
+  }
+
+  get resources(): FormArray {
+    return this.form.get('resources') as FormArray;
+  }
+  get outputs(): FormArray {
+    return this.form.get('outputs') as FormArray;
+  }
+  get targets(): FormArray {
+    return this.form.get('targets') as FormArray;
+  }
+
+  addResource() {
+    this.resources.push(
+      this.fb.group({
+        type: ['worker', Validators.required],
+        name: ['', Validators.required],
+        quantity: [1, Validators.required],
+        uom: ['hour', Validators.required],
+      })
+    );
+  }
+
+  addOutput() {
+    this.outputs.push(
+      this.fb.group({
+        type: ['ffb harvested', Validators.required],
+        name: ['', Validators.required],
+        quantity: [0, Validators.required],
+        uom: ['bunch', Validators.required],
+        weight: [0, Validators.required],
+        weightUom: ['kg', Validators.required],
+      })
+    );
+  }
+
+  addTarget() {
+    this.targets.push(
+      this.fb.group({
+        type: ['location', Validators.required],
+        name: ['', Validators.required],
+        quantity: [0, Validators.required],
+        uom: ['acres', Validators.required],
+      })
+    );
+  }
+
+  removeTarget(i: number) {
+    this.targets.removeAt(i);
+  }
+
+  removeOutput(i: number) {
+    this.outputs.removeAt(i);
+  }
+
+  removeResource(i: number) {
+    this.resources.removeAt(i);
+  }
+
+  removeItem(array: FormArray, index: number) {
+    array.removeAt(index);
+  }
+
+  onSubmit() {
+    if (this.form.invalid) return;
+
+    const formValue = this.form.value;
+
+    const activityData = {
+      name: formValue.name,
+      type: formValue.type,
+      dateStart: new Date(formValue.dateStart).toISOString(),
+      dateEnd: new Date(formValue.dateEnd).toISOString(),
+
+      duration: {
+        value: {
+          quantity: formValue.durationQuantity || 0,
+          uom: formValue.durationUom || 'hours',
+        },
+      },
+
+      resources: formValue.resources.map((r: any) => ({
+        type: r.type,
+        name: r.name,
+        value: {
+          quantity: r.quantity,
+          uom: r.uom,
+        },
+      })),
+
+      outputs: formValue.outputs.map((o: any) => ({
+        type: o.type,
+        name: o.name,
+        value: {
+          quantity: o.quantity,
+          uom: o.uom,
+        },
+        weightValue: {
+          weight: o.weight,
+          uom: o.weightUom,
+        },
+      })),
+
+      targets: formValue.targets.map((t: any) => ({
+        type: t.type,
+        name: t.name,
+        value: {
+          quantity: t.quantity,
+          uom: t.uom,
+        },
+      })),
+    };
+
+    console.log('Final payload sent:', activityData);
+
+    const isEditMode = !!this.data;
+    const url = isEditMode
+      ? `${webConfig.exposedUrl}/api/activity/${this.data._id}`
+      : `${webConfig.exposedUrl}/api/activity`;
+    const httpMethod = isEditMode ? 'put' : 'post';
+
+    this.http[httpMethod](url, activityData).subscribe({
+      next: () => {
+        console.log(isEditMode ? 'Activity updated.' : 'Activity created.');
+        this.dialogRef.close('refresh');
+      },
+      error: (err) => {
+        console.error(
+          isEditMode ? 'Failed to update activity:' : 'Failed to create activity:',
+          err
+        );
+      },
+    });
+  }
+
+
+  cancel() {
+    this.dialogRef.close();
+  }
+}

+ 11 - 0
src/app/components/assign-task/task-assign-dialog.component.css

@@ -0,0 +1,11 @@
+.dialog-content {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  width: 100%;
+  min-width: 300px;
+}
+
+.full-width {
+  width: 100%;
+}

+ 36 - 0
src/app/components/assign-task/task-assign-dialog.component.html

@@ -0,0 +1,36 @@
+<h2 mat-dialog-title>Assign Task to {{ data.workerName }}</h2>
+
+<mat-dialog-content [formGroup]="taskForm" class="dialog-content">
+  <mat-form-field appearance="outline" class="full-width">
+    <mat-label>Task Description</mat-label>
+    <input matInput formControlName="description" placeholder="Describe the task" />
+  </mat-form-field>
+
+  <mat-form-field appearance="outline" class="full-width">
+    <mat-label>Due Date</mat-label>
+    <input matInput [matDatepicker]="picker" formControlName="date" />
+    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
+    <mat-datepicker #picker></mat-datepicker>
+  </mat-form-field>
+
+  <mat-form-field appearance="outline" class="full-width">
+    <mat-label>Status</mat-label>
+    <mat-select formControlName="status">
+      <mat-option *ngFor="let s of statuses" [value]="s">{{ s }}</mat-option>
+    </mat-select>
+  </mat-form-field>
+
+  <mat-form-field appearance="outline" class="full-width">
+    <mat-label>Priority</mat-label>
+    <mat-select formControlName="priority">
+      <mat-option *ngFor="let p of priorities" [value]="p">{{ p }}</mat-option>
+    </mat-select>
+  </mat-form-field>
+</mat-dialog-content>
+
+<mat-dialog-actions align="end">
+  <button mat-button (click)="cancel()">Cancel</button>
+  <button mat-flat-button color="primary" (click)="submit()" [disabled]="!taskForm.valid">
+    Assign
+  </button>
+</mat-dialog-actions>

+ 80 - 0
src/app/components/assign-task/task-assign-dialog.component.ts

@@ -0,0 +1,80 @@
+import { Component, Inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
+import { MatButtonModule } from '@angular/material/button';
+import { MatInputModule } from '@angular/material/input';
+import { MatSelectModule } from '@angular/material/select';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { MatNativeDateModule } from '@angular/material/core';
+
+export interface Task {
+  id?: string;
+  description: string;
+  date: Date;
+  status: 'PENDING' | 'WIP' | 'DONE';
+  priority?: 'LOW' | 'MEDIUM' | 'HIGH';
+}
+
+export interface TaskDialogData {
+  workerId: string;
+  workerName: string;
+}
+
+export interface TaskFormData {
+  id: string;
+  description: string;
+  date: Date;
+  status: 'PENDING' | 'WIP' | 'DONE';
+  priority?: 'LOW' | 'MEDIUM' | 'HIGH';
+}
+
+@Component({
+  selector: 'app-task-assign-dialog',
+  standalone: true,
+  templateUrl: './task-assign-dialog.component.html',
+  styleUrls: ['./task-assign-dialog.component.css'],
+  imports: [
+    CommonModule,
+    ReactiveFormsModule,
+    MatDialogModule,
+    MatButtonModule,
+    MatInputModule,
+    MatSelectModule,
+    MatDatepickerModule,
+    MatNativeDateModule,
+  ],
+})
+export class TaskAssignDialogComponent {
+  taskForm: FormGroup;
+
+  statuses = ['PENDING', 'WIP', 'DONE'];
+  priorities = ['LOW', 'MEDIUM', 'HIGH'];
+
+  constructor(
+    private fb: FormBuilder,
+    private dialogRef: MatDialogRef<TaskAssignDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: TaskDialogData
+  ) {
+    this.taskForm = this.fb.group({
+      description: ['', Validators.required],
+      date: ['', Validators.required],
+      status: ['PENDING', Validators.required],
+      priority: ['MEDIUM'],
+    });
+  }
+
+  submit() {
+    if (this.taskForm.valid) {
+      const task: Task = {
+        id: Date.now().toString(),
+        ...this.taskForm.value,
+      };
+      this.dialogRef.close(task);
+    }
+  }
+
+  cancel() {
+    this.dialogRef.close(null);
+  }
+}

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

@@ -8,12 +8,15 @@
     <app-dashboard-home></app-dashboard-home>
   </mat-tab>
 
-  <mat-tab label="Attendance">
+  <!-- <mat-tab label="Attendance">
     <app-attendance></app-attendance>
   </mat-tab>
 
   <mat-tab label="Payment">
     <app-payment></app-payment>
+  </mat-tab> -->
+  <mat-tab label="Activities">
+    <app-activity></app-activity>
   </mat-tab>
 
   <mat-tab label="Plantation">

+ 3 - 1
src/app/dashboard/dashboard.component.ts

@@ -8,6 +8,7 @@ import { AttendanceComponent } from '../attendance/attendance.component';
 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';
 
 @Component({
   selector: 'app-dashboard',
@@ -20,7 +21,8 @@ import { PlantationTreeComponent } from "../plantation/plantation-tree.component
     DashboardHomeComponent,
     PaymentComponent,
     AttendanceComponent,
-    PlantationTreeComponent
+    PlantationTreeComponent,
+    ActivityComponent
 ],
   templateUrl: './dashboard.component.html',
   styleUrls: ['./dashboard.component.css']

+ 34 - 0
src/app/plantation/plantation-tree.component.css

@@ -82,3 +82,37 @@
 .workers-section li {
   padding: 2px 0;
 }
+
+.worker-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  margin-top: 4px;
+}
+
+.worker-details {
+  margin-left: 40px;
+  background: #f0f0f0;
+  padding: 6px 10px;
+  border-radius: 6px;
+}
+
+.task-list {
+  margin-left: 20px;
+  list-style: none;
+  padding: 0;
+}
+
+.task-list li {
+  margin: 3px 0;
+  font-size: 13px;
+}
+
+.task-list .pending { color: #b36b00; }
+.task-list .wip { color: #007bff; }
+.task-list .done { color: #28a745; }
+
+.priority-tag {
+  font-size: 11px;
+  color: #555;
+}

+ 33 - 1
src/app/plantation/plantation-tree.component.html

@@ -61,10 +61,42 @@
         <strong>Workers:</strong>
         <ul>
           <li *ngFor="let w of node.data.workers">
-            👷‍♂️ {{ w.name }} ({{ w.role || 'Worker' }}) – {{ w.personCode }}
+            <div class="worker-row">
+              <button mat-icon-button (click)="toggleWorkerDetails(w)">
+                <mat-icon>
+                  {{ isWorkerExpanded(w) ? 'expand_more' : 'chevron_right' }}
+                </mat-icon>
+              </button>
+              👷‍♂️ <strong>{{ w.name }}</strong> ({{ w.role || 'Worker' }}) – {{ w.personCode }}
+              <button mat-icon-button color="accent" matTooltip="Assign Task" (click)="assignTask(w)">
+                <mat-icon>assignment</mat-icon>
+              </button>
+            </div>
+
+            <!-- Worker details collapsible -->
+            <div *ngIf="isWorkerExpanded(w)" class="worker-details">
+              <p><strong>DOB:</strong> {{ w.DOB | date }}</p>
+              <p><strong>Nationality:</strong> {{ w.nationality }}</p>
+
+              <div *ngIf="w.tasks?.length">
+                <strong>Tasks:</strong>
+                <ul class="task-list">
+                  <li *ngFor="let t of w.tasks">
+                    🧾 {{ t.description }} —
+                    <em>{{ t.date | date }}</em> —
+                    <span [ngClass]="t.status.toLowerCase()">{{ t.status }}</span>
+                    <span *ngIf="t.priority" class="priority-tag">({{ t.priority }})</span>
+                  </li>
+                </ul>
+              </div>
+              <div *ngIf="!w.tasks?.length">
+                <em>No tasks assigned yet.</em>
+              </div>
+            </div>
           </li>
         </ul>
       </div>
+
     </div>
 
     <!-- 🔽 Child nodes -->

+ 62 - 4
src/app/plantation/plantation-tree.component.ts

@@ -13,6 +13,7 @@ import { webConfig } from '../config';
 import { MatDialog } from '@angular/material/dialog';
 import { NodeFormDialogComponent, PlantationNodeFormData } from '../components/node form/node-form-dialog.component';
 import { WorkerFormData, WorkerFormDialogComponent } from '../components/worker-dialog/worker-form-dialog.component';
+import { TaskAssignDialogComponent, TaskFormData } from '../components/assign-task/task-assign-dialog.component';
 
 interface PlantationNodeData {
     id: string;
@@ -30,6 +31,24 @@ interface TreeNode<T = PlantationNodeData> {
     children?: TreeNode<T>[];
 }
 
+interface Task {
+    id: string;
+    description: string;
+    date: string;
+    status: 'PENDING' | 'WIP' | 'DONE';
+    priority?: 'LOW' | 'MEDIUM' | 'HIGH';
+}
+
+interface Worker {
+    id: string
+    name: string;
+    DOB: string;
+    nationality: string;
+    personCode: string;
+    role: string;
+    tasks?: Task[];
+}
+
 @Component({
     selector: 'app-plantation-tree',
     standalone: true,
@@ -52,6 +71,7 @@ export class PlantationTreeComponent implements OnInit {
     loading = false;
     /** track which nodes have their detail panel open */
     expandedDetails = new Set<string>();
+    expandedWorkers = new Set<string>(); // workerCode-based or unique personCode
 
     constructor(private http: HttpClient, private dialog: MatDialog) { }
 
@@ -141,14 +161,20 @@ export class PlantationTreeComponent implements OnInit {
         dialogRef.afterClosed().subscribe((workerData: WorkerFormData | null) => {
             if (!workerData) return;
 
-            // Assuming backend endpoint: POST /api/plantation-tree/:nodeId/workers/add
-            this.http.post(`${this.apiUrl}/add/${node.id}/workers/`, workerData).subscribe({
+            // ✅ Assign a UUID before sending to backend
+            const newWorker = {
+                ...workerData,
+                id: crypto.randomUUID(),
+            };
+
+            this.http.post(`${this.apiUrl}/add/${node.id}/workers/`, newWorker).subscribe({
                 next: () => this.loadTree(),
-                error: err => console.error('Add worker failed:', err),
+                error: (err) => console.error('Add worker failed:', err),
             });
         });
     }
 
+
     getNodeIconClass(type?: string): string {
         switch (type) {
             case 'ROOT': return 'icon-root';
@@ -175,7 +201,7 @@ export class PlantationTreeComponent implements OnInit {
 
     canAddWorker(type?: string): boolean {
         // Workers are allowed for BLOCK or TREE levels
-        return type === 'BLOCK' || type === 'ZONE';
+        return type === 'BLOCK' || type === 'ZONE' || type === 'TREE';
     }
 
     isRootNode(node: TreeNode): boolean {
@@ -193,4 +219,36 @@ export class PlantationTreeComponent implements OnInit {
     isDetailsExpanded(node: TreeNode): boolean {
         return this.expandedDetails.has(node.id);
     }
+
+    toggleWorkerDetails(worker: Worker) {
+        if (this.expandedWorkers.has(worker.personCode)) {
+            this.expandedWorkers.delete(worker.personCode);
+        } else {
+            this.expandedWorkers.add(worker.personCode);
+        }
+    }
+
+    isWorkerExpanded(worker: Worker): boolean {
+        return this.expandedWorkers.has(worker.personCode);
+    }
+
+
+    assignTask(worker: Worker) {
+        const dialogRef = this.dialog.open(TaskAssignDialogComponent, {
+            width: '400px',
+            data: { workerId: worker.personCode }
+        });
+
+        dialogRef.afterClosed().subscribe((taskData: TaskFormData | null) => {
+            if (!taskData) return;
+
+            // Add unique ID before sending
+            const taskWithId = { ...taskData, id: crypto.randomUUID() };
+
+            this.http.post(`${this.apiUrl}/worker/${worker.id}/tasks`, taskWithId).subscribe({
+                next: () => this.loadTree(), // ✅ refresh tree to show new task
+                error: err => console.error('Assign task failed:', err),
+            });
+        });
+    }
 }