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 = this.actions$.pipe( ofType(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 = this.actions$.pipe( ofType(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 = this.actions$.pipe( ofType(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 = this.actions$.pipe( ofType(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 = this.actions$.pipe( ofType(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 = this.actions$.pipe( ofType(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']; } }