Rubrica cleanup, PBX/Google contact sync, and import pilot validation
This commit is contained in:
parent
8e8f95b7c6
commit
a9204e125b
401
app/Console/Commands/GesconBonificaRubricaCanaliCommand.php
Normal file
401
app/Console/Commands/GesconBonificaRubricaCanaliCommand.php
Normal file
|
|
@ -0,0 +1,401 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\RubricaContattoCanale;
|
||||||
|
use App\Models\RubricaUniversale;
|
||||||
|
use App\Models\Stabile;
|
||||||
|
use App\Support\PhoneNumber;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class GesconBonificaRubricaCanaliCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'gescon:bonifica-rubrica-canali
|
||||||
|
{--stabile= : Codice stabile da bonificare}
|
||||||
|
{--admin-id= : Limita la bonifica a un amministratore}
|
||||||
|
{--limit=5000 : Numero massimo di anagrafiche da analizzare}
|
||||||
|
{--apply : Applica realmente le modifiche}';
|
||||||
|
|
||||||
|
protected $description = 'Normalizza telefoni, email e PEC multi-valore della rubrica e li materializza in rubrica_contatti_canali.';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('rubrica_universale')) {
|
||||||
|
$this->error('Tabella rubrica_universale non disponibile.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('rubrica_contatti_canali')) {
|
||||||
|
$this->error('Tabella rubrica_contatti_canali non disponibile.');
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apply = (bool) $this->option('apply');
|
||||||
|
$limit = max(1, min((int) ($this->option('limit') ?? 5000), 50000));
|
||||||
|
$adminId = (int) ($this->option('admin-id') ?: 0);
|
||||||
|
$stabile = trim((string) ($this->option('stabile') ?? ''));
|
||||||
|
|
||||||
|
$query = RubricaUniversale::query()->orderBy('id');
|
||||||
|
|
||||||
|
if ($adminId > 0 && Schema::hasColumn('rubrica_universale', 'amministratore_id')) {
|
||||||
|
$query->where('amministratore_id', $adminId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($stabile !== '') {
|
||||||
|
$rubricaIds = $this->resolveScopedRubricaIds($stabile);
|
||||||
|
if ($rubricaIds === []) {
|
||||||
|
$this->warn('Nessuna rubrica trovata per lo stabile richiesto.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereIn('id', $rubricaIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
$records = $query->limit($limit)->get();
|
||||||
|
if ($records->isEmpty()) {
|
||||||
|
$this->info('Nessuna rubrica da bonificare.');
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'analizzate' => 0,
|
||||||
|
'rubriche_aggiornate' => 0,
|
||||||
|
'canali_creati' => 0,
|
||||||
|
'canali_aggiornati' => 0,
|
||||||
|
'saltate' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($records as $rubrica) {
|
||||||
|
$stats['analizzate']++;
|
||||||
|
|
||||||
|
$contactSet = $this->extractStructuredContacts($rubrica);
|
||||||
|
if ($contactSet['channels'] === [] && $contactSet['updates'] === []) {
|
||||||
|
$stats['saltate']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($apply && $contactSet['updates'] !== []) {
|
||||||
|
$rubrica->fill($contactSet['updates']);
|
||||||
|
$rubrica->save();
|
||||||
|
$stats['rubriche_aggiornate']++;
|
||||||
|
} elseif ($contactSet['updates'] !== []) {
|
||||||
|
$stats['rubriche_aggiornate']++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($contactSet['channels'] as $channel) {
|
||||||
|
$existing = RubricaContattoCanale::query()
|
||||||
|
->where('rubrica_id', (int) $rubrica->id)
|
||||||
|
->where('tipo', $channel['tipo'])
|
||||||
|
->where('valore', $channel['valore'])
|
||||||
|
->whereNull('stabile_id')
|
||||||
|
->whereNull('unita_immobiliare_id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $apply) {
|
||||||
|
if ($existing instanceof RubricaContattoCanale) {
|
||||||
|
$stats['canali_aggiornati']++;
|
||||||
|
} else {
|
||||||
|
$stats['canali_creati']++;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existing instanceof RubricaContattoCanale) {
|
||||||
|
$existing->etichetta = $channel['etichetta'];
|
||||||
|
$existing->is_principale = $channel['is_principale'];
|
||||||
|
$existing->meta = $channel['meta'];
|
||||||
|
$existing->save();
|
||||||
|
$stats['canali_aggiornati']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RubricaContattoCanale::query()->create([
|
||||||
|
'rubrica_id' => (int) $rubrica->id,
|
||||||
|
'tipo' => $channel['tipo'],
|
||||||
|
'etichetta' => $channel['etichetta'],
|
||||||
|
'valore' => $channel['valore'],
|
||||||
|
'is_principale' => $channel['is_principale'],
|
||||||
|
'meta' => $channel['meta'],
|
||||||
|
]);
|
||||||
|
$stats['canali_creati']++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = $apply ? 'apply' : 'dry-run';
|
||||||
|
$this->info('Bonifica rubrica canali completata (' . $mode . '): ' . json_encode($stats, JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, int> */
|
||||||
|
private function resolveScopedRubricaIds(string $stabileCode): array
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('stabili')) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$trimmed = ltrim($stabileCode, '0');
|
||||||
|
$stabileIds = Stabile::query()
|
||||||
|
->where(function (Builder $query) use ($stabileCode, $trimmed): void {
|
||||||
|
$query->where('codice_stabile', $stabileCode);
|
||||||
|
if ($trimmed !== '' && $trimmed !== $stabileCode) {
|
||||||
|
$query->orWhere('codice_stabile', $trimmed);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->pluck('id')
|
||||||
|
->map(fn($id) => (int) $id)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($stabileIds === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rubricaIds = RubricaContattoCanale::query()
|
||||||
|
->whereIn('stabile_id', $stabileIds)
|
||||||
|
->pluck('rubrica_id')
|
||||||
|
->map(fn($id) => (int) $id)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if (Schema::hasTable('rubrica_ruoli')) {
|
||||||
|
$roleRubricaIds = \DB::table('rubrica_ruoli')
|
||||||
|
->whereIn('stabile_id', $stabileIds)
|
||||||
|
->pluck('rubrica_id')
|
||||||
|
->map(fn($id) => (int) $id)
|
||||||
|
->all();
|
||||||
|
$rubricaIds = array_merge($rubricaIds, $roleRubricaIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stabileRubricaIds = Stabile::query()
|
||||||
|
->whereIn('id', $stabileIds)
|
||||||
|
->whereNotNull('rubrica_id')
|
||||||
|
->pluck('rubrica_id')
|
||||||
|
->map(fn($id) => (int) $id)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$rubricaIds = array_merge($rubricaIds, $stabileRubricaIds);
|
||||||
|
$rubricaIds = array_values(array_unique(array_filter($rubricaIds, fn(int $id): bool => $id > 0)));
|
||||||
|
|
||||||
|
return $rubricaIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{updates: array<string,mixed>, channels: array<int,array<string,mixed>>} */
|
||||||
|
private function extractStructuredContacts(RubricaUniversale $rubrica): array
|
||||||
|
{
|
||||||
|
$emails = $this->uniqueEmails([
|
||||||
|
...$this->splitEmails((string) ($rubrica->email ?? '')),
|
||||||
|
]);
|
||||||
|
$pecs = $this->uniqueEmails([
|
||||||
|
...$this->splitEmails((string) ($rubrica->pec ?? '')),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mobilePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_cellulare ?? '')), 'cellulare');
|
||||||
|
$officePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_ufficio ?? '')), 'telefono');
|
||||||
|
$homePhones = $this->normalizePhones($this->splitPhones((string) ($rubrica->telefono_casa ?? '')), 'telefono');
|
||||||
|
|
||||||
|
$primaryEmail = $emails[0] ?? $this->normalizeEmail((string) ($rubrica->email ?? ''));
|
||||||
|
$primaryPec = $pecs[0] ?? $this->normalizeEmail((string) ($rubrica->pec ?? ''));
|
||||||
|
$primaryMobile = $mobilePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_cellulare ?? ''));
|
||||||
|
$primaryOffice = $officePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_ufficio ?? ''));
|
||||||
|
$primaryHome = $homePhones[0]['value'] ?? $this->normalizePhoneDisplay((string) ($rubrica->telefono_casa ?? ''));
|
||||||
|
|
||||||
|
$updates = [];
|
||||||
|
if ($primaryEmail !== $this->normalizeEmail((string) ($rubrica->email ?? ''))) {
|
||||||
|
$updates['email'] = $primaryEmail;
|
||||||
|
}
|
||||||
|
if ($primaryPec !== $this->normalizeEmail((string) ($rubrica->pec ?? ''))) {
|
||||||
|
$updates['pec'] = $primaryPec;
|
||||||
|
}
|
||||||
|
if ($primaryMobile !== $this->normalizePhoneDisplay((string) ($rubrica->telefono_cellulare ?? ''))) {
|
||||||
|
$updates['telefono_cellulare'] = $primaryMobile;
|
||||||
|
[$e164, $prefix, $national] = PhoneNumber::splitItaly($primaryMobile);
|
||||||
|
$updates['prefisso_cellulare'] = $prefix;
|
||||||
|
$updates['telefono_cellulare_nazionale'] = $national;
|
||||||
|
}
|
||||||
|
if ($primaryOffice !== $this->normalizePhoneDisplay((string) ($rubrica->telefono_ufficio ?? ''))) {
|
||||||
|
$updates['telefono_ufficio'] = $primaryOffice;
|
||||||
|
}
|
||||||
|
if ($primaryHome !== $this->normalizePhoneDisplay((string) ($rubrica->telefono_casa ?? ''))) {
|
||||||
|
$updates['telefono_casa'] = $primaryHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
$channels = [];
|
||||||
|
foreach ($emails as $index => $email) {
|
||||||
|
$channels[] = $this->buildChannel('email', $email, $index === 0 ? 'Email primaria' : 'Email aggiuntiva', $index === 0, 'rubrica.email');
|
||||||
|
}
|
||||||
|
foreach ($pecs as $index => $email) {
|
||||||
|
$channels[] = $this->buildChannel('pec', $email, $index === 0 ? 'PEC primaria' : 'PEC aggiuntiva', $index === 0, 'rubrica.pec');
|
||||||
|
}
|
||||||
|
foreach ($mobilePhones as $index => $phone) {
|
||||||
|
$channels[] = $this->buildChannel('cellulare', $phone['value'], $index === 0 ? 'Cellulare principale' : 'Cellulare aggiuntivo', $index === 0, 'rubrica.telefono_cellulare');
|
||||||
|
}
|
||||||
|
foreach ($officePhones as $index => $phone) {
|
||||||
|
$channels[] = $this->buildChannel('telefono', $phone['value'], $index === 0 ? 'Telefono ufficio' : 'Telefono aggiuntivo', $index === 0, 'rubrica.telefono_ufficio');
|
||||||
|
}
|
||||||
|
foreach ($homePhones as $index => $phone) {
|
||||||
|
$channels[] = $this->buildChannel('telefono', $phone['value'], $index === 0 ? 'Telefono casa' : 'Telefono casa aggiuntivo', false, 'rubrica.telefono_casa');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'updates' => $updates,
|
||||||
|
'channels' => $this->deduplicateChannels($channels),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, string> */
|
||||||
|
private function splitEmails(string $value): array
|
||||||
|
{
|
||||||
|
$raw = trim($value);
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = preg_split('/[;,\n\r]+/', $raw) ?: [];
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(fn(string $part): ?string => $this->normalizeEmail($part), $parts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, string> */
|
||||||
|
private function splitPhones(string $value): array
|
||||||
|
{
|
||||||
|
$raw = trim($value);
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! preg_match('/[;,\n\r\/|]/', $raw)) {
|
||||||
|
return [$raw];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = preg_split('/[;,\n\r\/|]+/', $raw) ?: [];
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(fn(string $part): ?string => $this->normalizePhoneDisplay($part), $parts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeEmail(?string $value): ?string
|
||||||
|
{
|
||||||
|
$normalized = strtolower(trim((string) $value));
|
||||||
|
return filter_var($normalized, FILTER_VALIDATE_EMAIL) ? $normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizePhoneDisplay(?string $value): ?string
|
||||||
|
{
|
||||||
|
$raw = trim((string) $value);
|
||||||
|
if ($raw === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clean = preg_replace('/[^0-9+]/', '', $raw);
|
||||||
|
return is_string($clean) && $clean !== '' ? $clean : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, string> $values
|
||||||
|
* @return array<int, array{value:string,type:string}>
|
||||||
|
*/
|
||||||
|
private function normalizePhones(array $values, string $defaultType): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$clean = $this->normalizePhoneDisplay($value);
|
||||||
|
if ($clean === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$digits = PhoneNumber::normalizeDigits($clean);
|
||||||
|
if ($digits === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $defaultType;
|
||||||
|
if (str_starts_with($digits, '3')) {
|
||||||
|
$type = 'cellulare';
|
||||||
|
} elseif (str_starts_with($digits, '0')) {
|
||||||
|
$type = 'telefono';
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedValue = $type === 'cellulare'
|
||||||
|
? (PhoneNumber::toE164Italy($clean) ?? $clean)
|
||||||
|
: $clean;
|
||||||
|
|
||||||
|
$result[] = [
|
||||||
|
'value' => $normalizedValue,
|
||||||
|
'type' => $type,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$unique = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$key = $row['type'] . '|' . PhoneNumber::normalizeDigits($row['value']);
|
||||||
|
if (isset($seen[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[$key] = true;
|
||||||
|
$unique[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, string> $values
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function uniqueEmails(array $values): array
|
||||||
|
{
|
||||||
|
$unique = [];
|
||||||
|
foreach ($values as $value) {
|
||||||
|
$normalized = $this->normalizeEmail($value);
|
||||||
|
if ($normalized === null || in_array($normalized, $unique, true)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$unique[] = $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $unique;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string,mixed> */
|
||||||
|
private function buildChannel(string $tipo, string $valore, string $etichetta, bool $isPrincipale, string $source): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tipo' => $tipo,
|
||||||
|
'valore' => $valore,
|
||||||
|
'etichetta' => $etichetta,
|
||||||
|
'is_principale' => $isPrincipale,
|
||||||
|
'meta' => [
|
||||||
|
'source' => $source,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int,array<string,mixed>> $channels
|
||||||
|
* @return array<int,array<string,mixed>>
|
||||||
|
*/
|
||||||
|
private function deduplicateChannels(array $channels): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
$key = $channel['tipo'] . '|';
|
||||||
|
if (in_array($channel['tipo'], ['telefono', 'cellulare'], true)) {
|
||||||
|
$key .= PhoneNumber::normalizeDigits((string) $channel['valore']);
|
||||||
|
} else {
|
||||||
|
$key .= strtolower((string) $channel['valore']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($seen[$key])) {
|
||||||
|
if (! empty($channel['is_principale'])) {
|
||||||
|
$result[$seen[$key]]['is_principale'] = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$key] = count($result);
|
||||||
|
$result[] = $channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,14 @@
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Amministratore;
|
use App\Models\Amministratore;
|
||||||
|
use App\Models\RubricaContattoCanale;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
use App\Support\PhoneNumber;
|
use App\Support\PhoneNumber;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class GooglePushRubricaContactsCommand extends Command
|
class GooglePushRubricaContactsCommand extends Command
|
||||||
{
|
{
|
||||||
|
|
@ -86,8 +88,10 @@ public function handle(): int
|
||||||
$errors = 0;
|
$errors = 0;
|
||||||
|
|
||||||
foreach ($records as $record) {
|
foreach ($records as $record) {
|
||||||
$email = trim((string) ($record->email ?? ''));
|
$emails = $this->emailsForGoogle($record);
|
||||||
$phone = $this->mobileForGoogle($record) ?? '';
|
$phones = $this->phonesForGoogle($record);
|
||||||
|
$email = $emails[0]['value'] ?? '';
|
||||||
|
$phone = $phones[0]['value'] ?? '';
|
||||||
$phoneNormalized = $this->normalizePhone($phone);
|
$phoneNormalized = $this->normalizePhone($phone);
|
||||||
|
|
||||||
[$givenName, $familyName, $fullName] = $this->buildGoogleNameParts($record);
|
[$givenName, $familyName, $fullName] = $this->buildGoogleNameParts($record);
|
||||||
|
|
@ -100,11 +104,11 @@ public function handle(): int
|
||||||
]],
|
]],
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($email !== '') {
|
if ($emails !== []) {
|
||||||
$payload['emailAddresses'] = [['value' => $email]];
|
$payload['emailAddresses'] = $emails;
|
||||||
}
|
}
|
||||||
if ($phone !== '') {
|
if ($phones !== []) {
|
||||||
$payload['phoneNumbers'] = [['value' => $phone, 'type' => 'mobile', 'formattedType' => 'Cellulare']];
|
$payload['phoneNumbers'] = $phones;
|
||||||
}
|
}
|
||||||
|
|
||||||
$resourceName = null;
|
$resourceName = null;
|
||||||
|
|
@ -282,12 +286,14 @@ private function loadGoogleContactIndex(Amministratore $amministratore, array $g
|
||||||
|
|
||||||
foreach ($connections as $person) {
|
foreach ($connections as $person) {
|
||||||
$resourceName = trim((string) Arr::get($person, 'resourceName', ''));
|
$resourceName = trim((string) Arr::get($person, 'resourceName', ''));
|
||||||
$email = trim((string) Arr::get($person, 'emailAddresses.0.value', ''));
|
foreach ((array) Arr::get($person, 'emailAddresses', []) as $emailRow) {
|
||||||
$phone = trim((string) Arr::get($person, 'phoneNumbers.0.value', ''));
|
$email = trim((string) Arr::get($emailRow, 'value', ''));
|
||||||
|
|
||||||
if ($email !== '' && $resourceName !== '') {
|
if ($email !== '' && $resourceName !== '') {
|
||||||
$existingEmails[strtolower($email)] = $resourceName;
|
$existingEmails[strtolower($email)] = $resourceName;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
foreach ((array) Arr::get($person, 'phoneNumbers', []) as $phoneRow) {
|
||||||
|
$phone = trim((string) Arr::get($phoneRow, 'value', ''));
|
||||||
if ($phone !== '' && $resourceName !== '') {
|
if ($phone !== '' && $resourceName !== '') {
|
||||||
$normalized = $this->normalizePhone($phone);
|
$normalized = $this->normalizePhone($phone);
|
||||||
if ($normalized !== '') {
|
if ($normalized !== '') {
|
||||||
|
|
@ -295,6 +301,7 @@ private function loadGoogleContactIndex(Amministratore $amministratore, array $g
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$nextPageToken = $response->json('nextPageToken');
|
$nextPageToken = $response->json('nextPageToken');
|
||||||
if (! is_string($nextPageToken) || $nextPageToken === '') {
|
if (! is_string($nextPageToken) || $nextPageToken === '') {
|
||||||
|
|
@ -408,6 +415,126 @@ private function mobileForGoogle(RubricaUniversale $record): ?string
|
||||||
return PhoneNumber::toE164Italy($candidate);
|
return PhoneNumber::toE164Italy($candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<int, array<string, string>> */
|
||||||
|
private function emailsForGoogle(RubricaUniversale $record): array
|
||||||
|
{
|
||||||
|
$emails = [];
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
$push = function (?string $value, string $type, string $formattedType) use (&$emails, &$seen): void {
|
||||||
|
$email = strtolower(trim((string) $value));
|
||||||
|
if ($email === '' || isset($seen[$email])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$seen[$email] = true;
|
||||||
|
$emails[] = [
|
||||||
|
'value' => $email,
|
||||||
|
'type' => $type,
|
||||||
|
'formattedType' => $formattedType,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ($this->splitEmailValues((string) ($record->email ?? '')) as $email) {
|
||||||
|
$push($email, 'work', 'Email');
|
||||||
|
}
|
||||||
|
foreach ($this->splitEmailValues((string) ($record->pec ?? '')) as $email) {
|
||||||
|
$push($email, 'other', 'PEC');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('rubrica_contatti_canali')) {
|
||||||
|
$channels = RubricaContattoCanale::query()
|
||||||
|
->where('rubrica_id', (int) $record->id)
|
||||||
|
->whereIn('tipo', ['email', 'pec'])
|
||||||
|
->orderByDesc('is_principale')
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['tipo', 'valore']);
|
||||||
|
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
$push((string) ($channel->valore ?? ''), (string) ($channel->tipo ?? '') === 'pec' ? 'other' : 'work', (string) ($channel->tipo ?? '') === 'pec' ? 'PEC' : 'Email');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $emails;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, array<string, string>> */
|
||||||
|
private function phonesForGoogle(RubricaUniversale $record): array
|
||||||
|
{
|
||||||
|
$phones = [];
|
||||||
|
$seen = [];
|
||||||
|
|
||||||
|
$push = function (?string $value, string $type, string $formattedType) use (&$phones, &$seen): void {
|
||||||
|
$phone = trim((string) $value);
|
||||||
|
if ($phone === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = $this->normalizePhone($phone);
|
||||||
|
if ($normalized === '' || isset($seen[$normalized])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$seen[$normalized] = true;
|
||||||
|
$phones[] = [
|
||||||
|
'value' => $phone,
|
||||||
|
'type' => $type,
|
||||||
|
'formattedType' => $formattedType,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach ($this->splitPhoneValues((string) ($record->telefono_cellulare ?? '')) as $phone) {
|
||||||
|
$push(PhoneNumber::toE164Italy($phone) ?? $phone, 'mobile', 'Cellulare');
|
||||||
|
}
|
||||||
|
if (($primaryMobile = $this->mobileForGoogle($record)) !== null) {
|
||||||
|
$push($primaryMobile, 'mobile', 'Cellulare');
|
||||||
|
}
|
||||||
|
foreach ($this->splitPhoneValues((string) ($record->telefono_ufficio ?? '')) as $phone) {
|
||||||
|
$push($phone, 'work', 'Ufficio');
|
||||||
|
}
|
||||||
|
foreach ($this->splitPhoneValues((string) ($record->telefono_casa ?? '')) as $phone) {
|
||||||
|
$push($phone, 'home', 'Casa');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('rubrica_contatti_canali')) {
|
||||||
|
$channels = RubricaContattoCanale::query()
|
||||||
|
->where('rubrica_id', (int) $record->id)
|
||||||
|
->whereIn('tipo', ['telefono', 'cellulare'])
|
||||||
|
->orderByDesc('is_principale')
|
||||||
|
->orderBy('id')
|
||||||
|
->get(['tipo', 'valore']);
|
||||||
|
|
||||||
|
foreach ($channels as $channel) {
|
||||||
|
$isMobile = (string) ($channel->tipo ?? '') === 'cellulare';
|
||||||
|
$value = $isMobile ? (PhoneNumber::toE164Italy((string) ($channel->valore ?? '')) ?? (string) ($channel->valore ?? '')) : (string) ($channel->valore ?? '');
|
||||||
|
$push($value, $isMobile ? 'mobile' : 'work', $isMobile ? 'Cellulare' : 'Telefono');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $phones;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, string> */
|
||||||
|
private function splitEmailValues(string $value): array
|
||||||
|
{
|
||||||
|
$parts = preg_split('/[;,\n\r]+/', trim($value)) ?: [];
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(function (string $part): ?string {
|
||||||
|
$email = strtolower(trim($part));
|
||||||
|
return filter_var($email, FILTER_VALIDATE_EMAIL) ? $email : null;
|
||||||
|
}, $parts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int, string> */
|
||||||
|
private function splitPhoneValues(string $value): array
|
||||||
|
{
|
||||||
|
$parts = preg_split('/[;,\n\r\/|]+/', trim($value)) ?: [];
|
||||||
|
|
||||||
|
return array_values(array_filter(array_map(function (string $part): ?string {
|
||||||
|
$clean = preg_replace('/[^0-9+]/', '', trim($part));
|
||||||
|
return is_string($clean) && $clean !== '' ? $clean : null;
|
||||||
|
}, $parts)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{0:string,1:string,2:string}
|
* @return array{0:string,1:string,2:string}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Amministratore;
|
use App\Models\Amministratore;
|
||||||
|
use App\Models\RubricaContattoCanale;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
use App\Support\PhoneNumber;
|
use App\Support\PhoneNumber;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class GoogleSyncRubricaContactsCommand extends Command
|
class GoogleSyncRubricaContactsCommand extends Command
|
||||||
{
|
{
|
||||||
|
|
@ -194,9 +196,13 @@ public function handle(): int
|
||||||
$existing->modificato_da = $adminUserId;
|
$existing->modificato_da = $adminUserId;
|
||||||
}
|
}
|
||||||
$existing->save();
|
$existing->save();
|
||||||
|
$this->syncAdditionalGoogleChannels($existing, $person);
|
||||||
}
|
}
|
||||||
$updated++;
|
$updated++;
|
||||||
} else {
|
} else {
|
||||||
|
if (! $dryRun) {
|
||||||
|
$this->syncAdditionalGoogleChannels($existing, $person);
|
||||||
|
}
|
||||||
$skipped++;
|
$skipped++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,7 +214,7 @@ public function handle(): int
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
RubricaUniversale::create([
|
$createdRubrica = RubricaUniversale::create([
|
||||||
'nome' => $nome,
|
'nome' => $nome,
|
||||||
'cognome' => $cognome,
|
'cognome' => $cognome,
|
||||||
'tipo_contatto' => $tipoContatto,
|
'tipo_contatto' => $tipoContatto,
|
||||||
|
|
@ -225,6 +231,7 @@ public function handle(): int
|
||||||
'creato_da' => $adminUserId > 0 ? $adminUserId : null,
|
'creato_da' => $adminUserId > 0 ? $adminUserId : null,
|
||||||
'modificato_da' => $adminUserId > 0 ? $adminUserId : null,
|
'modificato_da' => $adminUserId > 0 ? $adminUserId : null,
|
||||||
]);
|
]);
|
||||||
|
$this->syncAdditionalGoogleChannels($createdRubrica, $person);
|
||||||
}
|
}
|
||||||
|
|
||||||
$nextPageToken = $response->json('nextPageToken');
|
$nextPageToken = $response->json('nextPageToken');
|
||||||
|
|
@ -400,4 +407,66 @@ private function shouldSyncField(?string $current, ?string $incoming, bool $isGo
|
||||||
|
|
||||||
return mb_strtolower($current) !== mb_strtolower($incoming);
|
return mb_strtolower($current) !== mb_strtolower($incoming);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function syncAdditionalGoogleChannels(RubricaUniversale $rubrica, array $person): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('rubrica_contatti_canali')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$emailRows = (array) Arr::get($person, 'emailAddresses', []);
|
||||||
|
foreach ($emailRows as $index => $emailRow) {
|
||||||
|
$value = strtolower(trim((string) Arr::get($emailRow, 'value', '')));
|
||||||
|
if ($value === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tipo = str_contains($value, '@cert.') ? 'pec' : 'email';
|
||||||
|
RubricaContattoCanale::query()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'rubrica_id' => (int) $rubrica->id,
|
||||||
|
'tipo' => $tipo,
|
||||||
|
'valore' => $value,
|
||||||
|
'stabile_id' => null,
|
||||||
|
'unita_immobiliare_id' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'etichetta' => $tipo === 'pec' ? 'PEC Google' : 'Email Google',
|
||||||
|
'is_principale' => $index === 0,
|
||||||
|
'meta' => ['source' => 'google_workspace'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$phoneRows = (array) Arr::get($person, 'phoneNumbers', []);
|
||||||
|
foreach ($phoneRows as $index => $phoneRow) {
|
||||||
|
$raw = trim((string) Arr::get($phoneRow, 'value', ''));
|
||||||
|
if ($raw === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$digits = PhoneNumber::normalizeDigits($raw);
|
||||||
|
if ($digits === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tipo = str_starts_with($digits, '3') || str_starts_with($digits, '39') ? 'cellulare' : 'telefono';
|
||||||
|
$value = $tipo === 'cellulare' ? (PhoneNumber::toE164Italy($raw) ?? $raw) : $raw;
|
||||||
|
|
||||||
|
RubricaContattoCanale::query()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'rubrica_id' => (int) $rubrica->id,
|
||||||
|
'tipo' => $tipo,
|
||||||
|
'valore' => $value,
|
||||||
|
'stabile_id' => null,
|
||||||
|
'unita_immobiliare_id' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'etichetta' => $tipo === 'cellulare' ? 'Cellulare Google' : 'Telefono Google',
|
||||||
|
'is_principale' => $index === 0,
|
||||||
|
'meta' => ['source' => 'google_workspace'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class ImportGesconFullPipeline extends Command
|
||||||
{--solo= : Step singolo: unita|relazioni|soggetti|diritti|millesimi|voci|operazioni|rate|incassi|giroconti|straord|addebiti|detrazioni|tutto}
|
{--solo= : Step singolo: unita|relazioni|soggetti|diritti|millesimi|voci|operazioni|rate|incassi|giroconti|straord|addebiti|detrazioni|tutto}
|
||||||
{--no-views : Non ricrea le viste QA}
|
{--no-views : Non ricrea le viste QA}
|
||||||
{--force-mapping : Applica mapping anche sovrascrivendo valori esistenti}
|
{--force-mapping : Applica mapping anche sovrascrivendo valori esistenti}
|
||||||
|
{--fill-missing-millesimi : Crea righe mancanti a zero per ogni unita/tabella (uso eccezionale)}
|
||||||
{--amministratore-id= : Forza l\'assegnazione dello stabile a questo amministratore}
|
{--amministratore-id= : Forza l\'assegnazione dello stabile a questo amministratore}
|
||||||
';
|
';
|
||||||
|
|
||||||
|
|
@ -80,6 +81,70 @@ private function scopeCondominQuery($query)
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private array $stagingLatestLegacyYearCache = [];
|
||||||
|
|
||||||
|
private function resolveLatestStagingLegacyYear(string $table, ?string $stabile = null): ?string
|
||||||
|
{
|
||||||
|
$cacheKey = $table . '|' . ($stabile ?? '*');
|
||||||
|
if (array_key_exists($cacheKey, $this->stagingLatestLegacyYearCache)) {
|
||||||
|
return $this->stagingLatestLegacyYearCache[$cacheKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::connection('gescon_import')->hasTable($table)) {
|
||||||
|
return $this->stagingLatestLegacyYearCache[$cacheKey] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::connection('gescon_import')->hasColumn($table, 'legacy_year')) {
|
||||||
|
return $this->stagingLatestLegacyYearCache[$cacheKey] = $this->requestedLegacyYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = DB::connection('gescon_import')->table($table)
|
||||||
|
->whereNotNull('legacy_year')
|
||||||
|
->where('legacy_year', '!=', '');
|
||||||
|
|
||||||
|
if ($stabile !== null && $stabile !== '' && Schema::connection('gescon_import')->hasColumn($table, 'cod_stabile')) {
|
||||||
|
$query->where('cod_stabile', $stabile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $query->orderByDesc('legacy_year')->value('legacy_year');
|
||||||
|
|
||||||
|
return $this->stagingLatestLegacyYearCache[$cacheKey] = ($value !== null ? trim((string) $value) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentSnapshotCondominQuery()
|
||||||
|
{
|
||||||
|
$query = DB::connection('gescon_import')->table('condomin');
|
||||||
|
if ($this->option('stabile')) {
|
||||||
|
$query->where('cod_stabile', $this->option('stabile'));
|
||||||
|
}
|
||||||
|
if ($this->option('scala')) {
|
||||||
|
$query->where('scala', $this->option('scala'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestYear = $this->resolveLatestStagingLegacyYear('condomin', (string) ($this->option('stabile') ?? ''));
|
||||||
|
if ($latestYear !== null && Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_year')) {
|
||||||
|
$query->where('legacy_year', $latestYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentSnapshotComproprietariQuery()
|
||||||
|
{
|
||||||
|
$query = DB::connection('gescon_import')->table('comproprietari');
|
||||||
|
if ($this->option('stabile')) {
|
||||||
|
$query->where('cod_stabile', $this->option('stabile'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestYear = $this->resolveLatestStagingLegacyYear('comproprietari', (string) ($this->option('stabile') ?? ''))
|
||||||
|
?: $this->resolveLatestStagingLegacyYear('condomin', (string) ($this->option('stabile') ?? ''));
|
||||||
|
if ($latestYear !== null && Schema::connection('gescon_import')->hasColumn('comproprietari', 'legacy_year')) {
|
||||||
|
$query->where('legacy_year', $latestYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
// Mapping campi configurato via UI (per-utente/per-stabile). Caricato on-demand.
|
// Mapping campi configurato via UI (per-utente/per-stabile). Caricato on-demand.
|
||||||
private const BOOLEAN_TARGET_KEYS = [
|
private const BOOLEAN_TARGET_KEYS = [
|
||||||
'stabile.ascensore',
|
'stabile.ascensore',
|
||||||
|
|
@ -526,13 +591,7 @@ private function stepUnita(?int $limit): int
|
||||||
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
|
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
|
||||||
$offset = 0;
|
$offset = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
$q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'));
|
$q = $this->currentSnapshotCondominQuery();
|
||||||
if ($this->option('stabile')) {
|
|
||||||
$q->where('cod_stabile', $this->option('stabile'));
|
|
||||||
}
|
|
||||||
if ($this->option('scala')) {
|
|
||||||
$q->where('scala', $this->option('scala'));
|
|
||||||
}
|
|
||||||
$q->limit($batch)->offset($offset);
|
$q->limit($batch)->offset($offset);
|
||||||
$rows = $q->get();
|
$rows = $q->get();
|
||||||
if ($rows->isEmpty()) {
|
if ($rows->isEmpty()) {
|
||||||
|
|
@ -577,8 +636,14 @@ private function stepUnita(?int $limit): int
|
||||||
} else {
|
} else {
|
||||||
$rowStabileId = $this->stabileId;
|
$rowStabileId = $this->stabileId;
|
||||||
}
|
}
|
||||||
$interno = $r->interno ?? null;
|
$interno = isset($r->interno) ? trim((string) $r->interno) : null;
|
||||||
$sub = $r->sub ?? $interno;
|
if ($interno === '') {
|
||||||
|
$interno = null;
|
||||||
|
}
|
||||||
|
$sub = isset($r->catasto_sub) ? trim((string) $r->catasto_sub) : (isset($r->sub) ? trim((string) $r->sub) : $interno);
|
||||||
|
if ($sub === '') {
|
||||||
|
$sub = null;
|
||||||
|
}
|
||||||
// Riconosci pertinenze nel campo INT o note: cantina, box/garage, posto auto, soffitta/solaio, locale tecnico
|
// Riconosci pertinenze nel campo INT o note: cantina, box/garage, posto auto, soffitta/solaio, locale tecnico
|
||||||
$internoStr = is_string($interno) ? trim($interno) : (string) $interno;
|
$internoStr = is_string($interno) ? trim($interno) : (string) $interno;
|
||||||
$ptype = null;
|
$ptype = null;
|
||||||
|
|
@ -614,12 +679,37 @@ private function stepUnita(?int $limit): int
|
||||||
$pianoVal = (int) $pv;
|
$pianoVal = (int) $pv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$normalizedInterno = $ptype && $pcode ? $pcode : $interno;
|
||||||
|
if ($normalizedInterno !== null) {
|
||||||
|
$normalizedInterno = trim((string) $normalizedInterno);
|
||||||
|
if ($normalizedInterno === '' || (! $ptype && preg_match('/^0+$/', $normalizedInterno))) {
|
||||||
|
$normalizedInterno = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
if ($hasDetailedSchema) {
|
if ($hasDetailedSchema) {
|
||||||
// Codice unita coerente: STAB-SCALA-INT (per cantine: STAB-SCALA-CAN<n>)
|
$exists = null;
|
||||||
$intPart = $ptype && $pcode ? $pcode : (($interno ?? null) ?: '0');
|
if ($legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
|
||||||
|
$exists = DB::table('unita_immobiliari')
|
||||||
|
->where('stabile_id', $rowStabileId)
|
||||||
|
->where('legacy_cond_id', $legacyCondId)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($normalizedInterno === null && ! $exists) {
|
||||||
|
$this->warn(sprintf(
|
||||||
|
'Unita saltata per stabile %s cod_cond %s: interno mancante nello snapshot corrente autorevole.',
|
||||||
|
(string) ($r->cod_stabile ?? $this->option('stabile') ?? '-'),
|
||||||
|
$legacyCondId !== null && $legacyCondId !== '' ? $legacyCondId : '-'
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codice unita coerente: STAB-SCALA-INT (per pertinenze: STAB-SCALA-CAN<n>/BOX<n>/...)
|
||||||
|
$intPart = $normalizedInterno;
|
||||||
$codiceBaseStab = ($r->cod_stabile ?? $this->option('stabile')) ?: 'S0';
|
$codiceBaseStab = ($r->cod_stabile ?? $this->option('stabile')) ?: 'S0';
|
||||||
// Se stabili-per-scala, includi comunque la scala nel codice per leggibilità
|
$codice = $intPart !== null
|
||||||
$codice = $codiceBaseStab . '-' . ($scalaVal ?: 'A') . '-' . $intPart;
|
? $this->buildLegacyUnitCode($codiceBaseStab, $scalaVal ?: 'A', (string) $intPart, $legacyCondId)
|
||||||
|
: (string) ($exists->codice_unita ?? '');
|
||||||
// Filtri post-normalizzazione: pertinenze e piano min/max
|
// Filtri post-normalizzazione: pertinenze e piano min/max
|
||||||
if ($this->option('only-pertinenze')) {
|
if ($this->option('only-pertinenze')) {
|
||||||
$opt = strtolower((string) $this->option('only-pertinenze'));
|
$opt = strtolower((string) $this->option('only-pertinenze'));
|
||||||
|
|
@ -637,18 +727,11 @@ private function stepUnita(?int $limit): int
|
||||||
if ($pMax !== null && $pianoVal > $pMax) {
|
if ($pMax !== null && $pianoVal > $pMax) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$exists = null;
|
if (! $exists && $codice !== '') {
|
||||||
if ($legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
|
|
||||||
$exists = DB::table('unita_immobiliari')
|
|
||||||
->where('stabile_id', $rowStabileId)
|
|
||||||
->where('legacy_cond_id', $legacyCondId)
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
if (! $exists) {
|
|
||||||
$exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first();
|
$exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first();
|
||||||
if ($exists && $legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
|
if ($exists && $legacyCondId !== null && $legacyCondId !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
|
||||||
if (! empty($exists->legacy_cond_id) && (string) $exists->legacy_cond_id !== (string) $legacyCondId) {
|
if (! empty($exists->legacy_cond_id) && (string) $exists->legacy_cond_id !== (string) $legacyCondId) {
|
||||||
$codice = $codice . '-C' . $legacyCondId;
|
$codice = $this->buildLegacyUnitCode($codiceBaseStab, $scalaVal ?: 'A', (string) $intPart, 'C' . $legacyCondId);
|
||||||
$exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first();
|
$exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -661,6 +744,39 @@ private function stepUnita(?int $limit): int
|
||||||
$patch['legacy_cond_id'] = $legacyCondId;
|
$patch['legacy_cond_id'] = $legacyCondId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ($codice !== '' && Schema::hasColumn('unita_immobiliari', 'codice_unita') && (string) ($exists->codice_unita ?? '') !== $codice) {
|
||||||
|
$codeOwner = DB::table('unita_immobiliari')
|
||||||
|
->where('codice_unita', $codice)
|
||||||
|
->where('id', '!=', $exists->id)
|
||||||
|
->first();
|
||||||
|
if (! $codeOwner) {
|
||||||
|
$patch['codice_unita'] = $codice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($normalizedInterno !== null && Schema::hasColumn('unita_immobiliari', 'interno') && (string) ($exists->interno ?? '') !== $normalizedInterno) {
|
||||||
|
$patch['interno'] = $normalizedInterno;
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('unita_immobiliari', 'subalterno') && (string) ($exists->subalterno ?? '') !== (string) ($sub ?? '')) {
|
||||||
|
$patch['subalterno'] = $sub;
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('unita_immobiliari', 'piano') && (int) ($exists->piano ?? 0) !== $pianoVal) {
|
||||||
|
$patch['piano'] = $pianoVal;
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('unita_immobiliari', 'denominazione')) {
|
||||||
|
$denominazione = trim(($r->cognome ?? '') . ' ' . ($r->nome ?? '')) ?: null;
|
||||||
|
if ($denominazione !== null && (string) ($exists->denominazione ?? '') !== $denominazione) {
|
||||||
|
$patch['denominazione'] = $denominazione;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('unita_immobiliari', 'tipo_unita') && (string) ($exists->tipo_unita ?? '') !== ($ptype ?: 'abitazione')) {
|
||||||
|
$patch['tipo_unita'] = $ptype ?: 'abitazione';
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('unita_immobiliari', 'attiva') && ! (bool) ($exists->attiva ?? false)) {
|
||||||
|
$patch['attiva'] = true;
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('unita_immobiliari', 'deleted_at') && ! empty($exists->deleted_at)) {
|
||||||
|
$patch['deleted_at'] = null;
|
||||||
|
}
|
||||||
if ($this->splitMode === 'palazzine') {
|
if ($this->splitMode === 'palazzine') {
|
||||||
if (Schema::hasColumn('unita_immobiliari', 'palazzina')) {
|
if (Schema::hasColumn('unita_immobiliari', 'palazzina')) {
|
||||||
if (empty($exists->palazzina) || $exists->palazzina !== $scalaVal) {
|
if (empty($exists->palazzina) || $exists->palazzina !== $scalaVal) {
|
||||||
|
|
@ -692,8 +808,8 @@ private function stepUnita(?int $limit): int
|
||||||
'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null,
|
'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null,
|
||||||
'scala' => $scalaVal,
|
'scala' => $scalaVal,
|
||||||
'piano' => $pianoVal,
|
'piano' => $pianoVal,
|
||||||
'interno' => (string) ($ptype && $pcode ? $pcode : $interno),
|
'interno' => $normalizedInterno,
|
||||||
'subalterno' => (string) $sub,
|
'subalterno' => $sub,
|
||||||
'categoria_catastale' => null,
|
'categoria_catastale' => null,
|
||||||
'classe' => null,
|
'classe' => null,
|
||||||
'consistenza' => null,
|
'consistenza' => null,
|
||||||
|
|
@ -788,8 +904,7 @@ private function stepUnita(?int $limit): int
|
||||||
|
|
||||||
// Cleanup: rimuovi unità non presenti in condomin (evita "alieni")
|
// Cleanup: rimuovi unità non presenti in condomin (evita "alieni")
|
||||||
if (! $this->isDryRun && $this->option('stabile')) {
|
if (! $this->isDryRun && $this->option('stabile')) {
|
||||||
$condRows = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'))
|
$condRows = $this->currentSnapshotCondominQuery()
|
||||||
->where('cod_stabile', $this->option('stabile'))
|
|
||||||
->select('scala', 'interno', 'cod_cond')
|
->select('scala', 'interno', 'cod_cond')
|
||||||
->get();
|
->get();
|
||||||
$condSet = [];
|
$condSet = [];
|
||||||
|
|
@ -920,13 +1035,7 @@ private function stepSoggetti(?int $limit): int
|
||||||
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
|
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
|
||||||
$offset = 0;
|
$offset = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
$q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'));
|
$q = $this->currentSnapshotCondominQuery();
|
||||||
if ($this->option('stabile')) {
|
|
||||||
$q->where('cod_stabile', $this->option('stabile'));
|
|
||||||
}
|
|
||||||
if ($this->option('scala')) {
|
|
||||||
$q->where('scala', $this->option('scala'));
|
|
||||||
}
|
|
||||||
$q->limit($batch)->offset($offset);
|
$q->limit($batch)->offset($offset);
|
||||||
$rows = $q->get();
|
$rows = $q->get();
|
||||||
if ($rows->isEmpty()) {
|
if ($rows->isEmpty()) {
|
||||||
|
|
@ -1050,13 +1159,7 @@ private function stepDiritti(?int $limit): int
|
||||||
return $value ?? '';
|
return $value ?? '';
|
||||||
};
|
};
|
||||||
while (true) {
|
while (true) {
|
||||||
$q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'));
|
$q = $this->currentSnapshotCondominQuery();
|
||||||
if ($this->option('stabile')) {
|
|
||||||
$q->where('cod_stabile', $this->option('stabile'));
|
|
||||||
}
|
|
||||||
if ($this->option('scala')) {
|
|
||||||
$q->where('scala', $this->option('scala'));
|
|
||||||
}
|
|
||||||
$q->limit($batch)->offset($offset);
|
$q->limit($batch)->offset($offset);
|
||||||
$rows = $q->get();
|
$rows = $q->get();
|
||||||
if ($rows->isEmpty()) {
|
if ($rows->isEmpty()) {
|
||||||
|
|
@ -1262,10 +1365,7 @@ private function stepDiritti(?int $limit): int
|
||||||
|
|
||||||
// Import comproprietari (se presenti)
|
// Import comproprietari (se presenti)
|
||||||
if (Schema::connection('gescon_import')->hasTable('comproprietari')) {
|
if (Schema::connection('gescon_import')->hasTable('comproprietari')) {
|
||||||
$q = DB::connection('gescon_import')->table('comproprietari');
|
$q = $this->currentSnapshotComproprietariQuery();
|
||||||
if ($this->option('stabile')) {
|
|
||||||
$q->where('cod_stabile', $this->option('stabile'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$comprRows = $q->limit(50000)->get();
|
$comprRows = $q->limit(50000)->get();
|
||||||
foreach ($comprRows as $cr) {
|
foreach ($comprRows as $cr) {
|
||||||
|
|
@ -1970,8 +2070,10 @@ private function stepMillesimi(?int $limit): int
|
||||||
}
|
}
|
||||||
$affected = $detailsInserted + $detailsUpdated;
|
$affected = $detailsInserted + $detailsUpdated;
|
||||||
|
|
||||||
// 4) Completa righe mancanti: ogni unità deve avere una riga per ogni tabella
|
// 4) Completa righe mancanti solo se richiesto esplicitamente.
|
||||||
if ($detailsTable === 'dettaglio_millesimi' && Schema::hasTable('unita_immobiliari')) {
|
// Di default evitiamo placeholder a zero che spezzano il collegamento reale
|
||||||
|
// tra millesimi, nominativi e unita immobiliari.
|
||||||
|
if ($this->option('fill-missing-millesimi') && $detailsTable === 'dettaglio_millesimi' && Schema::hasTable('unita_immobiliari')) {
|
||||||
$detailCols = Schema::getColumnListing('dettaglio_millesimi');
|
$detailCols = Schema::getColumnListing('dettaglio_millesimi');
|
||||||
$unitaIds = DB::table('unita_immobiliari')
|
$unitaIds = DB::table('unita_immobiliari')
|
||||||
->where('stabile_id', $this->stabileId)
|
->where('stabile_id', $this->stabileId)
|
||||||
|
|
@ -2166,6 +2268,19 @@ private function stepVoci(?int $limit): int
|
||||||
$tabellaQuery->where('anno_gestione', $annoGestione);
|
$tabellaQuery->where('anno_gestione', $annoGestione);
|
||||||
}
|
}
|
||||||
$tabellaId = $tabellaQuery->value('id');
|
$tabellaId = $tabellaQuery->value('id');
|
||||||
|
if (! $tabellaId && $annoGestione && Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
|
||||||
|
$tabellaId = DB::table('tabelle_millesimali')
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->where(function ($q) use ($v) {
|
||||||
|
if (Schema::hasColumn('tabelle_millesimali', 'codice_tabella')) {
|
||||||
|
$q->orWhere('codice_tabella', $v->tabella);
|
||||||
|
}
|
||||||
|
if (Schema::hasColumn('tabelle_millesimali', 'nome_tabella')) {
|
||||||
|
$q->orWhere('nome_tabella', $v->tabella);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->value('id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, $numeroStraordinaria);
|
$gestioneId = $this->resolveGestioneContabileIdForStabile($stabileId, $legacyYear, $tipoLegacy, $numeroStraordinaria);
|
||||||
$data = [
|
$data = [
|
||||||
|
|
@ -3763,6 +3878,8 @@ private function preflightStaging(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
$legacyYearOpt = $this->option('anno') ? sprintf('%04d', (int) $this->option('anno')) : null;
|
$legacyYearOpt = $this->option('anno') ? sprintf('%04d', (int) $this->option('anno')) : null;
|
||||||
|
$authoritativeCondomin = $this->resolveCondominMdbForStabile($stab, null);
|
||||||
|
$authoritativeLegacyYear = (string) ($authoritativeCondomin['legacy_year'] ?? '');
|
||||||
|
|
||||||
// Autoderiva --mdb-condomin se non fornito, preferendo l'anno richiesto e poi l'ultima annualità disponibile.
|
// Autoderiva --mdb-condomin se non fornito, preferendo l'anno richiesto e poi l'ultima annualità disponibile.
|
||||||
if (! $this->option('mdb-condomin')) {
|
if (! $this->option('mdb-condomin')) {
|
||||||
|
|
@ -3783,12 +3900,10 @@ private function preflightStaging(): void
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($legacyYearOpt === null) {
|
if (! empty($resolved['legacy_year'])) {
|
||||||
if (! empty($resolved['anno_o'])) {
|
|
||||||
$legacyYearOpt = (string) $resolved['anno_o'];
|
|
||||||
} elseif (! empty($resolved['legacy_year'])) {
|
|
||||||
$legacyYearOpt = (string) $resolved['legacy_year'];
|
$legacyYearOpt = (string) $resolved['legacy_year'];
|
||||||
}
|
} elseif ($legacyYearOpt === null && ! empty($resolved['anno_o'])) {
|
||||||
|
$legacyYearOpt = (string) $resolved['anno_o'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3798,7 +3913,7 @@ private function preflightStaging(): void
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$needsLoad = $this->stagingTableNeedsLoadForStabile('condomin', $stab);
|
$needsLoad = $this->stagingTableNeedsLoadForStabile('condomin', $stab, $legacyYearOpt);
|
||||||
if ($needsLoad) {
|
if ($needsLoad) {
|
||||||
$mdbCondomin = $this->option('mdb-condomin');
|
$mdbCondomin = $this->option('mdb-condomin');
|
||||||
if ($mdbCondomin && is_file($mdbCondomin)) {
|
if ($mdbCondomin && is_file($mdbCondomin)) {
|
||||||
|
|
@ -3821,6 +3936,26 @@ private function preflightStaging(): void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($authoritativeLegacyYear !== '' && $authoritativeLegacyYear !== $legacyYearOpt) {
|
||||||
|
$authoritativePath = (string) ($authoritativeCondomin['path'] ?? '');
|
||||||
|
if ($authoritativePath !== '' && is_file($authoritativePath)) {
|
||||||
|
foreach (['condomin', 'comproprietari'] as $table) {
|
||||||
|
if (! Schema::connection('gescon_import')->hasTable($table)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (! $this->stagingTableNeedsLoadForStabile($table, $stab, $authoritativeLegacyYear)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->callSilently('gescon:load-mdb', [
|
||||||
|
'--mdb' => $authoritativePath,
|
||||||
|
'--table' => $table,
|
||||||
|
'--stabile' => $stab,
|
||||||
|
'--legacy-year' => $authoritativeLegacyYear,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Nota: partizionamento logico di condomin (unità, proprietari, inquilini) avviene negli step senza creare VIEW.
|
// Nota: partizionamento logico di condomin (unità, proprietari, inquilini) avviene negli step senza creare VIEW.
|
||||||
|
|
||||||
// Carica tabelle aggiuntive utili (millesimi, voci) se vuote per lo stabile
|
// Carica tabelle aggiuntive utili (millesimi, voci) se vuote per lo stabile
|
||||||
|
|
@ -3828,7 +3963,7 @@ private function preflightStaging(): void
|
||||||
if ($mdbCondomin && is_file($mdbCondomin)) {
|
if ($mdbCondomin && is_file($mdbCondomin)) {
|
||||||
// dett_tab
|
// dett_tab
|
||||||
if (Schema::connection('gescon_import')->hasTable('dett_tab')) {
|
if (Schema::connection('gescon_import')->hasTable('dett_tab')) {
|
||||||
$need = $this->stagingTableNeedsLoadForStabile('dett_tab', $stab);
|
$need = $this->stagingTableNeedsLoadForStabile('dett_tab', $stab, $legacyYearOpt);
|
||||||
if ($need) {
|
if ($need) {
|
||||||
$this->callSilently('gescon:load-mdb', [
|
$this->callSilently('gescon:load-mdb', [
|
||||||
'--mdb' => $mdbCondomin,
|
'--mdb' => $mdbCondomin,
|
||||||
|
|
@ -3840,7 +3975,7 @@ private function preflightStaging(): void
|
||||||
}
|
}
|
||||||
// tabelle_millesimali
|
// tabelle_millesimali
|
||||||
if (Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
|
if (Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
|
||||||
$need = $this->stagingTableNeedsLoadForStabile('tabelle_millesimali', $stab);
|
$need = $this->stagingTableNeedsLoadForStabile('tabelle_millesimali', $stab, $legacyYearOpt);
|
||||||
if ($need) {
|
if ($need) {
|
||||||
$this->callSilently('gescon:load-mdb', [
|
$this->callSilently('gescon:load-mdb', [
|
||||||
'--mdb' => $mdbCondomin,
|
'--mdb' => $mdbCondomin,
|
||||||
|
|
@ -3852,7 +3987,7 @@ private function preflightStaging(): void
|
||||||
}
|
}
|
||||||
// voc_spe (voci spesa)
|
// voc_spe (voci spesa)
|
||||||
if (Schema::connection('gescon_import')->hasTable('voc_spe')) {
|
if (Schema::connection('gescon_import')->hasTable('voc_spe')) {
|
||||||
$need = $this->stagingTableNeedsLoadForStabile('voc_spe', $stab);
|
$need = $this->stagingTableNeedsLoadForStabile('voc_spe', $stab, $legacyYearOpt);
|
||||||
if ($need) {
|
if ($need) {
|
||||||
$this->callSilently('gescon:load-mdb', [
|
$this->callSilently('gescon:load-mdb', [
|
||||||
'--mdb' => $mdbCondomin,
|
'--mdb' => $mdbCondomin,
|
||||||
|
|
@ -3864,7 +3999,7 @@ private function preflightStaging(): void
|
||||||
}
|
}
|
||||||
// comproprietari
|
// comproprietari
|
||||||
if (Schema::connection('gescon_import')->hasTable('comproprietari')) {
|
if (Schema::connection('gescon_import')->hasTable('comproprietari')) {
|
||||||
$need = $this->stagingTableNeedsLoadForStabile('comproprietari', $stab);
|
$need = $this->stagingTableNeedsLoadForStabile('comproprietari', $stab, $legacyYearOpt);
|
||||||
if ($need) {
|
if ($need) {
|
||||||
$this->callSilently('gescon:load-mdb', [
|
$this->callSilently('gescon:load-mdb', [
|
||||||
'--mdb' => $mdbCondomin,
|
'--mdb' => $mdbCondomin,
|
||||||
|
|
@ -3876,7 +4011,7 @@ private function preflightStaging(): void
|
||||||
}
|
}
|
||||||
// Cre_Deb_preced (crediti/debiti pregressi)
|
// Cre_Deb_preced (crediti/debiti pregressi)
|
||||||
if (Schema::connection('gescon_import')->hasTable('cre_deb_preced')) {
|
if (Schema::connection('gescon_import')->hasTable('cre_deb_preced')) {
|
||||||
$need = $this->stagingTableNeedsLoadForStabile('cre_deb_preced', $stab);
|
$need = $this->stagingTableNeedsLoadForStabile('cre_deb_preced', $stab, $legacyYearOpt);
|
||||||
if ($need) {
|
if ($need) {
|
||||||
$this->callSilently('gescon:load-mdb', [
|
$this->callSilently('gescon:load-mdb', [
|
||||||
'--mdb' => $mdbCondomin,
|
'--mdb' => $mdbCondomin,
|
||||||
|
|
@ -3898,7 +4033,7 @@ private function preflightStaging(): void
|
||||||
];
|
];
|
||||||
foreach ($more as $tb) {
|
foreach ($more as $tb) {
|
||||||
if (Schema::connection('gescon_import')->hasTable($tb)) {
|
if (Schema::connection('gescon_import')->hasTable($tb)) {
|
||||||
$need = $this->stagingTableNeedsLoadForStabile($tb, $stab);
|
$need = $this->stagingTableNeedsLoadForStabile($tb, $stab, $legacyYearOpt);
|
||||||
if ($need) {
|
if ($need) {
|
||||||
$this->callSilently('gescon:load-mdb', [
|
$this->callSilently('gescon:load-mdb', [
|
||||||
'--mdb' => $mdbCondomin,
|
'--mdb' => $mdbCondomin,
|
||||||
|
|
@ -4021,7 +4156,7 @@ private function resolveCondominMdbForStabile(string $stab, ?string $legacyYear
|
||||||
return Arr::except($candidates[0], ['sort_key']);
|
return Arr::except($candidates[0], ['sort_key']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function stagingTableNeedsLoadForStabile(string $table, string $stab): bool
|
private function stagingTableNeedsLoadForStabile(string $table, string $stab, ?string $legacyYear = null): bool
|
||||||
{
|
{
|
||||||
if (! Schema::connection('gescon_import')->hasTable($table)) {
|
if (! Schema::connection('gescon_import')->hasTable($table)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -4029,7 +4164,12 @@ private function stagingTableNeedsLoadForStabile(string $table, string $stab): b
|
||||||
|
|
||||||
$conn = DB::connection('gescon_import');
|
$conn = DB::connection('gescon_import');
|
||||||
if (Schema::connection('gescon_import')->hasColumn($table, 'cod_stabile')) {
|
if (Schema::connection('gescon_import')->hasColumn($table, 'cod_stabile')) {
|
||||||
return ! $conn->table($table)->where('cod_stabile', $stab)->exists();
|
$query = $conn->table($table)->where('cod_stabile', $stab);
|
||||||
|
if ($legacyYear !== null && $legacyYear !== '' && Schema::connection('gescon_import')->hasColumn($table, 'legacy_year')) {
|
||||||
|
$query->where('legacy_year', $legacyYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! $query->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $conn->table($table)->count() === 0;
|
return $conn->table($table)->count() === 0;
|
||||||
|
|
@ -4243,9 +4383,24 @@ private function findUnitaByStagingRow($r)
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$stabileId = $this->stabileId;
|
||||||
|
if ($stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
|
||||||
|
$legacyCond = trim((string) ($r->cod_cond ?? $r->legacy_cond_id ?? $r->id_cond ?? ''));
|
||||||
|
if ($legacyCond !== '' && Schema::hasColumn('unita_immobiliari', 'legacy_cond_id')) {
|
||||||
|
$match = DB::table('unita_immobiliari')
|
||||||
|
->where('stabile_id', $stabileId)
|
||||||
|
->when(Schema::hasColumn('unita_immobiliari', 'deleted_at'), fn($q) => $q->whereNull('deleted_at'))
|
||||||
|
->where('legacy_cond_id', $legacyCond)
|
||||||
|
->first();
|
||||||
|
if ($match) {
|
||||||
|
return $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$crit = [];
|
$crit = [];
|
||||||
if ($this->stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
|
if ($stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
|
||||||
$crit['stabile_id'] = $this->stabileId;
|
$crit['stabile_id'] = $stabileId;
|
||||||
}
|
}
|
||||||
if (Schema::hasColumn('unita_immobiliari', 'scala') && isset($r->scala)) {
|
if (Schema::hasColumn('unita_immobiliari', 'scala') && isset($r->scala)) {
|
||||||
$crit['scala'] = $r->scala;
|
$crit['scala'] = $r->scala;
|
||||||
|
|
@ -4255,15 +4410,18 @@ private function findUnitaByStagingRow($r)
|
||||||
if (is_string($int) && preg_match('/^\s*CAN\s*(\d+)\s*$/i', $int, $m)) {
|
if (is_string($int) && preg_match('/^\s*CAN\s*(\d+)\s*$/i', $int, $m)) {
|
||||||
$int = 'CAN' . $m[1];
|
$int = 'CAN' . $m[1];
|
||||||
}
|
}
|
||||||
if (($int === null || trim($int) === '') && isset($r->cod_cond)) {
|
|
||||||
$int = (string) $r->cod_cond;
|
|
||||||
}
|
|
||||||
if ($int !== null) {
|
if ($int !== null) {
|
||||||
|
$int = trim($int);
|
||||||
|
}
|
||||||
|
if ($int !== null && $int !== '' && ! preg_match('/^0+$/', $int)) {
|
||||||
$crit['interno'] = $int;
|
$crit['interno'] = $int;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (! empty($crit)) {
|
if (! empty($crit)) {
|
||||||
return DB::table('unita_immobiliari')->where($crit)->first();
|
return DB::table('unita_immobiliari')
|
||||||
|
->where($crit)
|
||||||
|
->when(Schema::hasColumn('unita_immobiliari', 'deleted_at'), fn($q) => $q->whereNull('deleted_at'))
|
||||||
|
->first();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -4719,6 +4877,35 @@ private function generateStableCondKey(string $prefix, $stabile, $cond, int $len
|
||||||
return $prefix . $digest;
|
return $prefix . $digest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildLegacyUnitCode(string $stabileCode, string $scala, string $interno, ?string $salt = null): string
|
||||||
|
{
|
||||||
|
$maxLen = 20;
|
||||||
|
|
||||||
|
$stablePart = $this->sanitizeUnitCodeSegment($stabileCode, 4) ?: 'S0';
|
||||||
|
$scalaPart = $this->sanitizeUnitCodeSegment($scala, 4) ?: 'A';
|
||||||
|
$internoPart = $this->sanitizeUnitCodeSegment($interno, 32) ?: 'INT';
|
||||||
|
|
||||||
|
$base = $stablePart . '-' . $scalaPart . '-' . $internoPart;
|
||||||
|
if (strlen($base) <= $maxLen) {
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = strtoupper(substr(hash('crc32b', implode('|', [$stabileCode, $scala, $interno, (string) $salt])), 0, 4));
|
||||||
|
$reserved = strlen($stablePart) + strlen($scalaPart) + strlen($hash) + 3;
|
||||||
|
$availableInterno = max(1, $maxLen - $reserved);
|
||||||
|
$internoCompact = substr($internoPart, 0, $availableInterno);
|
||||||
|
|
||||||
|
return $stablePart . '-' . $scalaPart . '-' . $internoCompact . '-' . $hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeUnitCodeSegment(?string $value, int $maxLen): string
|
||||||
|
{
|
||||||
|
$normalized = strtoupper(trim((string) $value));
|
||||||
|
$normalized = preg_replace('/[^A-Z0-9]+/u', '', $normalized) ?? '';
|
||||||
|
|
||||||
|
return substr($normalized, 0, max(1, $maxLen));
|
||||||
|
}
|
||||||
|
|
||||||
private function nextProtocolNumber(): int
|
private function nextProtocolNumber(): int
|
||||||
{
|
{
|
||||||
return ++$this->protocolCounter;
|
return ++$this->protocolCounter;
|
||||||
|
|
@ -5111,13 +5298,16 @@ private function buildComproprietariHistoryPayloads(array $historyRows, ?string
|
||||||
|
|
||||||
$currentNameKey = $this->normalizeLegacyPartyName($current['nominativo'] ?? null);
|
$currentNameKey = $this->normalizeLegacyPartyName($current['nominativo'] ?? null);
|
||||||
$currentRightKey = $this->normalizeLegacyPartyName($current['diritto_reale'] ?? $current['diritto_label'] ?? null);
|
$currentRightKey = $this->normalizeLegacyPartyName($current['diritto_reale'] ?? $current['diritto_label'] ?? null);
|
||||||
|
$sameSeries = ! empty($snapshot['id_compr']) && ! empty($current['id_compr'])
|
||||||
|
&& (int) $snapshot['id_compr'] === (int) $current['id_compr'];
|
||||||
$sameName = $snapshotNameKey !== '' && $snapshotNameKey === $currentNameKey;
|
$sameName = $snapshotNameKey !== '' && $snapshotNameKey === $currentNameKey;
|
||||||
$sameRight = $snapshotRightKey === $currentRightKey;
|
$sameRight = $snapshotRightKey === $currentRightKey;
|
||||||
$samePercent = $snapshot['percentuale'] === null || $current['percentuale'] === null
|
$samePercent = $snapshot['percentuale'] === null || $current['percentuale'] === null
|
||||||
|| abs((float) $snapshot['percentuale'] - (float) $current['percentuale']) < 0.0005;
|
|| abs((float) $snapshot['percentuale'] - (float) $current['percentuale']) < 0.0005;
|
||||||
|
|
||||||
if ($sameName && $sameRight && $samePercent) {
|
if (($sameName && $sameRight && $samePercent) || ($sameSeries && $sameRight)) {
|
||||||
$current['data_inizio'] = $snapshotStart ?? $current['data_inizio'];
|
$current['data_inizio'] = $snapshotStart ?? $current['data_inizio'];
|
||||||
|
$current['percentuale'] = $current['percentuale'] ?? $snapshot['percentuale'];
|
||||||
if (! empty($snapshot['legacy_year'])) {
|
if (! empty($snapshot['legacy_year'])) {
|
||||||
$current['legacy_years'][] = $snapshot['legacy_year'];
|
$current['legacy_years'][] = $snapshot['legacy_year'];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,11 @@ public function handle(): int
|
||||||
->when($stabileId > 0, fn($q) => $q->where('ui.stabile_id', $stabileId))
|
->when($stabileId > 0, fn($q) => $q->where('ui.stabile_id', $stabileId))
|
||||||
->count();
|
->count();
|
||||||
|
|
||||||
$sovrapposizioniPeriodi = Schema::hasTable('unita_immobiliare_nominativi')
|
$sameNormalizedNameSql = "UPPER(TRIM(REGEXP_REPLACE(COALESCE(a.nominativo, ''), '[[:space:]]+', ' '))) = UPPER(TRIM(REGEXP_REPLACE(COALESCE(b.nominativo, ''), '[[:space:]]+', ' ')))";
|
||||||
|
$sameComproprietarioSeriesSql = "a.fonte = 'legacy_comproprietari' AND b.fonte = 'legacy_comproprietari' AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(a.legacy_payload, '$.id_compr')), '') != '' AND COALESCE(JSON_UNQUOTE(JSON_EXTRACT(a.legacy_payload, '$.id_compr')), '') = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(b.legacy_payload, '$.id_compr')), '')";
|
||||||
|
$periodiSiSovrappongonoSql = "COALESCE(a.data_inizio, '1000-01-01') <= COALESCE(b.data_fine, '9999-12-31') AND COALESCE(b.data_inizio, '1000-01-01') <= COALESCE(a.data_fine, '9999-12-31')";
|
||||||
|
|
||||||
|
$duplicatiPeriodiStessoNominativo = Schema::hasTable('unita_immobiliare_nominativi')
|
||||||
? DB::table('unita_immobiliare_nominativi as a')
|
? DB::table('unita_immobiliare_nominativi as a')
|
||||||
->join('unita_immobiliare_nominativi as b', function ($join) : void {
|
->join('unita_immobiliare_nominativi as b', function ($join) : void {
|
||||||
$join->on('a.unita_immobiliare_id', '=', 'b.unita_immobiliare_id')
|
$join->on('a.unita_immobiliare_id', '=', 'b.unita_immobiliare_id')
|
||||||
|
|
@ -81,9 +85,27 @@ public function handle(): int
|
||||||
->whereColumn('a.id', '<', 'b.id');
|
->whereColumn('a.id', '<', 'b.id');
|
||||||
})
|
})
|
||||||
->join('unita_immobiliari as ui', 'ui.id', '=', 'a.unita_immobiliare_id')
|
->join('unita_immobiliari as ui', 'ui.id', '=', 'a.unita_immobiliare_id')
|
||||||
->where(function ($q): void {
|
->whereIn('a.fonte', ['legacy_condomin', 'legacy_comproprietari'])
|
||||||
$q->whereNull('a.data_fine')->orWhereNull('b.data_inizio')->orWhereColumn('a.data_fine', '>=', 'b.data_inizio');
|
->whereIn('b.fonte', ['legacy_condomin', 'legacy_comproprietari'])
|
||||||
|
->whereRaw($periodiSiSovrappongonoSql)
|
||||||
|
->whereRaw($sameNormalizedNameSql)
|
||||||
|
->whereRaw('NOT (' . $sameComproprietarioSeriesSql . ')')
|
||||||
|
->when($stabileId > 0, fn($q) => $q->where('ui.stabile_id', $stabileId))
|
||||||
|
->count()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
$compresenzeMultiIntestatario = Schema::hasTable('unita_immobiliare_nominativi')
|
||||||
|
? DB::table('unita_immobiliare_nominativi as a')
|
||||||
|
->join('unita_immobiliare_nominativi as b', function ($join) : void {
|
||||||
|
$join->on('a.unita_immobiliare_id', '=', 'b.unita_immobiliare_id')
|
||||||
|
->on('a.ruolo', '=', 'b.ruolo')
|
||||||
|
->whereColumn('a.id', '<', 'b.id');
|
||||||
})
|
})
|
||||||
|
->join('unita_immobiliari as ui', 'ui.id', '=', 'a.unita_immobiliare_id')
|
||||||
|
->whereIn('a.fonte', ['legacy_condomin', 'legacy_comproprietari'])
|
||||||
|
->whereIn('b.fonte', ['legacy_condomin', 'legacy_comproprietari'])
|
||||||
|
->whereRaw($periodiSiSovrappongonoSql)
|
||||||
|
->whereRaw('NOT (' . $sameNormalizedNameSql . ')')
|
||||||
->when($stabileId > 0, fn($q) => $q->where('ui.stabile_id', $stabileId))
|
->when($stabileId > 0, fn($q) => $q->where('ui.stabile_id', $stabileId))
|
||||||
->count()
|
->count()
|
||||||
: 0;
|
: 0;
|
||||||
|
|
@ -116,7 +138,8 @@ public function handle(): int
|
||||||
$this->line('Unita senza storico nominativi: ' . $unitaSenzaNominativi);
|
$this->line('Unita senza storico nominativi: ' . $unitaSenzaNominativi);
|
||||||
$this->line('Ruoli con stabile mismatch: ' . $mismatchRuoliStabile . ($applyFix ? ' (fix applicato)' : ''));
|
$this->line('Ruoli con stabile mismatch: ' . $mismatchRuoliStabile . ($applyFix ? ' (fix applicato)' : ''));
|
||||||
$this->line('Ruoli attivi senza cellulare: ' . $ruoliAttiviSenzaCell);
|
$this->line('Ruoli attivi senza cellulare: ' . $ruoliAttiviSenzaCell);
|
||||||
$this->line('Possibili sovrapposizioni periodi nominativi: ' . $sovrapposizioniPeriodi);
|
$this->line('Duplicati periodi stesso nominativo: ' . $duplicatiPeriodiStessoNominativo);
|
||||||
|
$this->line('Compresenze multi-intestatario contemporanee: ' . $compresenzeMultiIntestatario);
|
||||||
|
|
||||||
if (! empty($legacyYears)) {
|
if (! empty($legacyYears)) {
|
||||||
$this->line('Anni legacy disponibili (condomin):');
|
$this->line('Anni legacy disponibili (condomin):');
|
||||||
|
|
@ -125,7 +148,7 @@ public function handle(): int
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($unitaSenzaNominativi > 0 || $mismatchRuoliStabile > 0 || $sovrapposizioniPeriodi > 0) {
|
if ($unitaSenzaNominativi > 0 || $mismatchRuoliStabile > 0 || $duplicatiPeriodiStessoNominativo > 0) {
|
||||||
$this->warn('QA completato con anomalie da lavorare.');
|
$this->warn('QA completato con anomalie da lavorare.');
|
||||||
return self::INVALID;
|
return self::INVALID;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1102,7 +1102,7 @@ protected function getHeaderActions(): array
|
||||||
->send();
|
->send();
|
||||||
}),
|
}),
|
||||||
Action::make('contatti')
|
Action::make('contatti')
|
||||||
->label('Contatti')
|
->label('Modifica anagrafica')
|
||||||
->icon('heroicon-o-phone')
|
->icon('heroicon-o-phone')
|
||||||
->action(function (): void {
|
->action(function (): void {
|
||||||
$this->startInlineEdit();
|
$this->startInlineEdit();
|
||||||
|
|
@ -1158,7 +1158,7 @@ protected function getHeaderActions(): array
|
||||||
Action::make('contatti_avanzati')
|
Action::make('contatti_avanzati')
|
||||||
->label('Contatti avanzati')
|
->label('Contatti avanzati')
|
||||||
->icon('heroicon-o-rectangle-stack')
|
->icon('heroicon-o-rectangle-stack')
|
||||||
->visible(fn(): bool => Schema::hasTable('rubrica_contatto_canali'))
|
->visible(fn(): bool => Schema::hasTable('rubrica_contatti_canali'))
|
||||||
->modalWidth('7xl')
|
->modalWidth('7xl')
|
||||||
->form([
|
->form([
|
||||||
Repeater::make('canali')
|
Repeater::make('canali')
|
||||||
|
|
@ -2098,6 +2098,47 @@ protected function getTitoliOptions(): array
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
public function getTipoContattoOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'persona_fisica' => 'Persona fisica',
|
||||||
|
'persona_giuridica' => 'Persona giuridica',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
public function getCategoriaOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'condomino' => 'Condomino',
|
||||||
|
'inquilino' => 'Inquilino',
|
||||||
|
'fornitore' => 'Fornitore',
|
||||||
|
'collaboratore' => 'Collaboratore',
|
||||||
|
'amministratore' => 'Amministratore',
|
||||||
|
'azienda' => 'Azienda / ente',
|
||||||
|
'ufficio' => 'Ufficio / reparto',
|
||||||
|
'ente_pubblico' => 'Ente pubblico',
|
||||||
|
'banca' => 'Banca',
|
||||||
|
'assicurazione' => 'Assicurazione',
|
||||||
|
'altro' => 'Altro',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, string> */
|
||||||
|
public function getTipoUtenzaCallOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'condomino' => 'Condomino',
|
||||||
|
'inquilino' => 'Inquilino',
|
||||||
|
'fornitore' => 'Fornitore',
|
||||||
|
'collaboratore' => 'Collaboratore',
|
||||||
|
'amministratore' => 'Amministratore',
|
||||||
|
'ufficio' => 'Ufficio',
|
||||||
|
'altro' => 'Altro',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/** @return array<int, string> */
|
/** @return array<int, string> */
|
||||||
public function getUnitaOptions($stabileId): array
|
public function getUnitaOptions($stabileId): array
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
use App\Models\ChiamataPostIt;
|
use App\Models\ChiamataPostIt;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
|
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
|
@ -496,21 +497,7 @@ private function creaTicketDirettoDaForm(): void
|
||||||
|
|
||||||
private function findRubricaByPhone(string $value): ?RubricaUniversale
|
private function findRubricaByPhone(string $value): ?RubricaUniversale
|
||||||
{
|
{
|
||||||
$normalized = PhoneNumber::normalizeForMatch($value);
|
return app(RubricaPhoneLookupService::class)->findByPhone($value);
|
||||||
if ($normalized === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return RubricaUniversale::query()
|
|
||||||
->where(function ($q) use ($normalized): void {
|
|
||||||
$q->where('telefono_cellulare', $normalized)
|
|
||||||
->orWhere('telefono_ufficio', $normalized)
|
|
||||||
->orWhere('telefono_casa', $normalized)
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", ['%' . preg_replace('/\D+/', '', $normalized) . '%'])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", ['%' . preg_replace('/\D+/', '', $normalized) . '%'])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", ['%' . preg_replace('/\D+/', '', $normalized) . '%']);
|
|
||||||
})
|
|
||||||
->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createPostItRecord(): ChiamataPostIt
|
private function createPostItRecord(): ChiamataPostIt
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
use App\Models\Ticket;
|
use App\Models\Ticket;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
|
|
@ -65,9 +66,12 @@ class PostItGestione extends Page
|
||||||
/** @var array<string, array{id:int|null,name:string|null}> */
|
/** @var array<string, array{id:int|null,name:string|null}> */
|
||||||
private array $pbxExtensionCache = [];
|
private array $pbxExtensionCache = [];
|
||||||
|
|
||||||
/** @var array<int, CommunicationMessage|null> */
|
/** @var array<int|string, CommunicationMessage|null> */
|
||||||
private array $postItMessageCache = [];
|
private array $postItMessageCache = [];
|
||||||
|
|
||||||
|
/** @var array<int,string> */
|
||||||
|
private array $monitoredResponseGroups = ['601', '603'];
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$focus = (int) request()->query('focus_post_it', 0);
|
$focus = (int) request()->query('focus_post_it', 0);
|
||||||
|
|
@ -686,17 +690,17 @@ private function buildTicketDescriptionFromPostIt(int $postItId, ChiamataPostIt
|
||||||
|
|
||||||
private function buildGroupedPostItKey(ChiamataPostIt $postIt): string
|
private function buildGroupedPostItKey(ChiamataPostIt $postIt): string
|
||||||
{
|
{
|
||||||
$rubricaId = $this->resolveGroupedRubricaId($postIt);
|
|
||||||
if ($rubricaId !== null) {
|
|
||||||
return 'rubrica:' . $rubricaId;
|
|
||||||
}
|
|
||||||
|
|
||||||
$phone = $this->normalizeGroupedPhone((string) ($postIt->telefono ?? ''));
|
$phone = $this->normalizeGroupedPhone((string) ($postIt->telefono ?? ''));
|
||||||
if ($phone !== '') {
|
if ($phone !== '') {
|
||||||
return 'phone:' . $phone;
|
return 'phone:' . $phone;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'caller:' . mb_strtolower(trim((string) ($postIt->nome_chiamante ?? 'sconosciuto')));
|
$rubricaId = $this->resolveGroupedRubricaId($postIt);
|
||||||
|
if ($rubricaId !== null) {
|
||||||
|
return 'rubrica:' . $rubricaId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'caller:' . $this->normalizeGroupedCallerKey((string) ($postIt->nome_chiamante ?? 'sconosciuto'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isInternalPostIt(ChiamataPostIt $postIt): bool
|
private function isInternalPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
|
@ -734,21 +738,38 @@ private function isLegacyFilteredSmdrPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
|
||||||
private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage
|
private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage
|
||||||
{
|
{
|
||||||
$originId = (int) ($postIt->origine_id ?? 0);
|
$originKey = trim((string) ($postIt->origine_id ?? ''));
|
||||||
if ($originId <= 0) {
|
if ($originKey === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists($originId, $this->postItMessageCache)) {
|
if (array_key_exists($originKey, $this->postItMessageCache)) {
|
||||||
return $this->postItMessageCache[$originId];
|
return $this->postItMessageCache[$originKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
$message = CommunicationMessage::query()->find($originId);
|
$message = null;
|
||||||
$this->postItMessageCache[$originId] = $message;
|
if (ctype_digit($originKey)) {
|
||||||
|
$message = CommunicationMessage::query()->find((int) $originKey);
|
||||||
|
} elseif ((string) ($postIt->origine ?? '') === 'smdr') {
|
||||||
|
$message = CommunicationMessage::query()
|
||||||
|
->where('channel', 'smdr')
|
||||||
|
->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.smdr.fingerprint')) = ?", [$originKey])
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->postItMessageCache[$originKey] = $message;
|
||||||
|
|
||||||
return $message;
|
return $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function normalizeGroupedCallerKey(string $value): string
|
||||||
|
{
|
||||||
|
$normalized = preg_replace('/\s+/', ' ', trim(mb_strtolower($value))) ?? '';
|
||||||
|
|
||||||
|
return $normalized !== '' ? $normalized : 'sconosciuto';
|
||||||
|
}
|
||||||
|
|
||||||
private function normalizeGroupedPhone(string $phone): string
|
private function normalizeGroupedPhone(string $phone): string
|
||||||
{
|
{
|
||||||
$digits = preg_replace('/\D+/', '', $phone) ?: '';
|
$digits = preg_replace('/\D+/', '', $phone) ?: '';
|
||||||
|
|
@ -1024,25 +1045,8 @@ private function resolveRubricaIdByPhone(?string $phone): ?int
|
||||||
}
|
}
|
||||||
|
|
||||||
$adminId = (int) (Auth::user()?->amministratore?->id ?? 0);
|
$adminId = (int) (Auth::user()?->amministratore?->id ?? 0);
|
||||||
|
$result = app(RubricaPhoneLookupService::class)->findByPhone($digits, $adminId)?->id;
|
||||||
$query = RubricaUniversale::query();
|
$result = is_numeric($result) ? (int) $result : null;
|
||||||
if ($adminId > 0 && Schema::hasColumn('rubrica_universale', 'amministratore_id')) {
|
|
||||||
$query->where(function ($q) use ($adminId): void {
|
|
||||||
$q->where('amministratore_id', $adminId)
|
|
||||||
->orWhereNull('amministratore_id');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$rubricaId = $query
|
|
||||||
->where(function ($q) use ($digits): void {
|
|
||||||
$q->orWhereRaw("REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio,''), ' ', ''), '+', ''), '-', '') LIKE ?", ['%' . $digits . '%'])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare,''), ' ', ''), '+', ''), '-', '') LIKE ?", ['%' . $digits . '%'])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa,''), ' ', ''), '+', ''), '-', '') LIKE ?", ['%' . $digits . '%']);
|
|
||||||
})
|
|
||||||
->orderByDesc('id')
|
|
||||||
->value('id');
|
|
||||||
|
|
||||||
$result = is_numeric($rubricaId) ? (int) $rubricaId : null;
|
|
||||||
$this->rubricaPhoneCache[$digits] = $result;
|
$this->rubricaPhoneCache[$digits] = $result;
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
|
@ -1095,6 +1099,10 @@ private function normalizePostItOutcome(?string $outcome): string
|
||||||
|
|
||||||
public function isAnsweredPostIt(ChiamataPostIt $postIt): bool
|
public function isAnsweredPostIt(ChiamataPostIt $postIt): bool
|
||||||
{
|
{
|
||||||
|
if ($this->isMissedResponseGroupPostIt($postIt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
||||||
|
|
||||||
if (($postIt->durata_secondi ?? 0) > 0) {
|
if (($postIt->durata_secondi ?? 0) > 0) {
|
||||||
|
|
@ -1106,6 +1114,10 @@ public function isAnsweredPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
|
||||||
private function isMissedPostIt(ChiamataPostIt $postIt): bool
|
private function isMissedPostIt(ChiamataPostIt $postIt): bool
|
||||||
{
|
{
|
||||||
|
if ($this->isMissedResponseGroupPostIt($postIt)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
||||||
|
|
||||||
if (in_array($outcome, ['persa', 'missed', 'no_answer', 'unanswered', 'non_risposta'], true)) {
|
if (in_array($outcome, ['persa', 'missed', 'no_answer', 'unanswered', 'non_risposta'], true)) {
|
||||||
|
|
@ -1115,6 +1127,48 @@ private function isMissedPostIt(ChiamataPostIt $postIt): bool
|
||||||
return (string) $postIt->direzione === 'in_arrivo' && ! $this->isAnsweredPostIt($postIt) && (int) ($postIt->durata_secondi ?? 0) === 0;
|
return (string) $postIt->direzione === 'in_arrivo' && ! $this->isAnsweredPostIt($postIt) && (int) ($postIt->durata_secondi ?? 0) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isMissedResponseGroupPostIt(ChiamataPostIt $postIt): bool
|
||||||
|
{
|
||||||
|
if ((string) $postIt->direzione !== 'in_arrivo') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = $this->resolveMessageFromPostIt($postIt);
|
||||||
|
if (! $message instanceof CommunicationMessage || (string) $message->channel !== 'smdr') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$groupExtension = $this->normalizeExtension((string) data_get($message->metadata, 'smdr.extension', ''));
|
||||||
|
if (! $this->isMonitoredResponseGroupExtension($groupExtension)) {
|
||||||
|
$groupExtension = $this->normalizeExtension((string) ($message->target_extension ?? ''));
|
||||||
|
}
|
||||||
|
if (! $this->isMonitoredResponseGroupExtension($groupExtension)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$answerExtensions = array_values(array_unique(array_filter([
|
||||||
|
$this->normalizeExtension((string) data_get($message->metadata, 'smdr.target_extension', '')),
|
||||||
|
$this->normalizeExtension((string) ($message->target_extension ?? '')),
|
||||||
|
])));
|
||||||
|
|
||||||
|
foreach ($answerExtensions as $answerExtension) {
|
||||||
|
if (! $this->isMonitoredResponseGroupExtension($answerExtension)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isMonitoredResponseGroupExtension(?string $extension): bool
|
||||||
|
{
|
||||||
|
if ($extension === null || $extension === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($extension, $this->monitoredResponseGroups, true);
|
||||||
|
}
|
||||||
|
|
||||||
public function getPostItCallStateMeta(ChiamataPostIt $postIt): array
|
public function getPostItCallStateMeta(ChiamataPostIt $postIt): array
|
||||||
{
|
{
|
||||||
if ($this->isMissedPostIt($postIt)) {
|
if ($this->isMissedPostIt($postIt)) {
|
||||||
|
|
|
||||||
|
|
@ -834,7 +834,7 @@ public function hydrateNominativiStorici(): void
|
||||||
})->all();
|
})->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function hydrateRecapitiServizio(): void
|
public function hydrateRecapitiServizio(): void
|
||||||
{
|
{
|
||||||
$this->recapitiServizio = [];
|
$this->recapitiServizio = [];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Models\CommunicationMessage;
|
use App\Models\CommunicationMessage;
|
||||||
use App\Models\PbxClickToCallRequest;
|
use App\Models\PbxClickToCallRequest;
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
|
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||||
use App\Services\Cti\PbxRoutingService;
|
use App\Services\Cti\PbxRoutingService;
|
||||||
use App\Support\PhoneNumber;
|
use App\Support\PhoneNumber;
|
||||||
use Carbon\CarbonInterface;
|
use Carbon\CarbonInterface;
|
||||||
|
|
@ -491,25 +492,7 @@ private function mirrorToPeerIfEnabled(Request $request, string $endpoint, array
|
||||||
|
|
||||||
private function findRubricaByPhone(string $phone): ?RubricaUniversale
|
private function findRubricaByPhone(string $phone): ?RubricaUniversale
|
||||||
{
|
{
|
||||||
$digits = PhoneNumber::normalizeDigits($phone);
|
return app(RubricaPhoneLookupService::class)->findByPhone($phone);
|
||||||
if ($digits === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$tail = substr($digits, -7);
|
|
||||||
if ($tail === false || $tail === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return RubricaUniversale::query()
|
|
||||||
->where(function ($q) use ($tail) {
|
|
||||||
$q->orWhere('telefono_cellulare', 'like', '%' . $tail . '%')
|
|
||||||
->orWhere('telefono_ufficio', 'like', '%' . $tail . '%')
|
|
||||||
->orWhere('telefono_casa', 'like', '%' . $tail . '%')
|
|
||||||
->orWhere('telefono_cellulare_nazionale', 'like', '%' . $tail . '%');
|
|
||||||
})
|
|
||||||
->orderByDesc('updated_at')
|
|
||||||
->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function findOpenPostIt(string $eventId, string $phone): ?ChiamataPostIt
|
private function findOpenPostIt(string $eventId, string $phone): ?ChiamataPostIt
|
||||||
|
|
|
||||||
91
app/Services/Anagrafiche/RubricaPhoneLookupService.php
Normal file
91
app/Services/Anagrafiche/RubricaPhoneLookupService.php
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Anagrafiche;
|
||||||
|
|
||||||
|
use App\Models\RubricaUniversale;
|
||||||
|
use App\Support\PhoneNumber;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class RubricaPhoneLookupService
|
||||||
|
{
|
||||||
|
public function findByPhone(?string $phone, ?int $adminId = null): ?RubricaUniversale
|
||||||
|
{
|
||||||
|
$digits = PhoneNumber::normalizeDigits((string) $phone);
|
||||||
|
if ($digits === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tail = strlen($digits) >= 7 ? substr($digits, -7) : $digits;
|
||||||
|
if ($tail === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([[$digits, true], [$digits, false], [$tail, true], [$tail, false]] as [$needle, $directFirst]) {
|
||||||
|
$found = $directFirst
|
||||||
|
? ($this->directQuery((string) $needle, $adminId)->first() ?: $this->channelQuery((string) $needle, $adminId)->first())
|
||||||
|
: ($this->channelQuery((string) $needle, $adminId)->first() ?: $this->directQuery((string) $needle, $adminId)->first());
|
||||||
|
|
||||||
|
if ($found instanceof RubricaUniversale) {
|
||||||
|
return $found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function directQuery(string $digits, ?int $adminId)
|
||||||
|
{
|
||||||
|
return RubricaUniversale::query()
|
||||||
|
->when($this->hasAmministratoreColumn() && $adminId && $adminId > 0, function ($query) use ($adminId): void {
|
||||||
|
$query->where(function ($inner) use ($adminId): void {
|
||||||
|
$inner->where('amministratore_id', $adminId)
|
||||||
|
->orWhereNull('amministratore_id');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->where(function ($query) use ($digits): void {
|
||||||
|
$query
|
||||||
|
->orWhereRaw($this->normalizedDigitsSql('telefono_cellulare') . ' LIKE ?', ['%' . $digits . '%'])
|
||||||
|
->orWhereRaw($this->normalizedDigitsSql('telefono_ufficio') . ' LIKE ?', ['%' . $digits . '%'])
|
||||||
|
->orWhereRaw($this->normalizedDigitsSql('telefono_casa') . ' LIKE ?', ['%' . $digits . '%']);
|
||||||
|
|
||||||
|
if (Schema::hasColumn('rubrica_universale', 'telefono_cellulare_nazionale')) {
|
||||||
|
$query->orWhereRaw($this->normalizedDigitsSql('telefono_cellulare_nazionale') . ' LIKE ?', ['%' . $digits . '%']);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->orderByDesc('updated_at')
|
||||||
|
->orderByDesc('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function channelQuery(string $digits, ?int $adminId)
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('rubrica_contatti_canali')) {
|
||||||
|
return RubricaUniversale::query()->whereRaw('1 = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return RubricaUniversale::query()
|
||||||
|
->select('rubrica_universale.*')
|
||||||
|
->join('rubrica_contatti_canali as rcc', 'rcc.rubrica_id', '=', 'rubrica_universale.id')
|
||||||
|
->when($this->hasAmministratoreColumn() && $adminId && $adminId > 0, function ($query) use ($adminId): void {
|
||||||
|
$query->where(function ($inner) use ($adminId): void {
|
||||||
|
$inner->where('rubrica_universale.amministratore_id', $adminId)
|
||||||
|
->orWhereNull('rubrica_universale.amministratore_id');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->whereIn('rcc.tipo', ['telefono', 'cellulare'])
|
||||||
|
->whereRaw($this->normalizedDigitsSql('rcc.valore') . ' LIKE ?', ['%' . $digits . '%'])
|
||||||
|
->orderByDesc('rcc.is_principale')
|
||||||
|
->orderByDesc('rcc.updated_at')
|
||||||
|
->orderByDesc('rubrica_universale.updated_at')
|
||||||
|
->orderByDesc('rubrica_universale.id');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hasAmministratoreColumn(): bool
|
||||||
|
{
|
||||||
|
return Schema::hasColumn('rubrica_universale', 'amministratore_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizedDigitsSql(string $column): string
|
||||||
|
{
|
||||||
|
return "REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE({$column}, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '')";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
use App\Models\RubricaUniversale;
|
use App\Models\RubricaUniversale;
|
||||||
use App\Models\Soggetto;
|
use App\Models\Soggetto;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||||
|
|
||||||
class LiveIncomingCallService
|
class LiveIncomingCallService
|
||||||
{
|
{
|
||||||
|
|
@ -119,25 +120,7 @@ private function normalizePhoneDigits(string $value): string
|
||||||
|
|
||||||
private function matchRubricaByPhone(string $digits): ?RubricaUniversale
|
private function matchRubricaByPhone(string $digits): ?RubricaUniversale
|
||||||
{
|
{
|
||||||
if ($digits === '') {
|
return app(RubricaPhoneLookupService::class)->findByPhone($digits);
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$needle = '%' . $digits . '%';
|
|
||||||
$tail = strlen($digits) >= 7 ? substr($digits, -7) : $digits;
|
|
||||||
$tailNeedle = '%' . $tail . '%';
|
|
||||||
|
|
||||||
return RubricaUniversale::query()
|
|
||||||
->where(function ($query) use ($needle, $tailNeedle): void {
|
|
||||||
$query->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle])
|
|
||||||
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$tailNeedle]);
|
|
||||||
})
|
|
||||||
->orderByDesc('updated_at')
|
|
||||||
->first();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function matchSoggetto(?RubricaUniversale $rubrica, string $phone): ?Soggetto
|
private function matchSoggetto(?RubricaUniversale $rubrica, string $phone): ?Soggetto
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,83 @@ ## Finestra temporale smoke test
|
||||||
|
|
||||||
Per il primo passaggio operativo non importare tutto lo storico.
|
Per il primo passaggio operativo non importare tutto lo storico.
|
||||||
|
|
||||||
|
## Criterio autorevole unita e nominativi
|
||||||
|
|
||||||
|
- Lo snapshot corrente di `unita`, `soggetti` e `diritti` deve derivare sempre dall'ultimo anno legacy disponibile per lo stabile, non dall'anno storico che si sta rileggendo in quel momento.
|
||||||
|
- La chiave certa di allineamento tra staging e dominio e `condomin.cod_cond`, che nel dominio va mantenuta in `unita_immobiliari.legacy_cond_id`.
|
||||||
|
- `scala`, `interno` e `catasto_sub` servono per esporre il codice mnemonico operatore e per le verifiche di riconciliazione, ma non devono sostituire `cod_cond` come chiave interna.
|
||||||
|
- Se si rilancia un'importazione su un anno precedente, il preflight deve comunque precaricare anche l'ultima annualita disponibile per evitare che lo snapshot corrente venga ricostruito con un anno vecchio.
|
||||||
|
- Non vanno creati fallback artificiali come `interno = 0`, unita sintetiche o nominativi fittizi per colmare buchi che non esistono nella sorgente.
|
||||||
|
|
||||||
|
## Regola storico anni precedenti
|
||||||
|
|
||||||
|
- Gli anni precedenti arricchiscono `unita_immobiliare_nominativi` come cronologia delle variazioni.
|
||||||
|
- Non devono sovrascrivere il nominativo corrente dell'ultimo anno disponibile.
|
||||||
|
- Le compresenze di piu comproprietari nello stesso intervallo sono lecite e vanno distinte dai duplicati veri dello stesso nominativo.
|
||||||
|
|
||||||
|
## Bonifica anagrafiche post-import
|
||||||
|
|
||||||
|
- Dopo gli step `unita`, `soggetti` e `diritti`, eseguire la bonifica recapiti con `php artisan gescon:bonifica-rubrica-canali --stabile=<codice> --apply`.
|
||||||
|
- La bonifica spezza telefoni, email e PEC salvati nello stesso campo usando separatori legacy come `;`, `,`, newline o `/`.
|
||||||
|
- I recapiti vengono materializzati in `rubrica_contatti_canali` con tipo esplicito (`telefono`, `cellulare`, `email`, `pec`) e flag `is_principale`.
|
||||||
|
- Regola di classificazione attuale:
|
||||||
|
- numeri che iniziano con `3` => `cellulare`
|
||||||
|
- numeri che iniziano con `0` => `telefono`
|
||||||
|
- gli altri restano sul tipo di origine se gia noto
|
||||||
|
- La push/sync Google deve leggere e scrivere anche i canali aggiuntivi, non solo i campi primari della rubrica.
|
||||||
|
|
||||||
|
## Regola codice unita
|
||||||
|
|
||||||
|
- `unita_immobiliari.codice_unita` e limitato a `varchar(20)` nel dominio corrente.
|
||||||
|
- I codici generati dall'import devono quindi essere compatti e deterministici:
|
||||||
|
- rimozione dei separatori interni non significativi nei segmenti legacy
|
||||||
|
- se il codice supera ancora il limite, uso di suffisso hash corto stabile invece di troncare in modo ambiguo
|
||||||
|
- Questa regola e necessaria per stabili come `0013`, dove gli interni legacy aggregati possono produrre stringhe del tipo `0013-NGOT-107/109-C47`.
|
||||||
|
|
||||||
|
## Piloti verificati 08/04/2026
|
||||||
|
|
||||||
|
### `0002` - Carlo Alberto Racchia 2
|
||||||
|
|
||||||
|
- anno autorevole importato: `2024` (`0038`)
|
||||||
|
- `unita`: `61`
|
||||||
|
- `soggetti`: `52`
|
||||||
|
- `diritti`: `82` inseriti
|
||||||
|
- QA `netgescon:qa-unita-nominativi --stabile_id=8`:
|
||||||
|
- unita attive `61`
|
||||||
|
- unita senza nominativi `0`
|
||||||
|
- duplicati periodi `0`
|
||||||
|
- nessuna anomalia bloccante
|
||||||
|
- caso da tenere distinto dai buchi veri:
|
||||||
|
- `cod_cond = 62` nello snapshot `0038` risulta come record comune (`nome = CONDOMINIO (magazzino)`) e non ha `scala`, `interno`, `catasto_sub`, `piano`
|
||||||
|
- quindi oggi viene correttamente escluso dalla costruzione delle UI ordinarie se si pretende un identificativo fisico autorevole
|
||||||
|
|
||||||
|
### `0013` - Ottaviano 105 / Giulio Cesare 171
|
||||||
|
|
||||||
|
- anno autorevole importato: `2026` (`0015`)
|
||||||
|
- fix necessario prima del rilancio:
|
||||||
|
- compattazione deterministica di `codice_unita` per non superare il limite DB
|
||||||
|
- esito finale dopo fix:
|
||||||
|
- `unita`: `26` processate, `12` aggiornate
|
||||||
|
- `soggetti`: `50`
|
||||||
|
- `diritti`: `90` inseriti, `3` aggiornati, `39` ignorati
|
||||||
|
- QA `netgescon:qa-unita-nominativi --stabile_id=16`: nessuna anomalia bloccante
|
||||||
|
- attenzione funzionale aperta:
|
||||||
|
- stabile con unita fisicamente/catastalmente uniche ma spezzate nel legacy per gestire piu titolari
|
||||||
|
- la futura unione UI deve appoggiarsi a dati catastali e a logiche di cumulo/quote, non a merge distruttivi dei record legacy
|
||||||
|
|
||||||
|
### `0023` - Germanico 79 (legacy `8`)
|
||||||
|
|
||||||
|
- anno autorevole importato: `2025` (`0096`)
|
||||||
|
- `unita`: `27` processate, `16` aggiornate
|
||||||
|
- `soggetti`: `20`
|
||||||
|
- `diritti`: `25` inseriti, `15` aggiornati, `2` ignorati
|
||||||
|
- QA `netgescon:qa-unita-nominativi --stabile_id=24`: nessuna anomalia bloccante
|
||||||
|
|
||||||
|
## Regola straordinarie
|
||||||
|
|
||||||
|
- Le spese straordinarie vanno considerate aperte nella gestione in cui vengono lette.
|
||||||
|
- Codici legacy uguali presenti in anni diversi non vanno collassati tra loro: devono restare distinti almeno per anno gestione e, se serve, per numero progressivo straordinaria.
|
||||||
|
|
||||||
### Import base
|
### Import base
|
||||||
|
|
||||||
- anno corrente legacy piu recente disponibile
|
- anno corrente legacy piu recente disponibile
|
||||||
|
|
@ -122,6 +199,9 @@ ### E. QA dominio post-import
|
||||||
- stabile creato/aggiornato correttamente
|
- stabile creato/aggiornato correttamente
|
||||||
- unita importate
|
- unita importate
|
||||||
- rubrica/nominativi deduplicati
|
- rubrica/nominativi deduplicati
|
||||||
|
- snapshot corrente allineato all'ultima annualita disponibile
|
||||||
|
- `legacy_cond_id` valorizzato e coerente con `condomin.cod_cond`
|
||||||
|
- nessuna unita attiva con `interno = 0` o codice sintetico inventato
|
||||||
- rate/incassi coerenti per gli anni caricati
|
- rate/incassi coerenti per gli anni caricati
|
||||||
- nessun crash nelle pagine operative principali
|
- nessun crash nelle pagine operative principali
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ # Modifiche NetGescon
|
||||||
|
|
||||||
| Data | Versione | Area | Descrizione | Tipo |
|
| Data | Versione | Area | Descrizione | Tipo |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 08/04/2026 | 0.8.x-dev | Rubrica / PBX / Google | Introdotto `gescon:bonifica-rubrica-canali`, allineato il lookup telefonico PBX ai canali multipli di rubrica e estesa la sync Google ai recapiti secondari (`rubrica_contatti_canali`) per email, PEC e telefoni multipli sullo stesso nominativo | [U] |
|
||||||
|
| 08/04/2026 | 0.8.x-dev | Gescon / Import / Unita | Corretta la generazione di `codice_unita` in `gescon:import-full` con compattazione deterministica entro il limite DB (`varchar(20)`), sbloccando i casi reali di unita aggregate come `0013` | [U] |
|
||||||
|
| 08/04/2026 | 0.8.x-dev | Gescon / Import / Piloti | Validati in sola anagrafica i piloti `0002` (Racchia 2), `0013` (Ottaviano 105 / Giulio Cesare 171) e `0023` (Germanico 79 legacy 8), con QA bloccante pulito e evidenza del caso speciale `0002/cod_cond 62` come record comune senza identificativo fisico autorevole | [U] |
|
||||||
|
| 08/04/2026 | 0.8.x-dev | Gescon / Import / Unita-Nominativi | Rifattorizzato `gescon:import-full` per usare l'ultima annualita legacy come snapshot corrente autorevole di unita, soggetti e diritti, agganciando le UI per `cod_cond/legacy_cond_id`, precaricando l'anno autorevole anche se si importa uno storico e bloccando la creazione di unita fittizie con `interno=0` | [U] |
|
||||||
|
| 08/04/2026 | 0.8.x-dev | QA Database | Aggiornato `netgescon:qa-unita-nominativi` per distinguere i duplicati veri dello stesso nominativo dalle compresenze lecite di piu intestatari sulla stessa UI | [U] |
|
||||||
| 01/04/2026 | 0.8.x-dev | Gescon / Import / Anagrafica unica | Introdotto `gescon:materialize-persone-relazioni` per materializzare `persone` e `persone_unita_relazioni` da `unita_immobiliare_nominativi`, con innesto dello step `relazioni` nel `gescon:import-full` | [P] |
|
| 01/04/2026 | 0.8.x-dev | Gescon / Import / Anagrafica unica | Introdotto `gescon:materialize-persone-relazioni` per materializzare `persone` e `persone_unita_relazioni` da `unita_immobiliare_nominativi`, con innesto dello step `relazioni` nel `gescon:import-full` | [P] |
|
||||||
| 01/04/2026 | 0.8.x-dev | Supporto / Modifiche / Git staging | Spostato il flusso `Aggiornamento staging da Gitea` nella tab `Aggiornamenti + Bug`, con riepilogo esplicito di cosa aggiorna la UI e del comando predisposto `netgescon:git-sync` | [U] |
|
| 01/04/2026 | 0.8.x-dev | Supporto / Modifiche / Git staging | Spostato il flusso `Aggiornamento staging da Gitea` nella tab `Aggiornamenti + Bug`, con riepilogo esplicito di cosa aggiorna la UI e del comando predisposto `netgescon:git-sync` | [U] |
|
||||||
| 01/04/2026 | 0.8.x-dev | Rubrica / Stabili / Relazioni | Riallineato il caso reale `GERMANICO 96` materializzando 191 relazioni persona-unita e rilanciando la sync ruoli rubrica dal dato legacy gia importato | [U] |
|
| 01/04/2026 | 0.8.x-dev | Rubrica / Stabili / Relazioni | Riallineato il caso reale `GERMANICO 96` materializzando 191 relazioni persona-unita e rilanciando la sync ruoli rubrica dal dato legacy gia importato | [U] |
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
## Flusso rapido import MDB (DEV)
|
# Flusso rapido import MDB (DEV)
|
||||||
|
|
||||||
|
## Regola aggiornata 08/04/2026
|
||||||
|
|
||||||
|
- Per `unita`, `soggetti` e `diritti`, il corrente non si costruisce piu dall'anno passato in `--anno` se esiste un'annualita legacy piu recente.
|
||||||
|
- Per `0021`, lo snapshot corrente autorevole e `0004`.
|
||||||
|
- Gli anni precedenti, come `0003`, restano sorgente di cronologia e quadratura, ma non devono riaprire codici vecchi tipo `A/0`, `A/42`, `A/43`, `A/215`, `A/216`.
|
||||||
|
- Il matching tra staging e dominio va letto cosi:
|
||||||
|
- `condomin.cod_cond` -> `unita_immobiliari.legacy_cond_id`
|
||||||
|
- `scala/interno` -> codice mnemonico operatore e controllo di riconciliazione
|
||||||
|
- Le straordinarie con stesso codice su anni diversi restano distinte per anno/numero progressivo e vanno considerate aperte nella gestione in cui vengono lette.
|
||||||
|
|
||||||
- Usa solo **/home/michele/netgescon/netgescon-laravel** per i comandi (niente /var/www/netgescon).
|
- Usa solo **/home/michele/netgescon/netgescon-laravel** per i comandi (niente /var/www/netgescon).
|
||||||
- Carica lo staging `gescon_import.condomin` con mdb-tools:
|
- Carica lo staging `gescon_import.condomin` con mdb-tools:
|
||||||
|
|
@ -12,6 +22,7 @@ ## Flusso rapido import MDB (DEV)
|
||||||
- Se devi ricalcolare piano/scala: prima pulisci `unita_immobiliari` (e `unita_pertinenze`) per quello stabile, poi riesegui i due comandi sopra.
|
- Se devi ricalcolare piano/scala: prima pulisci `unita_immobiliari` (e `unita_pertinenze`) per quello stabile, poi riesegui i due comandi sopra.
|
||||||
|
|
||||||
### Esito corrente (stabile 0021 / anno 0003 in DEV)
|
### Esito corrente (stabile 0021 / anno 0003 in DEV)
|
||||||
|
|
||||||
- `load-mdb condomin`: 268 righe staging
|
- `load-mdb condomin`: 268 righe staging
|
||||||
- `import-full --solo=unita`: 212 record
|
- `import-full --solo=unita`: 212 record
|
||||||
- `import-full --solo=soggetti`: 55 record
|
- `import-full --solo=soggetti`: 55 record
|
||||||
|
|
@ -20,6 +31,67 @@ ### Esito corrente (stabile 0021 / anno 0003 in DEV)
|
||||||
|
|
||||||
Ripeti la stessa sequenza per gli altri anni/cartelle cambiando `--stabile` e `--anno` e il path del singolo_anno.mdb.
|
Ripeti la stessa sequenza per gli altri anni/cartelle cambiando `--stabile` e `--anno` e il path del singolo_anno.mdb.
|
||||||
|
|
||||||
|
### Verifica reale aggiornata (stabile 0021 / snapshot corrente 0004)
|
||||||
|
|
||||||
|
- `import-full --solo=unita --anno=0004`: `140` record processati, `138` aggiornati
|
||||||
|
- `import-full --solo=soggetti --anno=0004`: `2` record processati
|
||||||
|
- `import-full --solo=diritti --anno=0004`: `6` inseriti, `3` aggiornati, `270` ignorati
|
||||||
|
- QA `netgescon:qa-unita-nominativi --stabile_id=25`:
|
||||||
|
- unita attive `214`
|
||||||
|
- unita senza nominativi `0`
|
||||||
|
- tutte le `214` unita attive mappano su `condomin 0021/0004` tramite `legacy_cond_id = cod_cond`
|
||||||
|
- solo `210` mappano ancora su `0003`, confermando che `0004` e il riferimento corrente corretto
|
||||||
|
- nessuna unita attiva con `interno = 0`
|
||||||
|
- i vecchi codici `A/0`, `A/42`, `A/43`, `A/215`, `A/216` non sono piu attivi; restano solo come righe soft-deleted storiche
|
||||||
|
|
||||||
|
### Bonifica recapiti rubrica 08/04/2026
|
||||||
|
|
||||||
|
- nuovo comando operativo: `php artisan gescon:bonifica-rubrica-canali --stabile=<codice> --apply`
|
||||||
|
- scopo:
|
||||||
|
- separare email/PEC multiple salvate in un solo campo
|
||||||
|
- separare telefoni multipli e classificarli in `telefono` o `cellulare`
|
||||||
|
- popolare `rubrica_contatti_canali` per PBX, Google Contacts e UI rubrica
|
||||||
|
- verifica reale su `0021`:
|
||||||
|
- `192` rubriche analizzate
|
||||||
|
- `133` rubriche aggiornate
|
||||||
|
- `360` canali creati
|
||||||
|
- caso ATER (`rubrica_id=2151`) riallineato con `5` email e `2` telefoni distinti
|
||||||
|
|
||||||
|
### Piloti anagrafici aggiuntivi 08/04/2026
|
||||||
|
|
||||||
|
#### `0002` - Carlo Alberto Racchia 2
|
||||||
|
|
||||||
|
- anno autorevole: `2024` (`0038`)
|
||||||
|
- `unita`: `61`
|
||||||
|
- `soggetti`: `52`
|
||||||
|
- `diritti`: `82` inseriti
|
||||||
|
- QA `stabile_id=8`: nessuna anomalia bloccante
|
||||||
|
- nota importante:
|
||||||
|
- `cod_cond 62` nello snapshot autorevole e un record comune (`CONDOMINIO (magazzino)`) senza `scala/interno/sub/piano`
|
||||||
|
- non va trattato come buco dati dell'unita privata ma come voce comune priva di identificativo fisico utile alla UI unita
|
||||||
|
|
||||||
|
#### `0013` - Ottaviano 105 / Giulio Cesare 171
|
||||||
|
|
||||||
|
- anno autorevole: `2026` (`0015`)
|
||||||
|
- inizialmente l'import `unita` falliva per overflow di `codice_unita`
|
||||||
|
- fix applicato: compattazione deterministica del codice entro il limite `varchar(20)`
|
||||||
|
- esito finale:
|
||||||
|
- `unita`: `26` processate, `12` aggiornate
|
||||||
|
- `soggetti`: `50`
|
||||||
|
- `diritti`: `90` inseriti, `3` aggiornati, `39` ignorati
|
||||||
|
- QA `stabile_id=16`: nessuna anomalia bloccante
|
||||||
|
- da modellare a livello dominio/UI:
|
||||||
|
- unita legacy spezzate per proprietari multipli ma fisicamente/catastalmente uniche
|
||||||
|
- futura unione per dati catastali con riparto quote/millesimi separato
|
||||||
|
|
||||||
|
#### `0023` - Germanico 79 (legacy `8`)
|
||||||
|
|
||||||
|
- anno autorevole: `2025` (`0096`)
|
||||||
|
- `unita`: `27` processate, `16` aggiornate
|
||||||
|
- `soggetti`: `20`
|
||||||
|
- `diritti`: `25` inseriti, `15` aggiornati, `2` ignorati
|
||||||
|
- QA `stabile_id=24`: nessuna anomalia bloccante
|
||||||
|
|
||||||
il primo file da importare è /home/michele/netgescon/docs/02-architettura-laravel/Gescon/cartellestabili_e_anni.mdb qui trovi le prime info per le varie gestioni e cartelle
|
il primo file da importare è /home/michele/netgescon/docs/02-architettura-laravel/Gescon/cartellestabili_e_anni.mdb qui trovi le prime info per le varie gestioni e cartelle
|
||||||
|
|
||||||
il secondo file è /home/michele/netgescon/docs/02-architettura-laravel/Gescon/dbc/Stabili.mdb qui trovi le indicazioni di tutti gli stabili
|
il secondo file è /home/michele/netgescon/docs/02-architettura-laravel/Gescon/dbc/Stabili.mdb qui trovi le indicazioni di tutti gli stabili
|
||||||
|
|
@ -28,8 +100,6 @@ ### Esito corrente (stabile 0021 / anno 0003 in DEV)
|
||||||
/home/michele/netgescon/docs/02-architettura-laravel/Gescon/0021/generale_stabile_sf.mdb per le info generali
|
/home/michele/netgescon/docs/02-architettura-laravel/Gescon/0021/generale_stabile_sf.mdb per le info generali
|
||||||
e /home/michele/netgescon/docs/02-architettura-laravel/Gescon/0021/0001/singolo_anno.ldb per i dati di ogni singolo anno, naturlmente sono tutti collegati tra di loro in varie maniere
|
e /home/michele/netgescon/docs/02-architettura-laravel/Gescon/0021/0001/singolo_anno.ldb per i dati di ogni singolo anno, naturlmente sono tutti collegati tra di loro in varie maniere
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Trovi dentro la cartella /home/michele/netgescon/docs/02-architettura-laravel/Gescon e ti trovi dentro la cartella Gescon da dove importare i dati, Per la visualizzazione delle gestioni puoi utilizzare il riferimento che ti allego come immagine mettendo la visualizzazione anche del periodo che trovi dentro la cartella 0021 nel file GENERALE_STABILI.MDB nella tabella anni e poi i relativi campi sono semplici da trovare
|
Trovi dentro la cartella /home/michele/netgescon/docs/02-architettura-laravel/Gescon e ti trovi dentro la cartella Gescon da dove importare i dati, Per la visualizzazione delle gestioni puoi utilizzare il riferimento che ti allego come immagine mettendo la visualizzazione anche del periodo che trovi dentro la cartella 0021 nel file GENERALE_STABILI.MDB nella tabella anni e poi i relativi campi sono semplici da trovare
|
||||||
|
|
||||||
la cartella che devi considerare per l'importazione è la mappatura dei campi per la successiava automazione del sistema d'importazione è in docs 000-IMPORT
|
la cartella che devi considerare per l'importazione è la mappatura dei campi per la successiava automazione del sistema d'importazione è in docs 000-IMPORT
|
||||||
|
|
@ -37,5 +107,4 @@ ### Esito corrente (stabile 0021 / anno 0003 in DEV)
|
||||||
La costruzione delle viste è dei menù devi utilizzare il manuale 90-UI-interfaccia-unica
|
La costruzione delle viste è dei menù devi utilizzare il manuale 90-UI-interfaccia-unica
|
||||||
I manuali per i database è 91-database dove trovi anche le credenziali.
|
I manuali per i database è 91-database dove trovi anche le credenziali.
|
||||||
|
|
||||||
|
|
||||||
Ci sono alcuni campi che dobbiamo tenre in considerazione per l'im portazione primo va c
|
Ci sono alcuni campi che dobbiamo tenre in considerazione per l'im portazione primo va c
|
||||||
|
|
@ -1,6 +1,37 @@
|
||||||
@php
|
@php
|
||||||
$user = \Illuminate\Support\Facades\Auth::user();
|
$user = \Illuminate\Support\Facades\Auth::user();
|
||||||
|
|
||||||
|
$stabileHasRiscaldamento = static function (?\App\Models\Stabile $stabile): bool {
|
||||||
|
if (! $stabile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) ($stabile->riscaldamento_centralizzato ?? false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rateRiscaldamento = $stabile->rate_riscaldamento_mesi ?? [];
|
||||||
|
if (is_string($rateRiscaldamento)) {
|
||||||
|
$rateRiscaldamento = json_decode($rateRiscaldamento, true);
|
||||||
|
}
|
||||||
|
if (is_array($rateRiscaldamento) && $rateRiscaldamento !== []) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $stabile->configurazione_avanzata ?? [];
|
||||||
|
if (is_string($config)) {
|
||||||
|
$config = json_decode($config, true);
|
||||||
|
}
|
||||||
|
if (is_array($config) && array_key_exists('mostra_riscaldamento', $config) && $config['mostra_riscaldamento']) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Illuminate\Support\Facades\DB::table('gestioni_contabili')
|
||||||
|
->where('stabile_id', $stabile->id)
|
||||||
|
->where('tipo_gestione', 'riscaldamento')
|
||||||
|
->exists();
|
||||||
|
};
|
||||||
|
|
||||||
$stabileLabel = static function (?\App\Models\Stabile $stabile): string {
|
$stabileLabel = static function (?\App\Models\Stabile $stabile): string {
|
||||||
if (! $stabile) {
|
if (! $stabile) {
|
||||||
return 'Seleziona stabile';
|
return 'Seleziona stabile';
|
||||||
|
|
@ -32,7 +63,7 @@
|
||||||
|
|
||||||
$labelGestione = $gestioni[$gestioneAttiva] ?? (string) $gestioneAttiva;
|
$labelGestione = $gestioni[$gestioneAttiva] ?? (string) $gestioneAttiva;
|
||||||
|
|
||||||
$haRiscaldamento = (bool) ($stabileAttivo?->riscaldamento_centralizzato ?? false);
|
$haRiscaldamento = $stabileHasRiscaldamento($stabileAttivo);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="flex min-w-0 items-center gap-2 ps-3">
|
<div class="flex min-w-0 items-center gap-2 ps-3">
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
@if($isInlineEditing)
|
@if($isInlineEditing)
|
||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<x-slot name="heading">Modifica Contatto Inline</x-slot>
|
<x-slot name="heading">Modifica Contatto Inline</x-slot>
|
||||||
<x-slot name="description">Modifica i campi direttamente in pagina, senza modal.</x-slot>
|
<x-slot name="description">Modifica i campi principali senza lasciare aperti i pannelli laterali. Per numeri, email e PEC multiple usa i contatti avanzati.</x-slot>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
<div class="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
<label class="text-sm">
|
<label class="text-sm">
|
||||||
|
|
@ -20,11 +20,21 @@
|
||||||
|
|
||||||
<label class="text-sm">
|
<label class="text-sm">
|
||||||
<span class="mb-1 block text-gray-600">Tipo contatto</span>
|
<span class="mb-1 block text-gray-600">Tipo contatto</span>
|
||||||
<input type="text" wire:model.defer="inlineForm.tipo_contatto" class="w-full rounded-md border-gray-300 text-sm" />
|
<select wire:model.defer="inlineForm.tipo_contatto" class="w-full rounded-md border-gray-300 text-sm">
|
||||||
|
<option value="">Seleziona</option>
|
||||||
|
@foreach($this->getTipoContattoOptions() as $optionValue => $optionLabel)
|
||||||
|
<option value="{{ $optionValue }}">{{ $optionLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="text-sm">
|
<label class="text-sm">
|
||||||
<span class="mb-1 block text-gray-600">Categoria</span>
|
<span class="mb-1 block text-gray-600">Categoria</span>
|
||||||
<input type="text" wire:model.defer="inlineForm.categoria" class="w-full rounded-md border-gray-300 text-sm" />
|
<select wire:model.defer="inlineForm.categoria" class="w-full rounded-md border-gray-300 text-sm">
|
||||||
|
<option value="">Seleziona</option>
|
||||||
|
@foreach($this->getCategoriaOptions() as $optionValue => $optionLabel)
|
||||||
|
<option value="{{ $optionValue }}">{{ $optionLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="text-sm">
|
<label class="text-sm">
|
||||||
<span class="mb-1 block text-gray-600">Codice fiscale</span>
|
<span class="mb-1 block text-gray-600">Codice fiscale</span>
|
||||||
|
|
@ -105,7 +115,12 @@
|
||||||
|
|
||||||
<label class="text-sm">
|
<label class="text-sm">
|
||||||
<span class="mb-1 block text-gray-600">Profilo chiamante</span>
|
<span class="mb-1 block text-gray-600">Profilo chiamante</span>
|
||||||
<input type="text" wire:model.defer="inlineForm.tipo_utenza_call" class="w-full rounded-md border-gray-300 text-sm" />
|
<select wire:model.defer="inlineForm.tipo_utenza_call" class="w-full rounded-md border-gray-300 text-sm">
|
||||||
|
<option value="">Seleziona</option>
|
||||||
|
@foreach($this->getTipoUtenzaCallOptions() as $optionValue => $optionLabel)
|
||||||
|
<option value="{{ $optionValue }}">{{ $optionLabel }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="text-sm">
|
<label class="text-sm">
|
||||||
<span class="mb-1 block text-gray-600">Rif. stabile</span>
|
<span class="mb-1 block text-gray-600">Rif. stabile</span>
|
||||||
|
|
@ -128,10 +143,12 @@
|
||||||
|
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
<x-filament::button type="button" wire:click="saveInlineEdit">Salva modifiche</x-filament::button>
|
<x-filament::button type="button" wire:click="saveInlineEdit">Salva modifiche</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="mountAction('contatti_avanzati')">Contatti avanzati</x-filament::button>
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="mountAction('email_multiple')">Email aggiuntive</x-filament::button>
|
||||||
<x-filament::button type="button" color="gray" wire:click="cancelInlineEdit">Annulla</x-filament::button>
|
<x-filament::button type="button" color="gray" wire:click="cancelInlineEdit">Annulla</x-filament::button>
|
||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
@endif
|
@else
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
|
@ -673,7 +690,15 @@
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<x-filament::button type="button" color="gray" wire:click="mountAction('contatti')">
|
<x-filament::button type="button" color="gray" wire:click="mountAction('contatti')">
|
||||||
Contatti aggiuntivi
|
Modifica anagrafica inline
|
||||||
|
</x-filament::button>
|
||||||
|
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="mountAction('contatti_avanzati')">
|
||||||
|
Numeri, email e PEC multiple
|
||||||
|
</x-filament::button>
|
||||||
|
|
||||||
|
<x-filament::button type="button" color="gray" wire:click="mountAction('email_multiple')">
|
||||||
|
Email aggiuntive persona
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
|
|
||||||
<x-filament::button type="button" color="gray" wire:click="mountAction('ruoli_legami')">
|
<x-filament::button type="button" color="gray" wire:click="mountAction('ruoli_legami')">
|
||||||
|
|
@ -746,4 +771,5 @@
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
@endif
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user