Browse Source

fix some issues

Dr-Swopt 2 tháng trước cách đây
mục cha
commit
bb90bc46bd

+ 15 - 2
src/app/activity/activity.component.html

@@ -77,16 +77,29 @@
     <td mat-cell *matCellDef="let row">{{ row.type }}</td>
   </ng-container>
 
+  <ng-container matColumnDef="duration">
+    <th mat-header-cell *matHeaderCellDef>Duration</th>
+    <td mat-cell *matCellDef="let row">
+      {{ row.duration?.value?.quantity }} {{ row.duration?.value?.uom }}
+    </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>
+    <td mat-cell *matCellDef="let row">{{ formatDate(row.dateStart) }}</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>
+    <td mat-cell *matCellDef="let row">{{ formatDate(row.dateEnd) }}</td>
   </ng-container>
 
+  <ng-container matColumnDef="totalOutput">
+    <th mat-header-cell *matHeaderCellDef>Total Output</th>
+    <td mat-cell *matCellDef="let row">{{ getTotalOutput(row) }}</td>
+  </ng-container>
+
+
   <ng-container matColumnDef="actions">
     <th mat-header-cell *matHeaderCellDef></th>
     <td mat-cell *matCellDef="let row">

+ 37 - 1
src/app/activity/activity.component.ts

@@ -60,8 +60,11 @@ export class ActivityComponent implements OnInit {
     'type',
     'dateStart',
     'dateEnd',
+    'duration',      // ✅ Add this
+    'totalOutput',
     'actions',
   ];
+
   loading = false;
 
   ngOnInit() {
@@ -130,8 +133,20 @@ export class ActivityComponent implements OnInit {
     const endDate = this.endDateControl.value;
 
     this.filteredActivities = this.dataSource.filter((activity) => {
-      const matchesKeyword = activity.name.toLowerCase().includes(keyword);
+      // 1️⃣ Check activity name
+      const matchesName = activity.name.toLowerCase().includes(keyword);
+
+      // 2️⃣ Check resource names
+      const matchesResource = activity.resources?.some(res =>
+        res.name.toLowerCase().includes(keyword)
+      ) ?? false;
+
+      const matchesKeyword = matchesName || matchesResource;
+
+      // 3️⃣ Type filter
       const matchesType = selectedType ? activity.type === selectedType : true;
+
+      // 4️⃣ Date range filter
       const activityStart = new Date(activity.dateStart);
       const activityEnd = new Date(activity.dateEnd);
       const matchesDateRange =
@@ -142,6 +157,7 @@ export class ActivityComponent implements OnInit {
     });
   }
 
+
   resetFilters() {
     this.searchControl.setValue('');
     this.typeControl.setValue('');
@@ -209,5 +225,25 @@ export class ActivityComponent implements OnInit {
     }
   }
 
+  // Format date as "12 Jan 2025"
+  formatDate(dateStr: string | Date): string {
+    const date = new Date(dateStr);
+    const options: Intl.DateTimeFormatOptions = { day: '2-digit', month: 'short', year: 'numeric' };
+    return date.toLocaleDateString('en-US', options);
+  }
+
+  // Calculate total output quantity for an activity
+  getTotalOutput(activity: Activity): string {
+    if (!activity.outputs || !activity.outputs.length) return '0';
+
+    // Sum by value
+    const totalQuantity = activity.outputs.reduce((sum, o) => sum + (o.value?.quantity || 0), 0);
+    const uom = activity.outputs[0]?.value?.uom || '';
 
+    // Sum by weight
+    const totalWeight = activity.outputs.reduce((sum, o) => sum + (o.weightValue?.weight || 0), 0);
+    const weightUom = activity.outputs[0]?.weightValue?.uom || 'kg';
+
+    return `${totalQuantity} ${uom} (${totalWeight} ${weightUom})`;
+  }
 }

+ 11 - 1
src/app/components/activity-dialog/create-activity-dialog.component.css

@@ -61,4 +61,14 @@
 button[color='warn'] {
   background-color: #d32f2f; /* Stronger red */
   color: #fff;
-}
+}
+
+.snackbar-success {
+  background-color: #4caf50;
+  color: white;
+}
+
+.snackbar-error {
+  background-color: #f44336;
+  color: white;
+}

+ 17 - 8
src/app/components/activity-dialog/create-activity-dialog.component.ts

@@ -24,6 +24,7 @@ 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';
+import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
 
 @Component({
   selector: 'app-create-activity-dialog',
@@ -41,6 +42,7 @@ import { Component, Inject } from '@angular/core';
     MatIconModule,
     MatSelectModule,
     MatExpansionModule,
+    MatSnackBarModule,  // ✅ Add this
   ],
   templateUrl: './create-activity-dialog.component.html',
   styleUrls: ['./create-activity-dialog.component.css'],
@@ -53,6 +55,7 @@ export class CreateActivityDialogComponent {
     private fb: FormBuilder,
     private http: HttpClient,
     private dialogRef: MatDialogRef<CreateActivityDialogComponent>,
+    private snackBar: MatSnackBar, // ✅ Inject MatSnackBar
     @Inject(MAT_DIALOG_DATA) public data: any
   ) {
     this.form = this.fb.group({
@@ -208,12 +211,12 @@ export class CreateActivityDialogComponent {
 
     this.http.delete(`${webConfig.exposedUrl}/api/activity/${this.data._id}`).subscribe({
       next: () => {
-        alert('Activity deleted successfully.');
+        this.snackBar.open('Activity deleted successfully!', 'Close', { duration: 3000 });
         this.dialogRef.close('refresh');
       },
       error: (err) => {
+        this.snackBar.open('Failed to delete activity.', 'Close', { duration: 5000 });
         console.error('Failed to delete activity:', err);
-        alert('Failed to delete activity. Please try again.');
       },
     });
   }
@@ -231,8 +234,8 @@ export class CreateActivityDialogComponent {
 
       duration: {
         value: {
-          quantity: formValue.durationQuantity || 0,
-          uom: formValue.durationUom || 'hours',
+          quantity: formValue.duration?.value?.quantity || 0,
+          uom: formValue.duration?.value?.uom || 'hours',
         },
       },
 
@@ -278,14 +281,20 @@ export class CreateActivityDialogComponent {
 
     this.http[httpMethod](url, activityData).subscribe({
       next: () => {
-        console.log(isEditMode ? 'Activity updated.' : 'Activity created.');
+        this.snackBar.open(
+          isEditMode ? 'Activity updated successfully!' : 'Activity created successfully!',
+          'Close',
+          { duration: 3000 }
+        );
         this.dialogRef.close('refresh');
       },
       error: (err) => {
-        console.error(
-          isEditMode ? 'Failed to update activity:' : 'Failed to create activity:',
-          err
+        this.snackBar.open(
+          isEditMode ? 'Failed to update activity.' : 'Failed to create activity.',
+          'Close',
+          { duration: 5000 }
         );
+        console.error(err);
       },
     });
   }

+ 31 - 15
src/app/components/calculate-dialog/calculate-dialog.component.html

@@ -1,28 +1,20 @@
 <h2 mat-dialog-title>Activity Quantitative Report</h2>
 
 <mat-dialog-content>
-  <!-- WORKERS -->
+  <!-- Resources grouped by type -->
   <section>
-    <h3>👷 Workers ({{ workerSummary.length }})</h3>
-
-    <!-- Single collapsible panel for all workers -->
+    <h3>👷 Resources ({{ resourceSummary.length }})</h3>
     <mat-accordion>
-      <mat-expansion-panel>
+      <mat-expansion-panel *ngFor="let type of resourceSummary">
         <mat-expansion-panel-header>
           <mat-panel-title>
-            Show Worker Details
+            {{ type.type | titlecase }} ({{ type.resources.length }})
           </mat-panel-title>
         </mat-expansion-panel-header>
 
-        <!-- Worker rows -->
-        <div *ngFor="let worker of workerSummary" class="worker-row">
-          <div *ngFor="let act of worker.activities" class="activity-row">
-            <span class="worker-name">{{ worker.name }}</span>
-            <span class="activity-name">{{ act.activityName }}</span>
-            <span class="activity-output" *ngFor="let out of act.outputs">
-              {{ out.value }} {{ out.uom }}
-            </span>
-          </div>
+        <!-- List each resource of this type -->
+        <div *ngFor="let res of type.resources" class="resource-row">
+          <span>{{ res.name }}: {{ res.totalQuantity }} {{ res.uom }}</span>
         </div>
       </mat-expansion-panel>
     </mat-accordion>
@@ -59,6 +51,30 @@
     <h3>⏱️ Total Duration</h3>
     <p>{{ totalDuration }} {{ durationUom }}</p>
   </section>
+
+  <!-- MONTHLY AVERAGES -->
+  <section *ngIf="averageOutputsPerMonth && (averageOutputsPerMonth | keyvalue).length">
+    <h3>📦 Average Outputs per Month</h3>
+    <ul>
+      <li *ngFor="let uom of (averageOutputsPerMonth | keyvalue)">
+        {{ uom.value | number:'1.0-2' }} {{ totalOutputUoms[uom.key] }}
+      </li>
+    </ul>
+  </section>
+
+  <section *ngIf="averageTargetsPerMonth && (averageTargetsPerMonth | keyvalue).length">
+    <h3>🎯 Average Targets per Month</h3>
+    <ul>
+      <li *ngFor="let uom of (averageTargetsPerMonth | keyvalue)">
+        {{ uom.value | number:'1.0-2' }} {{ totalTargetUoms[uom.key] }}
+      </li>
+    </ul>
+  </section>
+
+  <section *ngIf="averageDurationPerMonth && averageDurationPerMonth > 0">
+    <h3>⏱️ Average Duration per Month</h3>
+    <p>{{ averageDurationPerMonth | number:'1.0-2' }} {{ durationUom }}</p>
+  </section>
 </mat-dialog-content>
 
 <mat-dialog-actions align="end">

+ 75 - 43
src/app/components/calculate-dialog/calculate-dialog.component.ts

@@ -1,18 +1,18 @@
-import { Component, Inject, Input, OnInit } from '@angular/core';
+import { Component, Inject, OnInit } from '@angular/core';
 import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
 import { Activity } from '../../activity/activity.interface';
 import { MatDialogModule } from '@angular/material/dialog';
 import { MatButtonModule } from '@angular/material/button';
 import { MatExpansionModule } from '@angular/material/expansion';
 import { CommonModule } from '@angular/common';
-interface WorkerActivity {
-    activityName: string;
-    outputs: { name: string; value: number; uom: string }[];
-}
 
-interface WorkerSummary {
-    name: string;
-    activities: WorkerActivity[];
+interface ResourceSummary {
+    type: string; // worker, tractor, etc.
+    resources: {
+        name: string;
+        totalQuantity: number;
+        uom: string;
+    }[];
 }
 
 @Component({
@@ -30,7 +30,7 @@ interface WorkerSummary {
 export class CalculateDialogComponent implements OnInit {
     filteredActivities: Activity[] = [];
 
-    workerSummary: WorkerSummary[] = [];
+    resourceSummary: ResourceSummary[] = [];
     totalOutputs: Record<string, number> = {};
     totalOutputUoms: Record<string, string> = {};
     totalTargets: Record<string, number> = {};
@@ -38,11 +38,17 @@ export class CalculateDialogComponent implements OnInit {
     totalDuration: number = 0;
     durationUom: string = '';
 
+    averageOutputsPerMonth: Record<string, number> = {};
+    averageTargetsPerMonth: Record<string, number> = {};
+    averageDurationPerMonth: number = 0;
+
+    overallStart: Date | null = null;
+    overallEnd: Date | null = null;
+
     constructor(
         private dialogRef: MatDialogRef<CalculateDialogComponent>,
         @Inject(MAT_DIALOG_DATA) public data: { activities: Activity[] }
     ) {
-        // assign injected data to local variable
         this.filteredActivities = data.activities;
     }
 
@@ -51,71 +57,97 @@ export class CalculateDialogComponent implements OnInit {
     }
 
     calculateTotals(): void {
-        const workerMap: Record<string, { activityName: string; outputs: { name: string; value: number; uom: string }[] }[]> = {};
+        const resourceTypeMap: Record<string, Record<string, { totalQuantity: number; uom: string }>> = {};
         const outputTotals: Record<string, number> = {};
         const outputUoms: Record<string, string> = {};
         const targetTotals: Record<string, number> = {};
         const targetUoms: Record<string, string> = {};
-        let totalDuration = 0;
+        const sumOutputs: Record<string, number> = {};
+        const sumTargets: Record<string, number> = {};
+
+        const allStartDates: Date[] = [];
+        const allEndDates: Date[] = [];
+        let totalDurationQuantity = 0;
         let durationUom = '';
 
         for (const activity of this.filteredActivities) {
-            // Workers
-            for (const resource of activity.resources) {
-                if (resource.type.toLowerCase() === 'worker') {
-                    if (!workerMap[resource.name]) {
-                        workerMap[resource.name] = [];
-                    }
-
-                    const activityOutputs = activity.outputs.map(o => ({
-                        name: o.name,
-                        value: o.value.quantity,
-                        uom: o.value.uom,
-                    }));
-
-                    workerMap[resource.name].push({
-                        activityName: activity.name,
-                        outputs: activityOutputs,
-                    });
+            const start = new Date(activity.dateStart);
+            const end = new Date(activity.dateEnd);
+            allStartDates.push(start);
+            allEndDates.push(end);
+
+            // Group resources by type and sum quantity
+            for (const res of activity.resources) {
+                if (!resourceTypeMap[res.type]) resourceTypeMap[res.type] = {};
+                if (!resourceTypeMap[res.type][res.name]) {
+                    resourceTypeMap[res.type][res.name] = { totalQuantity: 0, uom: res.value.uom };
                 }
+
+                resourceTypeMap[res.type][res.name].totalQuantity += res.value.quantity;
             }
 
-            // Outputs (totals)
+            // Outputs totals & sum for averages
             for (const output of activity.outputs) {
                 const { uom, quantity } = output.value;
                 outputTotals[uom] = (outputTotals[uom] || 0) + quantity;
                 outputUoms[uom] = uom;
+                sumOutputs[uom] = (sumOutputs[uom] || 0) + quantity;
             }
 
-            // Targets (totals)
+            // Targets totals & sum for averages
             for (const target of activity.targets) {
                 const { uom, quantity } = target.value;
                 targetTotals[uom] = (targetTotals[uom] || 0) + quantity;
                 targetUoms[uom] = uom;
+                sumTargets[uom] = (sumTargets[uom] || 0) + quantity;
             }
 
-            // Duration (sum only if hours)
-            const dur = activity.duration.value;
-            if (dur.uom.toLowerCase() === 'hour' || dur.uom.toLowerCase() === 'hours') {
-                totalDuration += dur.quantity;
-            }
-            durationUom = dur.uom;
+            // Duration sum
+            totalDurationQuantity += activity.duration.value.quantity;
+            durationUom = activity.duration.value.uom;
         }
 
-        // Map worker data into summary
-        this.workerSummary = Object.keys(workerMap).map(name => ({
-            name,
-            activities: workerMap[name],
+        // Overall start/end
+        const overallStart = new Date(Math.min(...allStartDates.map(d => d.getTime())));
+        const overallEnd = new Date(Math.max(...allEndDates.map(d => d.getTime())));
+        const totalDays = (overallEnd.getTime() - overallStart.getTime()) / (1000 * 60 * 60 * 24);
+        const totalMonths = totalDays / 30;
+
+        // Averages per month
+        const averageOutputs: Record<string, number> = {};
+        const averageTargets: Record<string, number> = {};
+        let averageDuration = 0;
+
+        if (totalMonths >= 1) {
+            for (const key in sumOutputs) averageOutputs[key] = sumOutputs[key] / totalMonths;
+            for (const key in sumTargets) averageTargets[key] = sumTargets[key] / totalMonths;
+            averageDuration = totalDurationQuantity / totalMonths;
+        }
+
+        // Transform map into ResourceSummary array
+        this.resourceSummary = Object.keys(resourceTypeMap).map(type => ({
+            type,
+            resources: Object.keys(resourceTypeMap[type]).map(name => ({
+                name,
+                totalQuantity: resourceTypeMap[type][name].totalQuantity,
+                uom: resourceTypeMap[type][name].uom
+            }))
         }));
 
         this.totalOutputs = outputTotals;
         this.totalOutputUoms = outputUoms;
         this.totalTargets = targetTotals;
         this.totalTargetUoms = targetUoms;
-        this.totalDuration = totalDuration;
+        this.totalDuration = totalDurationQuantity;
         this.durationUom = durationUom;
-    }
 
+        this.averageOutputsPerMonth = averageOutputs;
+        this.averageTargetsPerMonth = averageTargets;
+        this.averageDurationPerMonth = averageDuration;
+
+        this.overallStart = overallStart;
+        this.overallEnd = overallEnd;
+    }
 
     closeDialog(): void {
         this.dialogRef.close();