|
|
@@ -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 {
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+
|
|
|
}
|