Preserve rubrica identity during sync and fix ticket attachment previews

This commit is contained in:
michele 2026-04-09 18:44:51 +00:00
parent 684b71e2ff
commit 00b046d4d3
3 changed files with 219 additions and 85 deletions

View File

@ -1,6 +1,7 @@
<?php
namespace App\Console\Commands;
use App\Models\RubricaContattoCanale;
use App\Models\RubricaRuolo;
use App\Models\RubricaUniversale;
use App\Models\Stabile;
@ -373,6 +374,21 @@ public function handle(): int
$fiscal = $this->normalizeFiscalIds(! empty($t['cf']) ? (string) $t['cf'] : null);
$allEmails = $this->collectNormalizedEmails([
$t['email'] ?? null,
$row->email ?? null,
$row->e_mail_condomino ?? null,
$keyRow->email ?? null,
$keyRow->e_mail_condomino ?? null,
]);
$allPecs = $this->collectNormalizedEmails([
$t['pec'] ?? null,
$row->pec ?? null,
$row->pec_condominio ?? null,
$keyRow->pec ?? null,
$keyRow->pec_condominio ?? null,
]);
$data = [
'amministratore_id' => $amministratoreId,
'codice_univoco' => $rubricaCode,
@ -382,8 +398,8 @@ public function handle(): int
'tipo_contatto' => $ragione ? 'persona_giuridica' : 'persona_fisica',
'codice_fiscale' => $fiscal['cf'] ? mb_substr($fiscal['cf'], 0, 20) : null,
'partita_iva' => $fiscal['piva'] ? mb_substr($fiscal['piva'], 0, 20) : null,
'email' => ($emailNorm = $this->normalizeEmail(! empty($t['email']) ? (string) $t['email'] : null)) ? mb_substr($emailNorm, 0, 191) : null,
'pec' => ($pecNorm = $this->normalizeEmail(! empty($t['pec']) ? (string) $t['pec'] : null)) ? mb_substr($pecNorm, 0, 191) : null,
'email' => ($allEmails[0] ?? null) ? mb_substr((string) $allEmails[0], 0, 191) : null,
'pec' => ($allPecs[0] ?? null) ? mb_substr((string) $allPecs[0], 0, 191) : null,
'telefono_ufficio' => ($telNorm = $this->normalizePhone(! empty($t['telefono']) ? (string) $t['telefono'] : null)) ? mb_substr($telNorm, 0, 50) : null,
'telefono_cellulare' => ($cellNorm = $this->normalizePhone(! empty($t['cellulare']) ? (string) $t['cellulare'] : null)) ? mb_substr($cellNorm, 0, 50) : null,
'indirizzo' => $this->firstNonEmptyStr([
@ -418,24 +434,16 @@ public function handle(): int
continue;
}
$record = $this->findExistingRubrica($amministratoreId, $data) ?: RubricaUniversale::withTrashed()->where($match)->first();
$existingMatch = $this->findExistingRubricaMatch($amministratoreId, $data);
$record = $existingMatch['record'] ?: RubricaUniversale::withTrashed()->where($match)->first();
if ($record) {
if (method_exists($record, 'trashed') && $record->trashed()) {
$record->restore();
}
// Se ho trovato per identità (CF/PIVA/email/telefono), NON cambiare il codice_univoco esistente.
if (! empty($record->codice_univoco) && (string) $record->codice_univoco !== (string) $rubricaCode) {
unset($data['codice_univoco']);
}
// Non sovrascrivere note manuali: aggiungi solo se vuoto.
if (empty($record->note)) {
$data['note'] = $note;
}
$record->fill($data);
if ($record->isDirty()) {
$record->save();
}
$this->mergeImportDataIntoRubrica(
$record,
$data,
$rubricaCode,
$note,
(string) ($existingMatch['match_type'] ?? 'none'),
);
$updated++;
} else {
$data['note'] = $note;
@ -444,6 +452,16 @@ public function handle(): int
$record = RubricaUniversale::withTrashed()->where($match)->first();
}
if ($record) {
$this->syncRubricaEmailChannels(
$record,
(int) ($domain['id'] ?? 0),
$this->resolveUnitaImmobiliareId((int) ($domain['id'] ?? 0), $scala, $interno, $piano),
$allEmails,
$allPecs,
);
}
if ($linkRuolo && $record) {
$stabileId = (int) ($domain['id'] ?? 0);
if ($stabileId > 0) {
@ -590,18 +608,16 @@ public function handle(): int
$exists = RubricaUniversale::withTrashed()->where($match)->exists();
$exists ? $updated++ : $inserted++;
} else {
$record = $this->findExistingRubrica($amministratoreId, $data) ?: RubricaUniversale::withTrashed()->where($match)->first();
$existingMatch = $this->findExistingRubricaMatch($amministratoreId, $data);
$record = $existingMatch['record'] ?: RubricaUniversale::withTrashed()->where($match)->first();
if ($record) {
if (method_exists($record, 'trashed') && $record->trashed()) {
$record->restore();
}
if (! empty($record->codice_univoco) && (string) $record->codice_univoco !== (string) $rubricaCode) {
unset($data['codice_univoco']);
}
$record->fill($data);
if ($record->isDirty()) {
$record->save();
}
$this->mergeImportDataIntoRubrica(
$record,
$data,
$rubricaCode,
'Auto-sync comproprietari (staging)',
(string) ($existingMatch['match_type'] ?? 'none'),
);
$updated++;
} else {
$data['note'] = 'Auto-sync comproprietari (staging)';
@ -942,7 +958,10 @@ private function lookupTenantIdentityFromStaging(string $conn, string $codStabil
}
}
private function findExistingRubrica(int $amministratoreId, array $data): ?RubricaUniversale
/**
* @return array{record:?RubricaUniversale,match_type:string}
*/
private function findExistingRubricaMatch(int $amministratoreId, array $data): array
{
$ids = $this->normalizeFiscalIds($data['codice_fiscale'] ?? null);
$cf = $ids['cf'];
@ -968,7 +987,7 @@ private function findExistingRubrica(int $amministratoreId, array $data): ?Rubri
->orderBy('id')
->first();
if ($found) {
return $found;
return ['record' => $found, 'match_type' => 'fiscal'];
}
$orphan = RubricaUniversale::query()
@ -990,7 +1009,7 @@ private function findExistingRubrica(int $amministratoreId, array $data): ?Rubri
->orderBy('id')
->first();
if ($orphan) {
return $orphan;
return ['record' => $orphan, 'match_type' => 'fiscal'];
}
}
@ -1001,7 +1020,7 @@ private function findExistingRubrica(int $amministratoreId, array $data): ?Rubri
->where('email', $email)
->first();
if ($found) {
return $found;
return ['record' => $found, 'match_type' => 'email'];
}
$orphan = RubricaUniversale::query()
@ -1010,7 +1029,7 @@ private function findExistingRubrica(int $amministratoreId, array $data): ?Rubri
->orderBy('id')
->first();
if ($orphan) {
return $orphan;
return ['record' => $orphan, 'match_type' => 'email'];
}
}
@ -1029,7 +1048,7 @@ private function findExistingRubrica(int $amministratoreId, array $data): ?Rubri
})
->first();
if ($found) {
return $found;
return ['record' => $found, 'match_type' => 'phone'];
}
$orphan = RubricaUniversale::query()
@ -1044,11 +1063,86 @@ private function findExistingRubrica(int $amministratoreId, array $data): ?Rubri
->orderBy('id')
->first();
if ($orphan) {
return $orphan;
return ['record' => $orphan, 'match_type' => 'phone'];
}
}
return null;
return ['record' => null, 'match_type' => 'none'];
}
private function mergeImportDataIntoRubrica(
RubricaUniversale $record,
array $data,
string $rubricaCode,
string $note,
string $matchType,
): void {
if (method_exists($record, 'trashed') && $record->trashed()) {
$record->restore();
}
if (! empty($record->codice_univoco) && (string) $record->codice_univoco !== $rubricaCode) {
unset($data['codice_univoco']);
}
$data = $this->mergeRubricaDataPreservingExisting($record, $data);
if (empty($record->note)) {
$data['note'] = $note;
} else {
unset($data['note']);
}
$record->fill($data);
if ($record->isDirty()) {
$record->save();
}
}
private function mergeRubricaDataPreservingExisting(RubricaUniversale $record, array $incoming): array
{
$preserveIfFilled = [
'ragione_sociale',
'nome',
'cognome',
'tipo_contatto',
'codice_fiscale',
'partita_iva',
'email',
'pec',
'telefono_ufficio',
'telefono_cellulare',
'telefono_casa',
'indirizzo',
'cap',
'citta',
'provincia',
];
foreach ($preserveIfFilled as $field) {
if ($this->isFilledScalar($record->{$field} ?? null)) {
$incoming[$field] = $record->{$field};
}
}
if ($this->isFilledScalar($record->amministratore_id ?? null)) {
$incoming['amministratore_id'] = (int) $record->amministratore_id;
}
return $incoming;
}
private function isFilledScalar(mixed $value): bool
{
if ($value === null) {
return false;
}
if (is_string($value)) {
return trim($value) !== '';
}
return true;
}
private function normalizeDateToYmd(mixed $v): ?string
@ -1361,4 +1455,76 @@ private function normalizePercentValue(mixed $raw): ?float
return (float) $raw;
}
/** @return array<int,string> */
private function collectNormalizedEmails(array $values): array
{
$result = [];
foreach ($values as $value) {
if ($value === null) {
continue;
}
$parts = preg_split('/[;,\n\r]+/', (string) $value) ?: [];
foreach ($parts as $part) {
$email = $this->normalizeEmail($part);
if ($email === null || in_array($email, $result, true)) {
continue;
}
$result[] = $email;
}
}
return $result;
}
/** @param array<int,string> $emails */
private function syncRubricaEmailChannels(RubricaUniversale $record, int $stabileId, ?int $unitaId, array $emails, array $pecs): void
{
if (! Schema::hasTable('rubrica_contatti_canali')) {
return;
}
foreach ($emails as $index => $email) {
RubricaContattoCanale::query()->updateOrCreate(
[
'rubrica_id' => (int) $record->id,
'tipo' => 'email',
'valore' => $email,
'stabile_id' => $stabileId > 0 ? $stabileId : null,
'unita_immobiliare_id' => $unitaId,
],
[
'etichetta' => $index === 0 ? 'Email primaria legacy' : 'Email aggiuntiva legacy',
'is_principale' => $index === 0,
'meta' => [
'source' => 'gescon_import.condomin',
'scope' => 'auto_sync_anagrafiche',
],
],
);
}
foreach ($pecs as $index => $pec) {
RubricaContattoCanale::query()->updateOrCreate(
[
'rubrica_id' => (int) $record->id,
'tipo' => 'pec',
'valore' => $pec,
'stabile_id' => $stabileId > 0 ? $stabileId : null,
'unita_immobiliare_id' => $unitaId,
],
[
'etichetta' => $index === 0 ? 'PEC primaria legacy' : 'PEC aggiuntiva legacy',
'is_principale' => $index === 0,
'meta' => [
'source' => 'gescon_import.condomin',
'scope' => 'auto_sync_anagrafiche',
],
],
);
}
}
}

View File

@ -370,7 +370,7 @@ public function openAttachmentMapPreview(int $attachmentId): void
public function closeAttachmentPreview(): void
{
$this->attachmentPreview = null;
$this->attachmentPreview = null;
$this->attachmentMapPreview = null;
}

View File

@ -201,9 +201,7 @@
</div>
</div>
@elseif($activeTab === 'scheda')
@php
$ticket = $this->selectedTicket;
@endphp
@php($ticket = $this->selectedTicket)
@if(! $ticket)
<div class="mt-3 rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800">
Nessun ticket selezionato. Vai nella TAB Elenco e premi "Scheda" sulla riga del ticket.
@ -438,12 +436,8 @@
</div>
</div>
@endif
@endif
@if($activeTab === 'assicurazione')
@php
$ticket = $this->selectedTicket;
@endphp
@elseif($activeTab === 'assicurazione')
@php($ticket = $this->selectedTicket)
@if(! $ticket)
<div class="mt-3 rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800">
Nessun ticket selezionato. Apri un ticket e poi entra nella Tab 4 assicurazione.
@ -586,28 +580,7 @@
</div>
@if($attachmentPreview)
@php($attachmentPreviewUrl = (string) ($attachmentPreview['url'] ?? ''))
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" x-data='{
zoom: 1,
pdfPage: 1,
iframeKey: 0,
pdfUrl: @js($attachmentPreviewUrl),
refreshPdf() { this.iframeKey++; },
zoomIn() { this.zoom = Math.min(this.zoom + 0.2, 4); },
zoomOut() { this.zoom = Math.max(this.zoom - 0.2, 0.5); },
resetZoom() { this.zoom = 1; },
nextPage() { this.pdfPage = this.pdfPage + 1; this.refreshPdf(); },
prevPage() { this.pdfPage = Math.max(this.pdfPage - 1, 1); this.refreshPdf(); },
printPdf() {
const frame = this.$refs.pdfFrame;
if (frame?.contentWindow) {
frame.contentWindow.focus();
frame.contentWindow.print();
} else if (this.pdfUrl) {
window.open(this.pdfUrl, "_blank");
}
}
}'>
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
<div class="h-[92vh] w-full max-w-[96vw] rounded-xl bg-white shadow-2xl">
<div class="flex items-center justify-between border-b px-4 py-3">
<div>
@ -630,20 +603,15 @@
@endif
</div>
<div class="flex items-center gap-2">
@if($attachmentPreview['is_image'] ?? false)
<button type="button" x-on:click="zoomOut()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">-</button>
<button type="button" x-on:click="resetZoom()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">100%</button>
<button type="button" x-on:click="zoomIn()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">+</button>
@elseif($attachmentPreview['is_pdf'] ?? false)
<button type="button" x-on:click="prevPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina precedente</button>
<span class="text-xs text-slate-500">Pagina <span x-text="pdfPage"></span></span>
<button type="button" x-on:click="nextPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina successiva</button>
<button type="button" x-on:click="printPdf()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Stampa PDF</button>
@if(!empty($attachmentPreview['is_image']))
<span class="rounded-md bg-slate-100 px-2 py-1 text-xs text-slate-600">Anteprima immagine</span>
@elseif(!empty($attachmentPreview['is_pdf']))
<span class="rounded-md bg-slate-100 px-2 py-1 text-xs text-slate-600">Anteprima PDF</span>
@endif
@if(!empty($attachmentPreview['details']['gps']))
<button type="button" wire:click="openAttachmentMapPreview({{ (int) ($attachmentPreview['id'] ?? 0) }})" class="rounded-md bg-emerald-100 px-2 py-1 text-xs text-emerald-800 hover:bg-emerald-200">Google Maps</button>
@endif
<a href="{{ $attachmentPreview['url'] }}" target="_blank" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Apri in nuova scheda</a>
<a href="{{ (string) ($attachmentPreview['url'] ?? '') }}" target="_blank" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Apri in nuova scheda</a>
<button type="button" wire:click="closeAttachmentPreview" class="rounded-md bg-gray-100 px-2 py-1 text-xs hover:bg-gray-200">Chiudi</button>
</div>
</div>
@ -655,18 +623,18 @@
<div class="mt-1 whitespace-pre-wrap">{{ $attachmentPreview['details']['full_description'] }}</div>
</div>
@endif
@if($attachmentPreview['is_image'] ?? false)
@if(!empty($attachmentPreview['is_image']))
<div class="h-full overflow-auto rounded-lg bg-slate-50 p-4">
<div class="flex min-h-full min-w-full items-start justify-center">
<img src="{{ $attachmentPreview['url'] }}" alt="Anteprima allegato" class="max-w-none rounded border bg-white shadow" x-bind:style="`width: ${zoom * 100}%; height: auto;`" />
<img src="{{ (string) ($attachmentPreview['url'] ?? '') }}" alt="Anteprima allegato" class="max-w-full rounded border bg-white shadow" />
</div>
</div>
@elseif($attachmentPreview['is_pdf'] ?? false)
<iframe x-ref="pdfFrame" x-bind:key="iframeKey" x-bind:src="pdfUrl + '#toolbar=1&navpanes=0&scrollbar=1&view=FitH&zoom=page-width&page=' + pdfPage" class="h-[84vh] w-full rounded border"></iframe>
@elseif(!empty($attachmentPreview['is_pdf']))
<iframe src="{{ (string) ($attachmentPreview['url'] ?? '') }}#toolbar=1&navpanes=0&scrollbar=1&view=FitH&zoom=page-width" class="h-[84vh] w-full rounded border"></iframe>
@else
<div class="rounded-md border bg-gray-50 p-4 text-sm text-gray-700">
Anteprima non disponibile per questo formato. Scarica o apri il file:
<a href="{{ $attachmentPreview['url'] }}" class="ml-1 text-primary-600 hover:underline">Apri file</a>
<a href="{{ (string) ($attachmentPreview['url'] ?? '') }}" class="ml-1 text-primary-600 hover:underline">Apri file</a>
</div>
@endif
</div>