Preserve rubrica identity during sync and fix ticket attachment previews
This commit is contained in:
parent
684b71e2ff
commit
00b046d4d3
|
|
@ -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',
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,7 +370,7 @@ public function openAttachmentMapPreview(int $attachmentId): void
|
|||
|
||||
public function closeAttachmentPreview(): void
|
||||
{
|
||||
$this->attachmentPreview = null;
|
||||
$this->attachmentPreview = null;
|
||||
$this->attachmentMapPreview = null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user