Roberto Santini 1 viikko sitten
vanhempi
commit
c4d0e0c42e

+ 11
- 0
GestioneManutenzioni.skipper Näytä tiedosto

@@ -3907,6 +3907,17 @@
3907 3907
           <property name="type" value="boolean"/>
3908 3908
         </element>
3909 3909
       </revision>
3910
+      <revision uuid="7ae513b6-1349-4950-a0a7-3409bd0a7f87" date="2026-09-08 09:52:42.989062" exportable="true">
3911
+        <element action="add" uuid="8b114aab-e736-49f0-a36f-c9a9c14253ba" parent-uuid="b21d156e-828b-49fe-87a8-659eb8411896" sibling-uuid="330da781-7ebb-4a50-b6fd-b626f954735f" type="field">
3912
+          <property name="name" value="cc"/>
3913
+          <property name="type" value="json"/>
3914
+        </element>
3915
+        <element action="add" uuid="04bc48b2-88ce-462f-920f-01ade337f877" parent-uuid="b21d156e-828b-49fe-87a8-659eb8411896" sibling-uuid="8b114aab-e736-49f0-a36f-c9a9c14253ba" type="field">
3916
+          <property name="name" value="bcc"/>
3917
+          <property name="type" value="json"/>
3918
+        </element>
3919
+        <element action="reorder" uuid="91880b18-1a17-4b31-85e5-61ae81e6aba2" parent-uuid="b21d156e-828b-49fe-87a8-659eb8411896" sibling-uuid="04bc48b2-88ce-462f-920f-01ade337f877" previous-sibling-uuid="330da781-7ebb-4a50-b6fd-b626f954735f" type="field" helper="true"/>
3920
+      </revision>
3910 3921
     </migrations>
3911 3922
   </module>
3912 3923
   <visual-data>

+ 49
- 1
app/Console/Commands/ImportaRedmine.php Näytä tiedosto

@@ -28,12 +28,17 @@ class ImportaRedmine extends Command
28 28
                             {--tariffa= : €/ora per calcolare il costo dai tempi}
29 29
                             {--contratto= : ID del contratto servizio da assegnare alle segnalazioni}
30 30
                             {--limit= : Importa al massimo N issue (per progetto)}
31
-                            {--sync-cc : Aggiorna solo CC/CCN dalle ticket Helpdesk, senza reimportare le issue}';
31
+                            {--sync-cc : Aggiorna solo CC/CCN dalle ticket Helpdesk, senza reimportare le issue}
32
+                            {--restore-updated-at : Riallinea «Ultimo aggiornamento» (updated_at) dall\'ultima annotazione/journal, senza reimportare}';
32 33
 
33 34
     protected $description = 'Importa issue, tempi, costi e allegati da Redmine 6.x. Senza progetto importa tutti i progetti e crea le aziende mancanti.';
34 35
 
35 36
     public function handle(): int
36 37
     {
38
+        if ($this->option('restore-updated-at')) {
39
+            return $this->restoreUpdatedAt();
40
+        }
41
+
37 42
         try {
38 43
             $client = RedmineClient::fromConfig(
39 44
                 $this->option('url') ?: null,
@@ -478,6 +483,48 @@ class ImportaRedmine extends Command
478 483
         return self::SUCCESS;
479 484
     }
480 485
 
486
+    protected function restoreUpdatedAt(): int
487
+    {
488
+        $limit = $this->option('limit');
489
+        $this->info('Riallineo «Ultimo aggiornamento» delle segnalazioni Redmine dall\'ultima annotazione/journal.');
490
+        $this->comment('Non chiama Redmine e non tocca CC/CCN: ripristina updated_at come all\'import originale.');
491
+        if ($this->option('dry-run')) {
492
+            $this->warn('Modalità dry-run: nessuna scrittura sul database.');
493
+        }
494
+
495
+        try {
496
+            $stats = RedmineImportService::restoreUpdatedAt(
497
+                (bool) $this->option('dry-run'),
498
+                $limit !== null && $limit !== '' ? (int) $limit : null,
499
+                function (int $current, int $total) {
500
+                    if ($current === 1) {
501
+                        $this->output->progressStart($total);
502
+                    }
503
+                    $this->output->progressAdvance();
504
+                    if ($current === $total) {
505
+                        $this->output->progressFinish();
506
+                    }
507
+                },
508
+            );
509
+        } catch (Throwable $e) {
510
+            $this->error($e->getMessage());
511
+
512
+            return self::FAILURE;
513
+        }
514
+
515
+        $this->newLine();
516
+        $this->table(
517
+            ['Voce', 'Valore'],
518
+            [
519
+                ['Segnalazioni Redmine', $stats['tickets']],
520
+                ['updated_at riallineati', $stats['updated']],
521
+                ['Già allineati', $stats['unchanged']],
522
+            ],
523
+        );
524
+
525
+        return self::SUCCESS;
526
+    }
527
+
481 528
     /**
482 529
      * @param  array<string, mixed>  $stats
483 530
      */
@@ -576,6 +623,7 @@ class ImportaRedmine extends Command
576 623
         $this->comment('php artisan redmine:import                 importa tutti i progetti e crea le aziende');
577 624
         $this->comment('php artisan redmine:import IDENTIFIER     importa un progetto (crea l\'azienda se manca)');
578 625
         $this->comment('php artisan redmine:import IDENTIFIER 12  importa un progetto verso un\'azienda esistente');
626
+        $this->comment('php artisan redmine:import --restore-updated-at  riallinea «Ultimo aggiornamento» dalle annotazioni');
579 627
 
580 628
         return self::SUCCESS;
581 629
     }

+ 90
- 29
app/Services/Redmine/RedmineImportService.php Näytä tiedosto

@@ -272,7 +272,7 @@ class RedmineImportService
272 272
 
273 273
             $this->stampSegnalazioneFromLastAnnotazione(
274 274
                 $segnalazione,
275
-                $this->parseDate(data_get($issue, 'created_on')) ?? $segnalazione->created_at,
275
+                self::parseDate(data_get($issue, 'created_on')) ?? $segnalazione->created_at,
276 276
             );
277 277
 
278 278
             $this->segnalazioniByIssueId[(int) $issue['id']] = $segnalazione;
@@ -340,7 +340,7 @@ class RedmineImportService
340 340
             $timeStats = $this->importTimeEntries($segnalazione, $entries, false);
341 341
             $this->stampSegnalazioneFromLastAnnotazione(
342 342
                 $segnalazione,
343
-                $this->parseDate(data_get($fakeIssue, 'created_on')) ?? $segnalazione->created_at,
343
+                self::parseDate(data_get($fakeIssue, 'created_on')) ?? $segnalazione->created_at,
344 344
             );
345 345
             $this->segnalazioniByIssueId[$syntheticId] = $segnalazione;
346 346
 
@@ -363,7 +363,7 @@ class RedmineImportService
363 363
     {
364 364
         $opener = $this->resolveIssueOpener($issue);
365 365
         $assignee = $this->resolveUser(data_get($issue, 'assigned_to'), forceCreate: true, asCliente: false);
366
-        $created = $this->parseDate(data_get($issue, 'created_on')) ?? now();
366
+        $created = self::parseDate(data_get($issue, 'created_on')) ?? now();
367 367
         $issueId = (int) ($issue['id'] ?? 0);
368 368
 
369 369
         if ($issueId > 0) {
@@ -444,7 +444,7 @@ class RedmineImportService
444 444
 
445 445
         $html = $this->descriptionHtml($issue);
446 446
         $author = $this->resolveUser(data_get($issue, 'author'));
447
-        $created = $this->parseDate(data_get($issue, 'created_on')) ?? $segnalazione->created_at;
447
+        $created = self::parseDate(data_get($issue, 'created_on')) ?? $segnalazione->created_at;
448 448
 
449 449
         $createdNow = false;
450 450
         if (! $annotazione) {
@@ -496,7 +496,7 @@ class RedmineImportService
496 496
             }
497 497
 
498 498
             $user = $this->resolveUser(data_get($journal, 'user'));
499
-            $created = $this->parseDate(data_get($journal, 'created_on')) ?? now();
499
+            $created = self::parseDate(data_get($journal, 'created_on')) ?? now();
500 500
 
501 501
             if ($statusChange !== null) {
502 502
                 $this->recordStatusChangeAudit(
@@ -533,7 +533,7 @@ class RedmineImportService
533 533
             $annotazione->text = $html;
534 534
             $annotazione->riservata = $isPrivateIssue || (bool) ($journal['private_notes'] ?? false);
535 535
             $annotazione->save();
536
-            $this->stampDates($annotazione, $created, $this->parseDate(data_get($journal, 'updated_on')) ?? $created);
536
+            $this->stampDates($annotazione, $created, self::parseDate(data_get($journal, 'updated_on')) ?? $created);
537 537
 
538 538
             $map[(string) $journalId] = $annotazione->id;
539 539
             if ($createdNow) {
@@ -591,10 +591,10 @@ class RedmineImportService
591 591
             }
592 592
 
593 593
             $user = $this->resolveUser(data_get($entry, 'user'));
594
-            $spentOn = $this->parseDate(data_get($entry, 'spent_on'))
595
-                ?? $this->parseDate(data_get($entry, 'created_on'))
594
+            $spentOn = self::parseDate(data_get($entry, 'spent_on'))
595
+                ?? self::parseDate(data_get($entry, 'created_on'))
596 596
                 ?? now();
597
-            $created = $this->parseDate(data_get($entry, 'created_on')) ?? $spentOn;
597
+            $created = self::parseDate(data_get($entry, 'created_on')) ?? $spentOn;
598 598
 
599 599
             $comment = trim((string) ($entry['comments'] ?? ''));
600 600
             $activity = (string) data_get($entry, 'activity.name', '');
@@ -616,7 +616,7 @@ class RedmineImportService
616 616
                 : null;
617 617
             $annotazione->riservata = $riservata;
618 618
             $annotazione->save();
619
-            $this->stampDates($annotazione, $created, $this->parseDate(data_get($entry, 'updated_on')) ?? $created);
619
+            $this->stampDates($annotazione, $created, self::parseDate(data_get($entry, 'updated_on')) ?? $created);
620 620
 
621 621
             $map[(string) $entryId] = $annotazione->id;
622 622
             $tempi++;
@@ -654,7 +654,7 @@ class RedmineImportService
654 654
 
655 655
         $annotazioni = 0;
656 656
         $costi = 0;
657
-        $createdAt = $this->parseDate(data_get($issue, 'created_on')) ?? $segnalazione->created_at;
657
+        $createdAt = self::parseDate(data_get($issue, 'created_on')) ?? $segnalazione->created_at;
658 658
         $author = $this->resolveUser(data_get($issue, 'author'));
659 659
 
660 660
         foreach ($items as $item) {
@@ -737,7 +737,7 @@ class RedmineImportService
737 737
             $allegato->path_file = $path;
738 738
             $allegato->save();
739 739
 
740
-            $created = $this->parseDate(data_get($attachment, 'created_on'));
740
+            $created = self::parseDate(data_get($attachment, 'created_on'));
741 741
             if ($created) {
742 742
                 $allegato->timestamps = false;
743 743
                 $allegato->created_at = $created;
@@ -792,7 +792,7 @@ class RedmineImportService
792 792
             $todo->public = true;
793 793
             $done = (bool) ($item['is_done'] ?? $item['done'] ?? false);
794 794
             if ($done && ! $todo->completato_at) {
795
-                $todo->completato_at = $this->parseDate(data_get($issue, 'updated_on')) ?? now();
795
+                $todo->completato_at = self::parseDate(data_get($issue, 'updated_on')) ?? now();
796 796
             }
797 797
             if (! $done) {
798 798
                 $todo->completato_at = null;
@@ -1104,9 +1104,7 @@ class RedmineImportService
1104 1104
             if ($beforeCc === $afterCc && $afterCc === []) {
1105 1105
                 $stats['empty']++;
1106 1106
                 if (! $dryRun && $segnalazione->isDirty('meta')) {
1107
-                    Segnalazione::withoutAuditing(function () use ($segnalazione) {
1108
-                        $segnalazione->save();
1109
-                    });
1107
+                    self::saveWithoutTouchingTimestamps($segnalazione);
1110 1108
                 }
1111 1109
                 continue;
1112 1110
             }
@@ -1121,14 +1119,74 @@ class RedmineImportService
1121 1119
                 continue;
1122 1120
             }
1123 1121
 
1124
-            Segnalazione::withoutAuditing(function () use ($segnalazione) {
1125
-                $segnalazione->save();
1126
-            });
1122
+            self::saveWithoutTouchingTimestamps($segnalazione);
1127 1123
         }
1128 1124
 
1129 1125
         return $stats;
1130 1126
     }
1131 1127
 
1128
+    /**
1129
+     * Riallinea updated_at delle segnalazioni importate da Redmine
1130
+     * (ultima annotazione / journal), senza toccare CC né reimportare le issue.
1131
+     *
1132
+     * @return array{tickets:int, updated:int, unchanged:int}
1133
+     */
1134
+    public static function restoreUpdatedAt(bool $dryRun = false, ?int $limit = null, ?callable $onProgress = null): array
1135
+    {
1136
+        $query = Segnalazione::query()->where('origine', 'redmine')->orderBy('id');
1137
+        if ($limit !== null) {
1138
+            $query->limit(max(0, $limit));
1139
+        }
1140
+        $segnalazioni = $query->get();
1141
+        $stats = [
1142
+            'tickets' => $segnalazioni->count(),
1143
+            'updated' => 0,
1144
+            'unchanged' => 0,
1145
+        ];
1146
+        $total = $segnalazioni->count();
1147
+        $current = 0;
1148
+
1149
+        foreach ($segnalazioni as $segnalazione) {
1150
+            $current++;
1151
+            if ($onProgress) {
1152
+                $onProgress($current, $total, (int) $segnalazione->id);
1153
+            }
1154
+
1155
+            $restored = self::lastActivityAt($segnalazione) ?? $segnalazione->created_at;
1156
+            if (! $restored) {
1157
+                $stats['unchanged']++;
1158
+                continue;
1159
+            }
1160
+
1161
+            $currentTs = $segnalazione->updated_at?->getTimestamp();
1162
+            if ($currentTs === $restored->getTimestamp()) {
1163
+                $stats['unchanged']++;
1164
+                continue;
1165
+            }
1166
+
1167
+            $stats['updated']++;
1168
+            if ($dryRun) {
1169
+                continue;
1170
+            }
1171
+
1172
+            $segnalazione->timestamps = false;
1173
+            $segnalazione->updated_at = $restored;
1174
+            $segnalazione->saveQuietly();
1175
+            $segnalazione->timestamps = true;
1176
+        }
1177
+
1178
+        return $stats;
1179
+    }
1180
+
1181
+    protected static function saveWithoutTouchingTimestamps(Segnalazione $segnalazione): void
1182
+    {
1183
+        Segnalazione::withoutAuditing(function () use ($segnalazione) {
1184
+            $segnalazione->timestamps = false;
1185
+            $segnalazione->saveQuietly();
1186
+            $segnalazione->timestamps = true;
1187
+        });
1188
+    }
1189
+
1132 1190
     /**
1133 1191
      * @return list<string>
1134 1192
      */
@@ -1585,9 +1643,9 @@ class RedmineImportService
1585 1643
 
1586 1644
     protected function stampSegnalazioneFromLastAnnotazione(Segnalazione $segnalazione, mixed $created = null): void
1587 1645
     {
1588
-        $createdAt = $created instanceof Carbon ? $created : $this->parseDate($created);
1646
+        $createdAt = $created instanceof Carbon ? $created : self::parseDate($created);
1589 1647
         $createdAt = $createdAt ?? $segnalazione->created_at;
1590
-        $updatedAt = $this->lastActivityAt($segnalazione) ?? $createdAt;
1648
+        $updatedAt = self::lastActivityAt($segnalazione) ?? $createdAt;
1591 1649
 
1592 1650
         $segnalazione->timestamps = false;
1593 1651
         if ($createdAt) {
@@ -1597,7 +1655,7 @@ class RedmineImportService
1597 1655
         $segnalazione->saveQuietly();
1598 1656
     }
1599 1657
 
1600
-    protected function lastAnnotazioneAt(Segnalazione $segnalazione): ?Carbon
1658
+    protected static function lastAnnotazioneAt(Segnalazione $segnalazione): ?Carbon
1601 1659
     {
1602 1660
         $latest = null;
1603 1661
         $rows = Annotazione::query()
@@ -1607,7 +1665,7 @@ class RedmineImportService
1607 1665
         foreach ($rows as $annotazione) {
1608 1666
             foreach (['updated_at', 'created_at', 'tempo_data'] as $column) {
1609 1667
                 $raw = $annotazione->getAttributes()[$column] ?? null;
1610
-                $parsed = $this->parseDate($raw);
1668
+                $parsed = self::parseDate($raw);
1611 1669
                 if ($parsed && ($latest === null || $parsed->gt($latest))) {
1612 1670
                     $latest = $parsed;
1613 1671
                 }
@@ -1617,9 +1675,9 @@ class RedmineImportService
1617 1675
         return $latest;
1618 1676
     }
1619 1677
 
1620
-    protected function lastActivityAt(Segnalazione $segnalazione): ?Carbon
1678
+    protected static function lastActivityAt(Segnalazione $segnalazione): ?Carbon
1621 1679
     {
1622
-        $latest = $this->lastAnnotazioneAt($segnalazione);
1680
+        $latest = self::lastAnnotazioneAt($segnalazione);
1623 1681
 
1624 1682
         $auditAt = Audit::query()
1625 1683
             ->where('auditable_type', $segnalazione->getMorphClass())
@@ -1628,7 +1686,7 @@ class RedmineImportService
1628 1686
             ->where('tags', 'redmine')
1629 1687
             ->max('created_at');
1630 1688
 
1631
-        $parsed = $this->parseDate($auditAt);
1689
+        $parsed = self::parseDate($auditAt);
1632 1690
         if ($parsed && ($latest === null || $parsed->gt($latest))) {
1633 1691
             return $parsed;
1634 1692
         }
@@ -1715,8 +1773,8 @@ class RedmineImportService
1715 1773
 
1716 1774
     protected function stampDates(Annotazione $annotazione, mixed $created, mixed $updated): void
1717 1775
     {
1718
-        $createdAt = $created instanceof Carbon ? $created : $this->parseDate($created);
1719
-        $updatedAt = $updated instanceof Carbon ? $updated : $this->parseDate($updated);
1776
+        $createdAt = $created instanceof Carbon ? $created : self::parseDate($created);
1777
+        $updatedAt = $updated instanceof Carbon ? $updated : self::parseDate($updated);
1720 1778
         if (! $createdAt) {
1721 1779
             return;
1722 1780
         }
@@ -1728,11 +1786,14 @@ class RedmineImportService
1728 1786
         $annotazione->timestamps = true;
1729 1787
     }
1730 1788
 
1731
-    protected function parseDate(mixed $value): ?Carbon
1789
+    protected static function parseDate(mixed $value): ?Carbon
1732 1790
     {
1733 1791
         if ($value instanceof Carbon) {
1734 1792
             return $value;
1735 1793
         }
1794
+        if ($value instanceof \DateTimeInterface) {
1795
+            return Carbon::parse($value);
1796
+        }
1736 1797
         if (! is_string($value) || trim($value) === '') {
1737 1798
             return null;
1738 1799
         }

Loading…
Peruuta
Tallenna