agmission/Development/client/src/app/job/effects/job.effects.ts

246 lines
8.8 KiB
TypeScript

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map, switchMap, catchError } from 'rxjs/operators';
import { Action } from '@ngrx/store';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { toJob } from '../models/job.model';
import * as jobActions from '../actions/job.actions';
import { JobService } from '@app/domain/services/job.service';
import { JobCacheService } from '@app/domain/services/job-cache.service';
import { AppMessageService } from '@app/shared/app-message.service';
import { globals } from '@app/shared/global';
import { GAService } from '@app/shared/ga.service';
@Injectable()
export class JobEffects {
constructor(
private readonly actions$: Actions,
private readonly jobSvc: JobService,
private readonly jobCache: JobCacheService,
private readonly msgSvc: AppMessageService,
private readonly gaSvc: GAService
) {
}
@Effect()
loadJobs$: Observable<Action> = this.actions$.pipe(
ofType<jobActions.Fetch>(jobActions.FETCH),
switchMap(({ payload }) =>
this.jobSvc.loadJobs(payload).pipe(
map(data => {
const jobs = [];
if (data && Array.isArray(data)) {
data.forEach(item => {
jobs.push(toJob(item));
});
}
return new jobActions.FetchSuccess(jobs);
}),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.load).replace('#thing#', globals.jobs));
return of(new jobActions.FetchError());
})
)
)
);
@Effect()
createJob$: Observable<Action> = this.actions$.pipe(
ofType<jobActions.Create>(jobActions.CREATE),
switchMap(({ payload }) =>
this.jobSvc.createJob(payload).pipe(
map((job) => {
// Track job creation with GA4
this.gaSvc.trackJobCreated({
user_id: 'system',
platform: 'web',
job_type: this.normalizeJobType(payload.appType),
field_size_acres: payload.ttSprArea || 0,
crop_type: payload.crop?.name || 'unknown',
client_id: payload.client?._id?.toString() || 'unknown',
priority: 'medium' // Default priority
});
this.jobCache.invalidate();
return new jobActions.CreateSuccess(job);
}),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.save).replace('#thing#', globals.job));
return of(new jobActions.CreateFailed())
})
)
)
);
@Effect()
updateJob$: Observable<Action> = this.actions$.pipe(
ofType<jobActions.Update>(jobActions.UPDATE),
switchMap(({ payload }) => {
const oldStatus = payload.job?.status;
return this.jobSvc.saveJob(payload).pipe(
map((data) => {
const updatedJob = toJob(data);
const newStatus = updatedJob.status;
// Check if status changed during update
if (oldStatus !== undefined && oldStatus !== newStatus) {
this.gaSvc.trackJobStatusChanged({
user_id: 'system',
platform: 'web',
job_id: payload.job?._id?.toString() || 'unknown',
old_status: this.mapStatusToString(oldStatus),
new_status: this.mapStatusToString(newStatus),
status_change_reason: 'api_update'
});
}
// Track general job update
this.gaSvc.trackJobUpdated({
user_id: 'system',
platform: 'web',
job_id: payload.job?._id?.toString() || 'unknown',
fields_modified: this.detectModifiedFields(payload, updatedJob),
change_magnitude: oldStatus !== newStatus ? 'major' : 'minor',
save_method: 'manual'
});
return new jobActions.UpdateSuccess(updatedJob)
}),
catchError(err => {
if (err?.error?.error['.tag'] == 'cannot_edit_job_have_invoice_opened') {
this.msgSvc.addFailedMsg($localize`:@@cannotEditInvoicedJob:Cannot edit job linked with an opened, paid or uncollectible invoice`);
} else {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.save).replace('#thing#', globals.job));
}
return of(new jobActions.UpdateFailed());
})
)
})
);
@Effect()
deleteJob$: Observable<Action> = this.actions$.pipe(
ofType<jobActions.Delete>(jobActions.DELETE),
switchMap(({ payload }) =>
this.jobSvc.deleteJob(payload).pipe(
map(() => {
// Track job deletion with GA4
this.gaSvc.trackJobDeleted({
user_id: 'system', // Effects don't have direct user context
platform: 'web',
job_id: payload._id?.toString() || 'unknown',
job_type: payload.appType || 'unknown',
job_status: payload.status?.toString() || 'unknown',
deletion_reason: 'user_action',
deletion_method: 'api_call',
time_since_creation: payload.createdAt ?
Math.floor((new Date().getTime() - new Date(payload.createdAt).getTime()) / (1000 * 60 * 60)) : 0
});
this.jobCache.invalidate();
return new jobActions.DeleteSuccess(payload)
}),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.delete).replace('#thing#', globals.job));
return of(new jobActions.UpdateFailed());
})
)
)
);
@Effect()
assignJob$: Observable<Action> = this.actions$.pipe(
ofType<jobActions.Assign>(jobActions.ASSIGN),
switchMap(({ payload }) =>
this.jobSvc.assign(payload).pipe(
map(() => {
this.msgSvc.addSuccessMsg($localize`:@@jobAssigned:Job assigned`);
// Track job assignment with GA4
this.gaSvc.trackJobAssigned({
user_id: 'system',
platform: 'web',
job_id: payload.jobId?.toString() || 'unknown',
assignee_id: payload.asUsers?.[0]?._id?.toString() || 'unknown',
assignee_role: 'applicator', // Updated to use valid role
assignment_method: 'manual'
});
return new jobActions.AssignSuccess({ _id: payload.jobId })
}),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.assign).replace('#thing#', globals.job));
return of(new jobActions.AssignFailed());
})
)
)
);
@Effect()
saveMap$: Observable<Action> = this.actions$.pipe(
ofType<jobActions.SaveMap>(jobActions.SAVEMAP),
switchMap(({ payload }) =>
this.jobSvc.saveJobMapOps(payload).pipe(
map(() => {
this.msgSvc.addSuccessMsg($localize`:@@mapSaved:Map saved`);
return new jobActions.SaveMapSuccess()
}),
catchError(err => {
this.msgSvc.addFailedMsg(globals.doThingsFailed.replace('#do#', globals.save).replace('#thing#', globals.map));
return of(new jobActions.SaveMapFailed());
})
)
)
);
// Helper method to normalize job type to GA4 enum values
private normalizeJobType(appType: string): 'spraying' | 'seeding' | 'fertilizing' | 'harvesting' {
const type = appType?.toLowerCase();
if (type?.includes('spray')) return 'spraying';
if (type?.includes('seed')) return 'seeding';
if (type?.includes('fertiliz')) return 'fertilizing';
if (type?.includes('harvest')) return 'harvesting';
return 'spraying'; // Default fallback
}
/**
* Map numeric status to string for GA4 tracking
*/
private mapStatusToString(status: number): 'new' | 'ready' | 'downloaded' | 'sprayed' | 'archived' {
switch (status) {
case 0: return 'new';
case 1: return 'ready';
case 2: return 'downloaded';
case 3: return 'sprayed';
case 9: return 'archived';
default: return 'new';
}
}
/**
* Detect which fields were modified in the job update
*/
private detectModifiedFields(payload: any, updatedJob: any): string[] {
const modifiedFields: string[] = [];
const originalJob = payload.job;
if (!originalJob) return ['unknown'];
// Check common fields that might change
if (originalJob.status !== updatedJob.status) modifiedFields.push('status');
if (originalJob.name !== updatedJob.name) modifiedFields.push('name');
if (originalJob.priority !== updatedJob.priority) modifiedFields.push('priority');
if (originalJob.startDate !== updatedJob.startDate) modifiedFields.push('startDate');
if (originalJob.endDate !== updatedJob.endDate) modifiedFields.push('endDate');
if (originalJob.operator?._id !== updatedJob.operator?._id) modifiedFields.push('operator');
if (originalJob.vehicle?._id !== updatedJob.vehicle?._id) modifiedFields.push('vehicle');
return modifiedFields.length > 0 ? modifiedFields : ['unknown'];
}
}