Roberto Santini преди 1 седмица
родител
ревизия
c4d0e0c42e
променени са 3 файла, в които са добавени 150 реда и са изтрити 30 реда
  1. 11
    0
      GestioneManutenzioni.skipper
  2. 49
    1
      app/Console/Commands/ImportaRedmine.php
  3. 90
    29
      app/Services/Redmine/RedmineImportService.php

+ 11
- 0
GestioneManutenzioni.skipper Целия файл

3907
           <property name="type" value="boolean"/>
3907
           <property name="type" value="boolean"/>
3908
         </element>
3908
         </element>
3909
       </revision>
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
     </migrations>
3921
     </migrations>
3911
   </module>
3922
   </module>
3912
   <visual-data>
3923
   <visual-data>

+ 49
- 1
app/Console/Commands/ImportaRedmine.php Целия файл

28
                             {--tariffa= : €/ora per calcolare il costo dai tempi}
28
                             {--tariffa= : €/ora per calcolare il costo dai tempi}
29
                             {--contratto= : ID del contratto servizio da assegnare alle segnalazioni}
29
                             {--contratto= : ID del contratto servizio da assegnare alle segnalazioni}
30
                             {--limit= : Importa al massimo N issue (per progetto)}
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
     protected $description = 'Importa issue, tempi, costi e allegati da Redmine 6.x. Senza progetto importa tutti i progetti e crea le aziende mancanti.';
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
     public function handle(): int
36
     public function handle(): int
36
     {
37
     {
38
+        if ($this->option('restore-updated-at')) {
39
+            return $this->restoreUpdatedAt();
40
+        }
41
+
37
         try {
42
         try {
38
             $client = RedmineClient::fromConfig(
43
             $client = RedmineClient::fromConfig(
39
                 $this->option('url') ?: null,
44
                 $this->option('url') ?: null,
478
         return self::SUCCESS;
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
      * @param  array<string, mixed>  $stats
529
      * @param  array<string, mixed>  $stats
483
      */
530
      */
576
         $this->comment('php artisan redmine:import                 importa tutti i progetti e crea le aziende');
623
         $this->comment('php artisan redmine:import                 importa tutti i progetti e crea le aziende');
577
         $this->comment('php artisan redmine:import IDENTIFIER     importa un progetto (crea l\'azienda se manca)');
624
         $this->comment('php artisan redmine:import IDENTIFIER     importa un progetto (crea l\'azienda se manca)');
578
         $this->comment('php artisan redmine:import IDENTIFIER 12  importa un progetto verso un\'azienda esistente');
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
         return self::SUCCESS;
628
         return self::SUCCESS;
581
     }
629
     }

+ 90
- 29
app/Services/Redmine/RedmineImportService.php Целия файл

272
 
272
 
273
             $this->stampSegnalazioneFromLastAnnotazione(
273
             $this->stampSegnalazioneFromLastAnnotazione(
274
                 $segnalazione,
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
             $this->segnalazioniByIssueId[(int) $issue['id']] = $segnalazione;
278
             $this->segnalazioniByIssueId[(int) $issue['id']] = $segnalazione;
340
             $timeStats = $this->importTimeEntries($segnalazione, $entries, false);
340
             $timeStats = $this->importTimeEntries($segnalazione, $entries, false);
341
             $this->stampSegnalazioneFromLastAnnotazione(
341
             $this->stampSegnalazioneFromLastAnnotazione(
342
                 $segnalazione,
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
             $this->segnalazioniByIssueId[$syntheticId] = $segnalazione;
345
             $this->segnalazioniByIssueId[$syntheticId] = $segnalazione;
346
 
346
 
363
     {
363
     {
364
         $opener = $this->resolveIssueOpener($issue);
364
         $opener = $this->resolveIssueOpener($issue);
365
         $assignee = $this->resolveUser(data_get($issue, 'assigned_to'), forceCreate: true, asCliente: false);
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
         $issueId = (int) ($issue['id'] ?? 0);
367
         $issueId = (int) ($issue['id'] ?? 0);
368
 
368
 
369
         if ($issueId > 0) {
369
         if ($issueId > 0) {
444
 
444
 
445
         $html = $this->descriptionHtml($issue);
445
         $html = $this->descriptionHtml($issue);
446
         $author = $this->resolveUser(data_get($issue, 'author'));
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
         $createdNow = false;
449
         $createdNow = false;
450
         if (! $annotazione) {
450
         if (! $annotazione) {
496
             }
496
             }
497
 
497
 
498
             $user = $this->resolveUser(data_get($journal, 'user'));
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
             if ($statusChange !== null) {
501
             if ($statusChange !== null) {
502
                 $this->recordStatusChangeAudit(
502
                 $this->recordStatusChangeAudit(
533
             $annotazione->text = $html;
533
             $annotazione->text = $html;
534
             $annotazione->riservata = $isPrivateIssue || (bool) ($journal['private_notes'] ?? false);
534
             $annotazione->riservata = $isPrivateIssue || (bool) ($journal['private_notes'] ?? false);
535
             $annotazione->save();
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
             $map[(string) $journalId] = $annotazione->id;
538
             $map[(string) $journalId] = $annotazione->id;
539
             if ($createdNow) {
539
             if ($createdNow) {
591
             }
591
             }
592
 
592
 
593
             $user = $this->resolveUser(data_get($entry, 'user'));
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
                 ?? now();
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
             $comment = trim((string) ($entry['comments'] ?? ''));
599
             $comment = trim((string) ($entry['comments'] ?? ''));
600
             $activity = (string) data_get($entry, 'activity.name', '');
600
             $activity = (string) data_get($entry, 'activity.name', '');
616
                 : null;
616
                 : null;
617
             $annotazione->riservata = $riservata;
617
             $annotazione->riservata = $riservata;
618
             $annotazione->save();
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
             $map[(string) $entryId] = $annotazione->id;
621
             $map[(string) $entryId] = $annotazione->id;
622
             $tempi++;
622
             $tempi++;
654
 
654
 
655
         $annotazioni = 0;
655
         $annotazioni = 0;
656
         $costi = 0;
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
         $author = $this->resolveUser(data_get($issue, 'author'));
658
         $author = $this->resolveUser(data_get($issue, 'author'));
659
 
659
 
660
         foreach ($items as $item) {
660
         foreach ($items as $item) {
737
             $allegato->path_file = $path;
737
             $allegato->path_file = $path;
738
             $allegato->save();
738
             $allegato->save();
739
 
739
 
740
-            $created = $this->parseDate(data_get($attachment, 'created_on'));
740
+            $created = self::parseDate(data_get($attachment, 'created_on'));
741
             if ($created) {
741
             if ($created) {
742
                 $allegato->timestamps = false;
742
                 $allegato->timestamps = false;
743
                 $allegato->created_at = $created;
743
                 $allegato->created_at = $created;
792
             $todo->public = true;
792
             $todo->public = true;
793
             $done = (bool) ($item['is_done'] ?? $item['done'] ?? false);
793
             $done = (bool) ($item['is_done'] ?? $item['done'] ?? false);
794
             if ($done && ! $todo->completato_at) {
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
             if (! $done) {
797
             if (! $done) {
798
                 $todo->completato_at = null;
798
                 $todo->completato_at = null;
1104
             if ($beforeCc === $afterCc && $afterCc === []) {
1104
             if ($beforeCc === $afterCc && $afterCc === []) {
1105
                 $stats['empty']++;
1105
                 $stats['empty']++;
1106
                 if (! $dryRun && $segnalazione->isDirty('meta')) {
1106
                 if (! $dryRun && $segnalazione->isDirty('meta')) {
1107
-                    Segnalazione::withoutAuditing(function () use ($segnalazione) {
1108
-                        $segnalazione->save();
1109
-                    });
1107
+                    self::saveWithoutTouchingTimestamps($segnalazione);
1110
                 }
1108
                 }
1111
                 continue;
1109
                 continue;
1112
             }
1110
             }
1121
                 continue;
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
         return $stats;
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
      * @return list<string>
1191
      * @return list<string>
1134
      */
1192
      */
1585
 
1643
 
1586
     protected function stampSegnalazioneFromLastAnnotazione(Segnalazione $segnalazione, mixed $created = null): void
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
         $createdAt = $createdAt ?? $segnalazione->created_at;
1647
         $createdAt = $createdAt ?? $segnalazione->created_at;
1590
-        $updatedAt = $this->lastActivityAt($segnalazione) ?? $createdAt;
1648
+        $updatedAt = self::lastActivityAt($segnalazione) ?? $createdAt;
1591
 
1649
 
1592
         $segnalazione->timestamps = false;
1650
         $segnalazione->timestamps = false;
1593
         if ($createdAt) {
1651
         if ($createdAt) {
1597
         $segnalazione->saveQuietly();
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
         $latest = null;
1660
         $latest = null;
1603
         $rows = Annotazione::query()
1661
         $rows = Annotazione::query()
1607
         foreach ($rows as $annotazione) {
1665
         foreach ($rows as $annotazione) {
1608
             foreach (['updated_at', 'created_at', 'tempo_data'] as $column) {
1666
             foreach (['updated_at', 'created_at', 'tempo_data'] as $column) {
1609
                 $raw = $annotazione->getAttributes()[$column] ?? null;
1667
                 $raw = $annotazione->getAttributes()[$column] ?? null;
1610
-                $parsed = $this->parseDate($raw);
1668
+                $parsed = self::parseDate($raw);
1611
                 if ($parsed && ($latest === null || $parsed->gt($latest))) {
1669
                 if ($parsed && ($latest === null || $parsed->gt($latest))) {
1612
                     $latest = $parsed;
1670
                     $latest = $parsed;
1613
                 }
1671
                 }
1617
         return $latest;
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
         $auditAt = Audit::query()
1682
         $auditAt = Audit::query()
1625
             ->where('auditable_type', $segnalazione->getMorphClass())
1683
             ->where('auditable_type', $segnalazione->getMorphClass())
1628
             ->where('tags', 'redmine')
1686
             ->where('tags', 'redmine')
1629
             ->max('created_at');
1687
             ->max('created_at');
1630
 
1688
 
1631
-        $parsed = $this->parseDate($auditAt);
1689
+        $parsed = self::parseDate($auditAt);
1632
         if ($parsed && ($latest === null || $parsed->gt($latest))) {
1690
         if ($parsed && ($latest === null || $parsed->gt($latest))) {
1633
             return $parsed;
1691
             return $parsed;
1634
         }
1692
         }
1715
 
1773
 
1716
     protected function stampDates(Annotazione $annotazione, mixed $created, mixed $updated): void
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
         if (! $createdAt) {
1778
         if (! $createdAt) {
1721
             return;
1779
             return;
1722
         }
1780
         }
1728
         $annotazione->timestamps = true;
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
         if ($value instanceof Carbon) {
1791
         if ($value instanceof Carbon) {
1734
             return $value;
1792
             return $value;
1735
         }
1793
         }
1794
+        if ($value instanceof \DateTimeInterface) {
1795
+            return Carbon::parse($value);
1796
+        }
1736
         if (! is_string($value) || trim($value) === '') {
1797
         if (! is_string($value) || trim($value) === '') {
1737
             return null;
1798
             return null;
1738
         }
1799
         }

Loading…
Отказ
Запис