@@ -130,6 +130,59 @@ export class TaskService {
130130 }
131131 }
132132
133+ /**
134+ * Check if task has meaningful changes that require database update
135+ * Compares key fields and ignores timestamp-only changes
136+ */
137+ private hasTaskChanges ( existingTask : Task , newTask : Partial < Task > ) : boolean {
138+ // Fields to compare for changes
139+ const fieldsToCompare : ( keyof Task ) [ ] = [
140+ 'ownerID' , 'ownerName' , 'ownerEmail' , 'ownerPhone' ,
141+ 'owner_telegram_id' , 'owner_telegram_username' ,
142+ 'managerID' , 'managerName' ,
143+ 'manager_telegram_id' , 'manager_telegram_username' ,
144+ 'points' , 'status' , 'taskText' , 'priority' , 'notes' , 'milestone'
145+ ] ;
146+
147+ // Check if any field has changed
148+ for ( const field of fieldsToCompare ) {
149+ const existingValue = existingTask [ field ] ;
150+ const newValue = newTask [ field ] ;
151+
152+ // Skip if new value is undefined (not being updated)
153+ if ( newValue === undefined ) continue ;
154+
155+ // Compare values (handle null/empty string as equivalent)
156+ const normalizedExisting = existingValue === null ? '' : String ( existingValue || '' ) ;
157+ const normalizedNew = newValue === null ? '' : String ( newValue || '' ) ;
158+
159+ if ( normalizedExisting !== normalizedNew ) {
160+ return true ; // Found a difference
161+ }
162+ }
163+
164+ // Check date fields separately
165+ const dateFieldsToCompare : ( keyof Task ) [ ] = [ 'dueDate' , 'completed_at' , 'blocked_at' , 'last_sent' , 'last_reported' ] ;
166+
167+ for ( const field of dateFieldsToCompare ) {
168+ const existingValue = existingTask [ field ] ;
169+ const newValue = newTask [ field ] ;
170+
171+ // Skip if new value is undefined (not being updated)
172+ if ( newValue === undefined ) continue ;
173+
174+ // Compare dates
175+ const existingDate = existingValue ? new Date ( existingValue ) . getTime ( ) : null ;
176+ const newDate = newValue ? new Date ( newValue as Date ) . getTime ( ) : null ;
177+
178+ if ( existingDate !== newDate ) {
179+ return true ; // Found a difference
180+ }
181+ }
182+
183+ return false ; // No meaningful changes found
184+ }
185+
133186 async updateTaskById ( taskId : string , task : Partial < Task > ) : Promise < Task | null > {
134187 // Populate telegram IDs before updating
135188 await this . populateTelegramIds ( task ) ;
@@ -395,6 +448,8 @@ export class TaskService {
395448 }
396449
397450 let rowNumber = 1 ;
451+ let updatedCount = 0 ;
452+ let skippedCount = 0 ;
398453 const pageUserIdsAndNames : Array < [ string , string ] > = [ ] ;
399454
400455 // Get manager from first row or use default
@@ -548,8 +603,13 @@ export class TaskService {
548603 taskObj . last_reported = existingTask . last_reported ;
549604 }
550605
551- // Update existing task
552- await this . updateTaskById ( existingTask . id ! , taskObj ) ;
606+ // Only update if there are actual changes (saves database write operations)
607+ if ( this . hasTaskChanges ( existingTask , taskObj ) ) {
608+ await this . updateTaskById ( existingTask . id ! , taskObj ) ;
609+ updatedCount ++ ;
610+ } else {
611+ skippedCount ++ ;
612+ }
553613 } else {
554614 // Create new task
555615 if ( send ) {
@@ -566,6 +626,9 @@ export class TaskService {
566626 rowNumber ++ ;
567627 }
568628
629+ // Log update statistics for this project
630+ console . log ( `Project ${ projectName } : ${ updatedCount } tasks updated, ${ skippedCount } tasks unchanged (${ skippedCount > 0 ? Math . round ( skippedCount / ( updatedCount + skippedCount ) * 100 ) : 0 } % saved)` ) ;
631+
569632 // TODO: Handle activity reporting
570633 // const activity = await this.activityCrud.getOrCreateActivity(manager.number, manager.name1, projectName);
571634 // if (activity && shouldSendActivityReport(activity)) {
0 commit comments