|
|
@@ -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
|
}
|