Jelajahi Sumber

working dialog report and repopulation issue resolved

Dr-Swopt 2 bulan lalu
induk
melakukan
a8ace8bcb0

+ 62 - 23
src/app/activity/activity.component.css

@@ -2,31 +2,9 @@
   padding: 1.5rem;
 }
 
-/* Toolbar layout */
-.toolbar {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 1.5rem;
-  flex-wrap: wrap;
-}
-
-/* Left section: title + search + refresh */
-.left-section {
-  display: flex;
-  align-items: center;
-  gap: 1rem;
-  flex-wrap: wrap;
-}
-
-.search-field {
-  width: 250px;
-  max-width: 400px;
-  margin-top: 10px;
-}
-
 table {
   width: 100%;
+  margin-top: 10px;
 }
 
 mat-progress-spinner {
@@ -55,3 +33,64 @@ mat-progress-spinner {
     transform: rotate(360deg);
   }
 }
+
+/* --- Align toolbar items better --- */
+.toolbar {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start; /* So the title and form fields align vertically via padding */
+  flex-wrap: wrap;
+  gap: 0.5rem;
+}
+
+/* Left section layout */
+.left-section {
+  display: flex;
+  align-items: flex-end; /* ✅ Align the bottom edges of form fields and buttons */
+  flex-wrap: wrap;
+  gap: 1rem;
+  margin-top: 0.5rem;
+}
+
+/* Form fields should have consistent vertical alignment */
+.left-section mat-form-field {
+  margin-bottom: 0 !important;
+  transform: translateY(4px); /* ✅ Fine-tune downward alignment for perfect line-up */
+}
+
+/* Search bar width */
+.search-field {
+  width: 250px;
+  max-width: 400px;
+  margin-top: 0 !important; /* ✅ Remove the old margin-top */
+}
+
+/* Buttons */
+.left-section button {
+  margin-top: 0 !important;
+  height: 56px; /* ✅ Match Material form-field height for perfect alignment */
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+/* Warn button color (Reset Filters) */
+button[color="warn"] {
+  color: white !important;
+  background-color: #d32f2f !important;
+  margin-bottom: 10px
+}
+
+button[color="primary"] {
+  margin-bottom: 10px
+}
+
+button[color="accent"] {
+  margin-bottom: 10px
+}
+
+/* Optional: tweak title spacing so it aligns with form fields */
+.left-section h2 {
+  margin: 0;
+  line-height: 56px; /* ✅ Match field height for clean baseline alignment */
+}

+ 47 - 19
src/app/activity/activity.component.html

@@ -1,24 +1,40 @@
 <div class="toolbar">
   <!-- Left side: Title + Search + Refresh -->
   <div class="left-section">
-    <h2>Activities</h2>
+    <h2 style="margin-bottom: 15px;">Activities</h2>
 
-    <!-- 🔍 Search -->
+    <!-- 🔍 Search bar -->
     <mat-form-field appearance="outline" class="search-field">
-      <mat-label>Search activities</mat-label>
-      <input type="text" matInput [formControl]="searchControl" [matAutocomplete]="auto"
-        placeholder="Type to search..." />
-
-      <!-- ❌ Clear button -->
-      <button *ngIf="searchControl.value" mat-icon-button matSuffix aria-label="Clear search" (click)="clearSearch()">
+      <mat-label>Search</mat-label>
+      <input type="text" matInput [formControl]="searchControl" placeholder="Search by name..." />
+      <button *ngIf="searchControl.value" mat-icon-button matSuffix aria-label="Clear" (click)="clearSearch()">
         <mat-icon>close</mat-icon>
       </button>
+    </mat-form-field>
+
+    <!-- 🔽 Filter by Type -->
+    <mat-form-field appearance="outline">
+      <mat-label>Type</mat-label>
+      <mat-select [formControl]="typeControl">
+        <mat-option value="">All</mat-option>
+        <mat-option *ngFor="let t of uniqueTypes" [value]="t">{{ t }}</mat-option>
+      </mat-select>
+    </mat-form-field>
 
-      <mat-autocomplete #auto="matAutocomplete">
-        <mat-option *ngFor="let name of uniqueActivityNames" [value]="name">
-          {{ name }}
-        </mat-option>
-      </mat-autocomplete>
+    <!-- 📅 Start Date -->
+    <mat-form-field appearance="outline">
+      <mat-label>Start Date</mat-label>
+      <input matInput [matDatepicker]="startPicker" [formControl]="startDateControl" />
+      <mat-datepicker-toggle matSuffix [for]="startPicker"></mat-datepicker-toggle>
+      <mat-datepicker #startPicker></mat-datepicker>
+    </mat-form-field>
+
+    <!-- 📅 End Date -->
+    <mat-form-field appearance="outline">
+      <mat-label>End Date</mat-label>
+      <input matInput [matDatepicker]="endPicker" [formControl]="endDateControl" />
+      <mat-datepicker-toggle matSuffix [for]="endPicker"></mat-datepicker-toggle>
+      <mat-datepicker #endPicker></mat-datepicker>
     </mat-form-field>
 
     <!-- 🔄 Refresh -->
@@ -26,13 +42,25 @@
       matTooltip="Reload activities">
       <mat-icon>refresh</mat-icon>
     </button>
-  </div>
 
-  <!-- Right side: Create Activity -->
-  <button mat-flat-button color="primary" (click)="openCreateDialog()">
-    <mat-icon>add_circle</mat-icon>
-    New Activity
-  </button>
+    <!-- 🧹 Reset Filters -->
+    <button mat-stroked-button color="warn" (click)="resetFilters()" matTooltip="Clear all filters">
+      <mat-icon>clear_all</mat-icon>
+      Reset Filters
+    </button>
+
+    <!-- Right side: Create Activity -->
+    <button mat-flat-button color="primary" (click)="openCreateDialog()">
+      <mat-icon>add_circle</mat-icon>
+      New Activity
+    </button>
+
+    <!-- 🧮 Calculate Summary -->
+    <button mat-stroked-button color="accent" (click)="openCalculateDialog()">
+      <mat-icon>calculate</mat-icon>
+      Calculate
+    </button>
+  </div>
 </div>
 
 <mat-progress-spinner *ngIf="loading" mode="indeterminate" diameter="48"></mat-progress-spinner>

+ 88 - 27
src/app/activity/activity.component.ts

@@ -10,8 +10,14 @@ import { webConfig } from '../config';
 import { CreateActivityDialogComponent } from '../components/activity-dialog/create-activity-dialog.component';
 import { MatInputModule } from '@angular/material/input';
 import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';   // 🔽 For activity type dropdown
+import { MatDatepickerModule } from '@angular/material/datepicker'; // 📅 For date picker
+import { MatNativeDateModule } from '@angular/material/core'; // 🧩 For native JS date support
 import { MatAutocompleteModule } from '@angular/material/autocomplete';
 import { ReactiveFormsModule, FormControl } from '@angular/forms';
+import { combineLatest, startWith } from 'rxjs';
+import { Activity } from './activity.interface';
+import { CalculateDialogComponent } from '../components/calculate-dialog/calculate-dialog.component';
 
 @Component({
   selector: 'app-activity',
@@ -26,6 +32,9 @@ import { ReactiveFormsModule, FormControl } from '@angular/forms';
     MatProgressSpinnerModule,
     MatFormFieldModule,
     MatInputModule,
+    MatSelectModule,        // ✅ Add this
+    MatDatepickerModule,    // ✅ Add this
+    MatNativeDateModule,    // ✅ And this
     MatAutocompleteModule,
     ReactiveFormsModule,
     CreateActivityDialogComponent,
@@ -37,9 +46,13 @@ export class ActivityComponent implements OnInit {
   private http = inject(HttpClient);
   private dialog = inject(MatDialog);
   searchControl = new FormControl('');
-  dataSource: any[] = [];
-  filteredActivities: any[] = [];
+  dataSource: Activity[] = [];
+  filteredActivities: Activity[] = [];
   uniqueActivityNames: string[] = [];
+  uniqueTypes: string[] = [];
+  typeControl = new FormControl('');
+  startDateControl = new FormControl<Date | null>(null);
+  endDateControl = new FormControl<Date | null>(null);
 
 
   displayedColumns: string[] = [
@@ -54,25 +67,12 @@ export class ActivityComponent implements OnInit {
   ngOnInit() {
     this.loadActivities();
 
-    this.searchControl.valueChanges.subscribe((value) => {
-      const search = value?.toLowerCase() || '';
-
-      // Filter for table
-      this.filteredActivities = this.dataSource.filter((activity) =>
-        activity.name.toLowerCase().includes(search)
-      );
-
-      // Extract unique names for the dropdown
-      this.uniqueActivityNames = [
-        ...new Set(
-          this.dataSource
-            .filter((activity) =>
-              activity.name.toLowerCase().includes(search)
-            )
-            .map((a) => a.name)
-        ),
-      ];
-    });
+    combineLatest([
+      this.searchControl.valueChanges.pipe(startWith(this.searchControl.value)),
+      this.typeControl.valueChanges.pipe(startWith(this.typeControl.value)),
+      this.startDateControl.valueChanges.pipe(startWith(this.startDateControl.value)),
+      this.endDateControl.valueChanges.pipe(startWith(this.endDateControl.value)),
+    ]).subscribe(() => this.applyCombinedFilters());
   }
 
   refresh() {
@@ -84,17 +84,35 @@ export class ActivityComponent implements OnInit {
     this.filteredActivities = [...this.dataSource];
   }
 
+
   loadActivities() {
     this.loading = true;
     this.http.get<any[]>(`${webConfig.exposedUrl}/api/activity`).subscribe({
       next: (data) => {
-        this.dataSource = data;
-        this.filteredActivities = [...data];
-
-        // ✅ Extract unique names for the autocomplete dropdown
-        this.uniqueActivityNames = [...new Set(data.map(a => a.name))];
-
+        // 🔧 Helper: recursively normalize any { $numberDecimal: "x" } object to a number
+        const normalizeDecimals = (obj: any): any => {
+          if (Array.isArray(obj)) {
+            return obj.map(normalizeDecimals);
+          } else if (obj && typeof obj === 'object') {
+            if ('$numberDecimal' in obj) {
+              return parseFloat(obj.$numberDecimal);
+            }
+            const normalized: any = {};
+            for (const [key, value] of Object.entries(obj)) {
+              normalized[key] = normalizeDecimals(value);
+            }
+            return normalized;
+          }
+          return obj;
+        };
+
+        // ✅ Deeply clean every activity
+        this.dataSource = data.map((activity) => normalizeDecimals(activity));
+
+        this.filteredActivities = [...this.dataSource];
+        this.uniqueTypes = [...new Set(data.map((a) => a.type))];
         this.loading = false;
+        this.applyCombinedFilters();
       },
       error: (err) => {
         console.error('Failed to fetch activities', err);
@@ -104,6 +122,35 @@ export class ActivityComponent implements OnInit {
   }
 
 
+
+  applyCombinedFilters() {
+    const keyword = (this.searchControl.value || '').toLowerCase();
+    const selectedType = this.typeControl.value;
+    const startDate = this.startDateControl.value;
+    const endDate = this.endDateControl.value;
+
+    this.filteredActivities = this.dataSource.filter((activity) => {
+      const matchesKeyword = activity.name.toLowerCase().includes(keyword);
+      const matchesType = selectedType ? activity.type === selectedType : true;
+      const activityStart = new Date(activity.dateStart);
+      const activityEnd = new Date(activity.dateEnd);
+      const matchesDateRange =
+        (!startDate || activityStart >= startDate) &&
+        (!endDate || activityEnd <= endDate);
+
+      return matchesKeyword && matchesType && matchesDateRange;
+    });
+  }
+
+  resetFilters() {
+    this.searchControl.setValue('');
+    this.typeControl.setValue('');
+    this.startDateControl.setValue(null);
+    this.endDateControl.setValue(null);
+    this.filteredActivities = [...this.dataSource];
+  }
+
+
   applyFilter(value: string) {
     const filterValue = value.toLowerCase();
     this.filteredActivities = this.dataSource.filter((activity) =>
@@ -128,6 +175,18 @@ export class ActivityComponent implements OnInit {
     });
   }
 
+  openCalculateDialog() {
+    if (!this.filteredActivities.length) {
+      alert('No activities to calculate.');
+      return;
+    }
+
+    this.dialog.open(CalculateDialogComponent, {
+      width: '700px',
+      data: { activities: this.filteredActivities },
+    });
+  }
+
   openEditDialog(activity: any) {
     const dialogRef = this.dialog.open(CreateActivityDialogComponent, {
       width: '600px',
@@ -149,4 +208,6 @@ export class ActivityComponent implements OnInit {
       });
     }
   }
+
+
 }

+ 53 - 0
src/app/activity/activity.interface.ts

@@ -0,0 +1,53 @@
+// 📁 src/app/models/activity.model.ts
+
+export interface QuantityValue {
+  quantity: number;
+  uom: string;
+}
+
+export interface WeightValue {
+  weight: number;
+  uom: string;
+}
+
+export interface Resource {
+  type: string;
+  name: string;
+  value: QuantityValue;
+  id?: string;
+}
+
+export interface Output {
+  type: string;
+  name: string;
+  value: QuantityValue;
+  id?: string;
+  weightValue: WeightValue;
+}
+
+export interface Target {
+  type: string;
+  name: string;
+  value: QuantityValue;
+  id?: string;
+}
+
+export interface Duration {
+  value: QuantityValue;
+}
+
+/**
+ * Represents an activity item as stored in the backend
+ * and displayed in the UI.
+ */
+export interface Activity {
+  _id?: string; // ← MongoDB documents often include _id
+  name: string;
+  type: string;
+  resources: Resource[];
+  duration: Duration;
+  outputs: Output[];
+  targets: Target[];
+  dateStart: string; // ISO string (e.g. "2025-10-22T00:00:00.000Z")
+  dateEnd: string;
+}

+ 2 - 3
src/app/app.component.ts

@@ -11,8 +11,7 @@ import { SocketService } from './services/socket.service';
 })
 export class AppComponent {
 
-  constructor(private socketService: SocketService) {
-
+  constructor() {
   }
- 
+
 }

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

@@ -14,6 +14,26 @@
       <input matInput formControlName="type" placeholder="e.g. actual" />
     </mat-form-field>
 
+    <!-- Duration -->
+    <div formGroupName="duration" class="form-section">
+      <div formGroupName="value" class="row">
+        <mat-form-field appearance="outline" class="flex-1">
+          <mat-label>Duration Quantity</mat-label>
+          <input matInput type="number" formControlName="quantity" placeholder="e.g. 4" />
+        </mat-form-field>
+
+        <mat-form-field appearance="outline" class="flex-1">
+          <mat-label>Duration UOM</mat-label>
+          <mat-select formControlName="uom">
+            <mat-option value="hours">Hours</mat-option>
+            <mat-option value="days">Days</mat-option>
+            <mat-option value="weeks">Weeks</mat-option>
+          </mat-select>
+        </mat-form-field>
+      </div>
+    </div>
+
+
     <div class="row">
       <mat-form-field appearance="outline" class="flex-1">
         <mat-label>Start Date</mat-label>

+ 12 - 2
src/app/components/activity-dialog/create-activity-dialog.component.ts

@@ -61,19 +61,29 @@ export class CreateActivityDialogComponent {
       dateStart: [new Date(), Validators.required],
       dateEnd: [new Date(), Validators.required],
       duration: this.fb.group({
-        quantity: [0, Validators.required],
-        uom: ['hours', Validators.required],
+        value: this.fb.group({
+          quantity: [null],
+          uom: ['hours']
+        })
       }),
       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,
+        duration: {
+          value: {
+            quantity: this.data.duration?.value?.quantity,
+            uom: this.data.duration?.value?.uom
+          }
+        },
         dateStart: this.data.dateStart,
         dateEnd: this.data.dateEnd,
       });

+ 15 - 0
src/app/components/calculate-dialog/calculate-dialog.component.css

@@ -0,0 +1,15 @@
+.dialog-content {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+table {
+  width: 100%;
+}
+
+.totals {
+  border-top: 1px solid #ddd;
+  padding-top: 1rem;
+  margin-top: 1rem;
+}

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

@@ -0,0 +1,32 @@
+<h2 mat-dialog-title>Calculation Summary</h2>
+
+<mat-dialog-content class="dialog-content">
+    <!-- 👷 Workers Section -->
+    <h3>Workers</h3>
+    <table mat-table [dataSource]="workers" class="mat-elevation-z2">
+        <ng-container matColumnDef="workerName">
+            <th mat-header-cell *matHeaderCellDef>Worker</th>
+            <td mat-cell *matCellDef="let w">{{ w.workerName }}</td>
+        </ng-container>
+
+        <ng-container matColumnDef="activityName">
+            <th mat-header-cell *matHeaderCellDef>Activity</th>
+            <td mat-cell *matCellDef="let w">{{ w.activityName }}</td>
+        </ng-container>
+
+        <tr mat-header-row *matHeaderRowDef="['workerName', 'activityName']"></tr>
+        <tr mat-row *matRowDef="let row; columns: ['workerName', 'activityName']"></tr>
+    </table>
+
+    <!-- 📦 Totals -->
+    <div class="totals">
+        <h3>Totals</h3>
+        <p><strong>Total Output Quantity:</strong> {{ totalOutput }}</p>
+        <p><strong>Total Target Quantity:</strong> {{ totalTarget }}</p>
+        <p><strong>Total Duration (hours):</strong> {{ totalDurationHours | number: '1.2-2' }}</p>
+    </div>
+</mat-dialog-content>
+
+<mat-dialog-actions align="end">
+    <button mat-flat-button color="primary" mat-dialog-close>Close</button>
+</mat-dialog-actions>

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

@@ -0,0 +1,76 @@
+import { Component, Inject } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
+import { MatTableModule } from '@angular/material/table';
+import { MatButtonModule } from '@angular/material/button';
+import { CommonModule } from '@angular/common';
+import { Activity } from '../../activity/activity.interface';
+
+interface WorkerInfo {
+  workerName: string;
+  activityName: string;
+}
+
+@Component({
+  selector: 'app-calculate-dialog',
+  standalone: true,
+  templateUrl: './calculate-dialog.component.html',
+  styleUrls: ['./calculate-dialog.component.css'],
+  imports: [CommonModule, MatDialogModule, MatTableModule, MatButtonModule],
+})
+export class CalculateDialogComponent {
+  workers: WorkerInfo[] = [];
+  totalOutput = 0;
+  totalTarget = 0;
+  totalDurationHours = 0;
+
+  constructor(@Inject(MAT_DIALOG_DATA) public data: { activities: Activity[] }) {
+    const { activities } = data;
+
+    // 👷 1. Gather all workers
+    this.workers = activities.flatMap((activity) =>
+      activity.resources
+        .filter((r) => r.type?.toLowerCase() === 'worker')
+        .map((r) => ({
+          workerName: r.name,
+          activityName: activity.name,
+        }))
+    );
+
+    // 📦 2. Sum total outputs
+    this.totalOutput = activities.reduce((sum, activity) => {
+      const outputSum = activity.outputs.reduce((a, o) => {
+        const qty = Number(o.value?.quantity ?? 0);
+        return a + (isNaN(qty) ? 0 : qty);
+      }, 0);
+      return sum + outputSum;
+    }, 0);
+
+    // 🎯 3. Sum total targets
+    this.totalTarget = activities.reduce((sum, activity) => {
+      const targetSum = activity.targets.reduce((a, t) => {
+        const qty = Number(t.value?.quantity ?? 0);
+        return a + (isNaN(qty) ? 0 : qty);
+      }, 0);
+      return sum + targetSum;
+    }, 0);
+
+    // ⏱️ 4. Sum total duration in hours
+    this.totalDurationHours = activities.reduce((sum, activity) => {
+      const duration = activity.duration?.value;
+      if (!duration) return sum;
+
+      const qty = Number(duration.quantity ?? 0);
+      if (isNaN(qty)) return sum;
+
+      let hours = qty;
+      const uom = duration.uom?.toLowerCase();
+
+      // convert to hours
+      if (uom === 'minutes' || uom === 'mins' || uom === 'min') hours = qty / 60;
+      else if (uom === 'seconds' || uom === 'sec' || uom === 'secs') hours = qty / 3600;
+      else if (uom === 'days' || uom === 'day') hours = qty * 24;
+
+      return sum + hours;
+    }, 0);
+  }
+}

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

@@ -23,20 +23,16 @@ import { ActivityComponent } from '../activity/activity.component';
     AttendanceComponent,
     PlantationTreeComponent,
     ActivityComponent
-],
+  ],
   templateUrl: './dashboard.component.html',
   styleUrls: ['./dashboard.component.css']
 })
-export class DashboardComponent implements OnInit {
+export class DashboardComponent {
 
   constructor(private auth: AuthService) {
 
   }
-  ngOnInit(): void {
-    throw new Error('Method not implemented.');
-  }
 
- 
   logout(): void {
     this.auth.logout();
   }

+ 12 - 3
src/app/services/auth.service.ts

@@ -13,7 +13,14 @@ export class AuthService {
   private readonly tokenKey = 'auth_token';
   private userName!: string
 
-  constructor(private http: HttpClient, private router: Router) { }
+  constructor(private http: HttpClient, private router: Router) {
+    const token = this.getToken();
+    const name = localStorage.getItem('auth_name');
+
+    if (token && name) {
+      this.userName = name;
+    }
+  }
 
   // -- API Calls --
 
@@ -193,9 +200,9 @@ export class AuthService {
   }
 
   setUserName(username: string) {
-    this.userName = username
+    this.userName = username;
+    localStorage.setItem('auth_name', username);
   }
-
   getUsername(): string {
     return this.userName
   }
@@ -216,6 +223,8 @@ export class AuthService {
 
   logout(): void {
     localStorage.removeItem(this.tokenKey);
+    localStorage.removeItem('auth_name');
+    this.userName = '';
     this.router.navigate(['/webauthn-login']);
   }