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;
|
||||
|
||||
use App\Models\Amministratore;
|
||||
use App\Models\RubricaContattoCanale;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Support\PhoneNumber;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class GooglePushRubricaContactsCommand extends Command
|
||||
{
|
||||
|
|
@ -86,8 +88,10 @@ public function handle(): int
|
|||
$errors = 0;
|
||||
|
||||
foreach ($records as $record) {
|
||||
$email = trim((string) ($record->email ?? ''));
|
||||
$phone = $this->mobileForGoogle($record) ?? '';
|
||||
$emails = $this->emailsForGoogle($record);
|
||||
$phones = $this->phonesForGoogle($record);
|
||||
$email = $emails[0]['value'] ?? '';
|
||||
$phone = $phones[0]['value'] ?? '';
|
||||
$phoneNormalized = $this->normalizePhone($phone);
|
||||
|
||||
[$givenName, $familyName, $fullName] = $this->buildGoogleNameParts($record);
|
||||
|
|
@ -100,11 +104,11 @@ public function handle(): int
|
|||
]],
|
||||
];
|
||||
|
||||
if ($email !== '') {
|
||||
$payload['emailAddresses'] = [['value' => $email]];
|
||||
if ($emails !== []) {
|
||||
$payload['emailAddresses'] = $emails;
|
||||
}
|
||||
if ($phone !== '') {
|
||||
$payload['phoneNumbers'] = [['value' => $phone, 'type' => 'mobile', 'formattedType' => 'Cellulare']];
|
||||
if ($phones !== []) {
|
||||
$payload['phoneNumbers'] = $phones;
|
||||
}
|
||||
|
||||
$resourceName = null;
|
||||
|
|
@ -282,16 +286,19 @@ private function loadGoogleContactIndex(Amministratore $amministratore, array $g
|
|||
|
||||
foreach ($connections as $person) {
|
||||
$resourceName = trim((string) Arr::get($person, 'resourceName', ''));
|
||||
$email = trim((string) Arr::get($person, 'emailAddresses.0.value', ''));
|
||||
$phone = trim((string) Arr::get($person, 'phoneNumbers.0.value', ''));
|
||||
|
||||
if ($email !== '' && $resourceName !== '') {
|
||||
$existingEmails[strtolower($email)] = $resourceName;
|
||||
foreach ((array) Arr::get($person, 'emailAddresses', []) as $emailRow) {
|
||||
$email = trim((string) Arr::get($emailRow, 'value', ''));
|
||||
if ($email !== '' && $resourceName !== '') {
|
||||
$existingEmails[strtolower($email)] = $resourceName;
|
||||
}
|
||||
}
|
||||
if ($phone !== '' && $resourceName !== '') {
|
||||
$normalized = $this->normalizePhone($phone);
|
||||
if ($normalized !== '') {
|
||||
$existingPhones[$normalized] = $resourceName;
|
||||
foreach ((array) Arr::get($person, 'phoneNumbers', []) as $phoneRow) {
|
||||
$phone = trim((string) Arr::get($phoneRow, 'value', ''));
|
||||
if ($phone !== '' && $resourceName !== '') {
|
||||
$normalized = $this->normalizePhone($phone);
|
||||
if ($normalized !== '') {
|
||||
$existingPhones[$normalized] = $resourceName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -408,6 +415,126 @@ private function mobileForGoogle(RubricaUniversale $record): ?string
|
|||
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}
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\Amministratore;
|
||||
use App\Models\RubricaContattoCanale;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Support\PhoneNumber;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class GoogleSyncRubricaContactsCommand extends Command
|
||||
{
|
||||
|
|
@ -194,9 +196,13 @@ public function handle(): int
|
|||
$existing->modificato_da = $adminUserId;
|
||||
}
|
||||
$existing->save();
|
||||
$this->syncAdditionalGoogleChannels($existing, $person);
|
||||
}
|
||||
$updated++;
|
||||
} else {
|
||||
if (! $dryRun) {
|
||||
$this->syncAdditionalGoogleChannels($existing, $person);
|
||||
}
|
||||
$skipped++;
|
||||
}
|
||||
|
||||
|
|
@ -208,7 +214,7 @@ public function handle(): int
|
|||
continue;
|
||||
}
|
||||
|
||||
RubricaUniversale::create([
|
||||
$createdRubrica = RubricaUniversale::create([
|
||||
'nome' => $nome,
|
||||
'cognome' => $cognome,
|
||||
'tipo_contatto' => $tipoContatto,
|
||||
|
|
@ -225,6 +231,7 @@ public function handle(): int
|
|||
'creato_da' => $adminUserId > 0 ? $adminUserId : null,
|
||||
'modificato_da' => $adminUserId > 0 ? $adminUserId : null,
|
||||
]);
|
||||
$this->syncAdditionalGoogleChannels($createdRubrica, $person);
|
||||
}
|
||||
|
||||
$nextPageToken = $response->json('nextPageToken');
|
||||
|
|
@ -400,4 +407,66 @@ private function shouldSyncField(?string $current, ?string $incoming, bool $isGo
|
|||
|
||||
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}
|
||||
{--no-views : Non ricrea le viste QA}
|
||||
{--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}
|
||||
';
|
||||
|
||||
|
|
@ -80,6 +81,70 @@ private function scopeCondominQuery($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.
|
||||
private const BOOLEAN_TARGET_KEYS = [
|
||||
'stabile.ascensore',
|
||||
|
|
@ -526,13 +591,7 @@ private function stepUnita(?int $limit): int
|
|||
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
|
||||
$offset = 0;
|
||||
while (true) {
|
||||
$q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'));
|
||||
if ($this->option('stabile')) {
|
||||
$q->where('cod_stabile', $this->option('stabile'));
|
||||
}
|
||||
if ($this->option('scala')) {
|
||||
$q->where('scala', $this->option('scala'));
|
||||
}
|
||||
$q = $this->currentSnapshotCondominQuery();
|
||||
$q->limit($batch)->offset($offset);
|
||||
$rows = $q->get();
|
||||
if ($rows->isEmpty()) {
|
||||
|
|
@ -577,8 +636,14 @@ private function stepUnita(?int $limit): int
|
|||
} else {
|
||||
$rowStabileId = $this->stabileId;
|
||||
}
|
||||
$interno = $r->interno ?? null;
|
||||
$sub = $r->sub ?? $interno;
|
||||
$interno = isset($r->interno) ? trim((string) $r->interno) : null;
|
||||
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
|
||||
$internoStr = is_string($interno) ? trim($interno) : (string) $interno;
|
||||
$ptype = null;
|
||||
|
|
@ -614,12 +679,37 @@ private function stepUnita(?int $limit): int
|
|||
$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) {
|
||||
// Codice unita coerente: STAB-SCALA-INT (per cantine: STAB-SCALA-CAN<n>)
|
||||
$intPart = $ptype && $pcode ? $pcode : (($interno ?? null) ?: '0');
|
||||
$exists = null;
|
||||
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';
|
||||
// Se stabili-per-scala, includi comunque la scala nel codice per leggibilità
|
||||
$codice = $codiceBaseStab . '-' . ($scalaVal ?: 'A') . '-' . $intPart;
|
||||
$codice = $intPart !== null
|
||||
? $this->buildLegacyUnitCode($codiceBaseStab, $scalaVal ?: 'A', (string) $intPart, $legacyCondId)
|
||||
: (string) ($exists->codice_unita ?? '');
|
||||
// Filtri post-normalizzazione: pertinenze e piano min/max
|
||||
if ($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) {
|
||||
continue;
|
||||
}
|
||||
$exists = null;
|
||||
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) {
|
||||
if (! $exists && $codice !== '') {
|
||||
$exists = DB::table('unita_immobiliari')->where('codice_unita', $codice)->first();
|
||||
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) {
|
||||
$codice = $codice . '-C' . $legacyCondId;
|
||||
$codice = $this->buildLegacyUnitCode($codiceBaseStab, $scalaVal ?: 'A', (string) $intPart, 'C' . $legacyCondId);
|
||||
$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;
|
||||
}
|
||||
}
|
||||
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 (Schema::hasColumn('unita_immobiliari', 'palazzina')) {
|
||||
if (empty($exists->palazzina) || $exists->palazzina !== $scalaVal) {
|
||||
|
|
@ -692,8 +808,8 @@ private function stepUnita(?int $limit): int
|
|||
'palazzina' => $this->splitMode === 'palazzine' ? $scalaVal : null,
|
||||
'scala' => $scalaVal,
|
||||
'piano' => $pianoVal,
|
||||
'interno' => (string) ($ptype && $pcode ? $pcode : $interno),
|
||||
'subalterno' => (string) $sub,
|
||||
'interno' => $normalizedInterno,
|
||||
'subalterno' => $sub,
|
||||
'categoria_catastale' => null,
|
||||
'classe' => null,
|
||||
'consistenza' => null,
|
||||
|
|
@ -788,8 +904,7 @@ private function stepUnita(?int $limit): int
|
|||
|
||||
// Cleanup: rimuovi unità non presenti in condomin (evita "alieni")
|
||||
if (! $this->isDryRun && $this->option('stabile')) {
|
||||
$condRows = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'))
|
||||
->where('cod_stabile', $this->option('stabile'))
|
||||
$condRows = $this->currentSnapshotCondominQuery()
|
||||
->select('scala', 'interno', 'cod_cond')
|
||||
->get();
|
||||
$condSet = [];
|
||||
|
|
@ -920,13 +1035,7 @@ private function stepSoggetti(?int $limit): int
|
|||
$batch = $limit ? min(max(1, $limit), 1000) : 1000;
|
||||
$offset = 0;
|
||||
while (true) {
|
||||
$q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'));
|
||||
if ($this->option('stabile')) {
|
||||
$q->where('cod_stabile', $this->option('stabile'));
|
||||
}
|
||||
if ($this->option('scala')) {
|
||||
$q->where('scala', $this->option('scala'));
|
||||
}
|
||||
$q = $this->currentSnapshotCondominQuery();
|
||||
$q->limit($batch)->offset($offset);
|
||||
$rows = $q->get();
|
||||
if ($rows->isEmpty()) {
|
||||
|
|
@ -1050,13 +1159,7 @@ private function stepDiritti(?int $limit): int
|
|||
return $value ?? '';
|
||||
};
|
||||
while (true) {
|
||||
$q = $this->scopeCondominQuery(DB::connection('gescon_import')->table('condomin'));
|
||||
if ($this->option('stabile')) {
|
||||
$q->where('cod_stabile', $this->option('stabile'));
|
||||
}
|
||||
if ($this->option('scala')) {
|
||||
$q->where('scala', $this->option('scala'));
|
||||
}
|
||||
$q = $this->currentSnapshotCondominQuery();
|
||||
$q->limit($batch)->offset($offset);
|
||||
$rows = $q->get();
|
||||
if ($rows->isEmpty()) {
|
||||
|
|
@ -1262,10 +1365,7 @@ private function stepDiritti(?int $limit): int
|
|||
|
||||
// Import comproprietari (se presenti)
|
||||
if (Schema::connection('gescon_import')->hasTable('comproprietari')) {
|
||||
$q = DB::connection('gescon_import')->table('comproprietari');
|
||||
if ($this->option('stabile')) {
|
||||
$q->where('cod_stabile', $this->option('stabile'));
|
||||
}
|
||||
$q = $this->currentSnapshotComproprietariQuery();
|
||||
|
||||
$comprRows = $q->limit(50000)->get();
|
||||
foreach ($comprRows as $cr) {
|
||||
|
|
@ -1970,8 +2070,10 @@ private function stepMillesimi(?int $limit): int
|
|||
}
|
||||
$affected = $detailsInserted + $detailsUpdated;
|
||||
|
||||
// 4) Completa righe mancanti: ogni unità deve avere una riga per ogni tabella
|
||||
if ($detailsTable === 'dettaglio_millesimi' && Schema::hasTable('unita_immobiliari')) {
|
||||
// 4) Completa righe mancanti solo se richiesto esplicitamente.
|
||||
// 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');
|
||||
$unitaIds = DB::table('unita_immobiliari')
|
||||
->where('stabile_id', $this->stabileId)
|
||||
|
|
@ -2166,6 +2268,19 @@ private function stepVoci(?int $limit): int
|
|||
$tabellaQuery->where('anno_gestione', $annoGestione);
|
||||
}
|
||||
$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);
|
||||
$data = [
|
||||
|
|
@ -3762,7 +3877,9 @@ private function preflightStaging(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$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.
|
||||
if (! $this->option('mdb-condomin')) {
|
||||
|
|
@ -3783,12 +3900,10 @@ private function preflightStaging(): void
|
|||
));
|
||||
}
|
||||
|
||||
if ($legacyYearOpt === null) {
|
||||
if (! empty($resolved['anno_o'])) {
|
||||
$legacyYearOpt = (string) $resolved['anno_o'];
|
||||
} elseif (! empty($resolved['legacy_year'])) {
|
||||
$legacyYearOpt = (string) $resolved['legacy_year'];
|
||||
}
|
||||
if (! empty($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;
|
||||
}
|
||||
|
||||
$needsLoad = $this->stagingTableNeedsLoadForStabile('condomin', $stab);
|
||||
$needsLoad = $this->stagingTableNeedsLoadForStabile('condomin', $stab, $legacyYearOpt);
|
||||
if ($needsLoad) {
|
||||
$mdbCondomin = $this->option('mdb-condomin');
|
||||
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.
|
||||
|
||||
// Carica tabelle aggiuntive utili (millesimi, voci) se vuote per lo stabile
|
||||
|
|
@ -3828,7 +3963,7 @@ private function preflightStaging(): void
|
|||
if ($mdbCondomin && is_file($mdbCondomin)) {
|
||||
// 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) {
|
||||
$this->callSilently('gescon:load-mdb', [
|
||||
'--mdb' => $mdbCondomin,
|
||||
|
|
@ -3840,7 +3975,7 @@ private function preflightStaging(): void
|
|||
}
|
||||
// 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) {
|
||||
$this->callSilently('gescon:load-mdb', [
|
||||
'--mdb' => $mdbCondomin,
|
||||
|
|
@ -3852,7 +3987,7 @@ private function preflightStaging(): void
|
|||
}
|
||||
// voc_spe (voci spesa)
|
||||
if (Schema::connection('gescon_import')->hasTable('voc_spe')) {
|
||||
$need = $this->stagingTableNeedsLoadForStabile('voc_spe', $stab);
|
||||
$need = $this->stagingTableNeedsLoadForStabile('voc_spe', $stab, $legacyYearOpt);
|
||||
if ($need) {
|
||||
$this->callSilently('gescon:load-mdb', [
|
||||
'--mdb' => $mdbCondomin,
|
||||
|
|
@ -3864,7 +3999,7 @@ private function preflightStaging(): void
|
|||
}
|
||||
// comproprietari
|
||||
if (Schema::connection('gescon_import')->hasTable('comproprietari')) {
|
||||
$need = $this->stagingTableNeedsLoadForStabile('comproprietari', $stab);
|
||||
$need = $this->stagingTableNeedsLoadForStabile('comproprietari', $stab, $legacyYearOpt);
|
||||
if ($need) {
|
||||
$this->callSilently('gescon:load-mdb', [
|
||||
'--mdb' => $mdbCondomin,
|
||||
|
|
@ -3876,7 +4011,7 @@ private function preflightStaging(): void
|
|||
}
|
||||
// Cre_Deb_preced (crediti/debiti pregressi)
|
||||
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) {
|
||||
$this->callSilently('gescon:load-mdb', [
|
||||
'--mdb' => $mdbCondomin,
|
||||
|
|
@ -3898,7 +4033,7 @@ private function preflightStaging(): void
|
|||
];
|
||||
foreach ($more as $tb) {
|
||||
if (Schema::connection('gescon_import')->hasTable($tb)) {
|
||||
$need = $this->stagingTableNeedsLoadForStabile($tb, $stab);
|
||||
$need = $this->stagingTableNeedsLoadForStabile($tb, $stab, $legacyYearOpt);
|
||||
if ($need) {
|
||||
$this->callSilently('gescon:load-mdb', [
|
||||
'--mdb' => $mdbCondomin,
|
||||
|
|
@ -4021,7 +4156,7 @@ private function resolveCondominMdbForStabile(string $stab, ?string $legacyYear
|
|||
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)) {
|
||||
return false;
|
||||
|
|
@ -4029,7 +4164,12 @@ private function stagingTableNeedsLoadForStabile(string $table, string $stab): b
|
|||
|
||||
$conn = DB::connection('gescon_import');
|
||||
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;
|
||||
|
|
@ -4243,9 +4383,24 @@ private function findUnitaByStagingRow($r)
|
|||
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 = [];
|
||||
if ($this->stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
|
||||
$crit['stabile_id'] = $this->stabileId;
|
||||
if ($stabileId !== null && Schema::hasColumn('unita_immobiliari', 'stabile_id')) {
|
||||
$crit['stabile_id'] = $stabileId;
|
||||
}
|
||||
if (Schema::hasColumn('unita_immobiliari', 'scala') && isset($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)) {
|
||||
$int = 'CAN' . $m[1];
|
||||
}
|
||||
if (($int === null || trim($int) === '') && isset($r->cod_cond)) {
|
||||
$int = (string) $r->cod_cond;
|
||||
}
|
||||
if ($int !== null) {
|
||||
$int = trim($int);
|
||||
}
|
||||
if ($int !== null && $int !== '' && ! preg_match('/^0+$/', $int)) {
|
||||
$crit['interno'] = $int;
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
@ -4719,6 +4877,35 @@ private function generateStableCondKey(string $prefix, $stabile, $cond, int $len
|
|||
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
|
||||
{
|
||||
return ++$this->protocolCounter;
|
||||
|
|
@ -5111,13 +5298,16 @@ private function buildComproprietariHistoryPayloads(array $historyRows, ?string
|
|||
|
||||
$currentNameKey = $this->normalizeLegacyPartyName($current['nominativo'] ?? null);
|
||||
$currentRightKey = $this->normalizeLegacyPartyName($current['diritto_reale'] ?? $current['diritto_label'] ?? null);
|
||||
$sameName = $snapshotNameKey !== '' && $snapshotNameKey === $currentNameKey;
|
||||
$sameRight = $snapshotRightKey === $currentRightKey;
|
||||
$samePercent = $snapshot['percentuale'] === null || $current['percentuale'] === null
|
||||
$sameSeries = ! empty($snapshot['id_compr']) && ! empty($current['id_compr'])
|
||||
&& (int) $snapshot['id_compr'] === (int) $current['id_compr'];
|
||||
$sameName = $snapshotNameKey !== '' && $snapshotNameKey === $currentNameKey;
|
||||
$sameRight = $snapshotRightKey === $currentRightKey;
|
||||
$samePercent = $snapshot['percentuale'] === null || $current['percentuale'] === null
|
||||
|| 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['percentuale'] = $current['percentuale'] ?? $snapshot['percentuale'];
|
||||
if (! empty($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))
|
||||
->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')
|
||||
->join('unita_immobiliare_nominativi as b', function ($join) : void {
|
||||
$join->on('a.unita_immobiliare_id', '=', 'b.unita_immobiliare_id')
|
||||
|
|
@ -81,9 +85,27 @@ public function handle(): int
|
|||
->whereColumn('a.id', '<', 'b.id');
|
||||
})
|
||||
->join('unita_immobiliari as ui', 'ui.id', '=', 'a.unita_immobiliare_id')
|
||||
->where(function ($q): void {
|
||||
$q->whereNull('a.data_fine')->orWhereNull('b.data_inizio')->orWhereColumn('a.data_fine', '>=', 'b.data_inizio');
|
||||
->whereIn('a.fonte', ['legacy_condomin', 'legacy_comproprietari'])
|
||||
->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))
|
||||
->count()
|
||||
: 0;
|
||||
|
|
@ -116,7 +138,8 @@ public function handle(): int
|
|||
$this->line('Unita senza storico nominativi: ' . $unitaSenzaNominativi);
|
||||
$this->line('Ruoli con stabile mismatch: ' . $mismatchRuoliStabile . ($applyFix ? ' (fix applicato)' : ''));
|
||||
$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)) {
|
||||
$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.');
|
||||
return self::INVALID;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,10 +323,10 @@ private function hydrateCollegamentoWorkspace(): void
|
|||
{
|
||||
$suggestion = $this->resolveCollegamentoSuggerito();
|
||||
|
||||
$this->collegamentoSuggerito = $suggestion;
|
||||
$this->collegamentoStabileId = ! empty($suggestion['stabile_id']) ? (int) $suggestion['stabile_id'] : null;
|
||||
$this->collegamentoUnitaId = ! empty($suggestion['unita_id']) ? (int) $suggestion['unita_id'] : null;
|
||||
$this->collegamentoRuolo = ! empty($suggestion['role_standard']) ? (string) $suggestion['role_standard'] : 'altro';
|
||||
$this->collegamentoSuggerito = $suggestion;
|
||||
$this->collegamentoStabileId = ! empty($suggestion['stabile_id']) ? (int) $suggestion['stabile_id'] : null;
|
||||
$this->collegamentoUnitaId = ! empty($suggestion['unita_id']) ? (int) $suggestion['unita_id'] : null;
|
||||
$this->collegamentoRuolo = ! empty($suggestion['role_standard']) ? (string) $suggestion['role_standard'] : 'altro';
|
||||
$this->collegamentoRuoloCustom = ! empty($suggestion['role_custom']) ? (string) $suggestion['role_custom'] : '';
|
||||
}
|
||||
|
||||
|
|
@ -353,15 +353,15 @@ public function collegaCollegamentoSuggerito(): void
|
|||
public function salvaCollegamentoManuale(): void
|
||||
{
|
||||
$data = validator([
|
||||
'stabile_id' => $this->collegamentoStabileId,
|
||||
'stabile_id' => $this->collegamentoStabileId,
|
||||
'unita_immobiliare_id' => $this->collegamentoUnitaId,
|
||||
'ruolo_standard' => $this->collegamentoRuolo,
|
||||
'ruolo_custom' => $this->collegamentoRuoloCustom,
|
||||
'ruolo_standard' => $this->collegamentoRuolo,
|
||||
'ruolo_custom' => $this->collegamentoRuoloCustom,
|
||||
], [
|
||||
'stabile_id' => ['required', 'integer'],
|
||||
'stabile_id' => ['required', 'integer'],
|
||||
'unita_immobiliare_id' => ['nullable', 'integer'],
|
||||
'ruolo_standard' => ['required', 'string', 'max:120'],
|
||||
'ruolo_custom' => ['nullable', 'string', 'max:255'],
|
||||
'ruolo_standard' => ['required', 'string', 'max:120'],
|
||||
'ruolo_custom' => ['nullable', 'string', 'max:255'],
|
||||
])->validate();
|
||||
|
||||
$this->persistCollegamentoRubrica(
|
||||
|
|
@ -390,7 +390,7 @@ private function persistCollegamentoRubrica(
|
|||
return;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$user = Auth::user();
|
||||
$isSuperAdmin = $user instanceof User && method_exists($user, 'hasRole')
|
||||
? (bool) $user->hasRole('super-admin')
|
||||
: false;
|
||||
|
|
@ -423,12 +423,12 @@ private function persistCollegamentoRubrica(
|
|||
}
|
||||
|
||||
$ruoloStandard = trim($ruoloStandard) !== '' ? trim($ruoloStandard) : 'altro';
|
||||
$ruoloCustom = trim($ruoloCustom);
|
||||
$ruoloCustom = trim($ruoloCustom);
|
||||
|
||||
$attributes = [
|
||||
'rubrica_id' => (int) $this->rubrica->id,
|
||||
'ruolo_standard' => $ruoloStandard,
|
||||
'stabile_id' => (int) $stabile->id,
|
||||
'rubrica_id' => (int) $this->rubrica->id,
|
||||
'ruolo_standard' => $ruoloStandard,
|
||||
'stabile_id' => (int) $stabile->id,
|
||||
'unita_immobiliare_id' => $unita?->id,
|
||||
];
|
||||
|
||||
|
|
@ -446,11 +446,11 @@ private function persistCollegamentoRubrica(
|
|||
|
||||
$payload = [
|
||||
'ruolo_custom' => $ruoloCustom !== '' ? $ruoloCustom : null,
|
||||
'is_attivo' => true,
|
||||
'is_attivo' => true,
|
||||
'is_preferito' => $existing ? (bool) ($existing->is_preferito ?? false) : count($this->ruoliAttivi) === 0,
|
||||
'data_inizio' => $existing?->data_inizio ?: now()->toDateString(),
|
||||
'meta' => array_filter([
|
||||
'source' => $source,
|
||||
'data_inizio' => $existing?->data_inizio ?: now()->toDateString(),
|
||||
'meta' => array_filter([
|
||||
'source' => $source,
|
||||
'linked_from' => 'rubrica_scheda',
|
||||
], fn($value) => $value !== null && $value !== ''),
|
||||
];
|
||||
|
|
@ -463,12 +463,12 @@ private function persistCollegamentoRubrica(
|
|||
}
|
||||
|
||||
$this->rubrica->riferimento_stabile = $this->buildLocalStabileReference($stabile);
|
||||
$this->rubrica->riferimento_unita = $unita ? $this->buildLocalUnitReference($unita) : $this->rubrica->riferimento_unita;
|
||||
$this->rubrica->riferimento_unita = $unita ? $this->buildLocalUnitReference($unita) : $this->rubrica->riferimento_unita;
|
||||
if (in_array($ruoloStandard, ['condomino', 'inquilino', 'fornitore', 'amministratore', 'altro'], true)) {
|
||||
$this->rubrica->tipo_utenza_call = $ruoloStandard;
|
||||
}
|
||||
$this->rubrica->data_ultima_modifica = now()->toDateString();
|
||||
$this->rubrica->modificato_da = Auth::id();
|
||||
$this->rubrica->modificato_da = Auth::id();
|
||||
$this->rubrica->save();
|
||||
|
||||
$this->mount((int) $this->rubrica->id);
|
||||
|
|
@ -483,7 +483,7 @@ private function persistCollegamentoRubrica(
|
|||
/** @return array<string, mixed> */
|
||||
private function resolveCollegamentoSuggerito(): array
|
||||
{
|
||||
$legacy = $this->parseCollegamentoLegacyMeta((string) ($this->rubrica->note ?? ''));
|
||||
$legacy = $this->parseCollegamentoLegacyMeta((string) ($this->rubrica->note ?? ''));
|
||||
$stabile = $this->resolveCollegamentoStabile($legacy['cod_stabile'] ?? null, (string) ($this->rubrica->riferimento_stabile ?? ''));
|
||||
if (empty($stabile['id'])) {
|
||||
return [];
|
||||
|
|
@ -496,14 +496,14 @@ private function resolveCollegamentoSuggerito(): array
|
|||
);
|
||||
|
||||
return array_filter([
|
||||
'stabile_id' => (int) $stabile['id'],
|
||||
'stabile_label' => $this->buildCollegamentoStabileReference($stabile),
|
||||
'unita_id' => (int) ($unit['unit_id'] ?? 0) ?: null,
|
||||
'unita_label' => (string) ($unit['unit_label'] ?? ''),
|
||||
'stabile_id' => (int) $stabile['id'],
|
||||
'stabile_label' => $this->buildCollegamentoStabileReference($stabile),
|
||||
'unita_id' => (int) ($unit['unit_id'] ?? 0) ?: null,
|
||||
'unita_label' => (string) ($unit['unit_label'] ?? ''),
|
||||
'unita_reference' => (string) ($unit['unit_reference'] ?? ''),
|
||||
'role_standard' => $this->mapCollegamentoRole($legacy['ruolo'] ?? $this->rubrica->tipo_utenza_call),
|
||||
'role_custom' => null,
|
||||
'source' => (string) ($unit['source'] ?? 'riferimento_stabile'),
|
||||
'role_standard' => $this->mapCollegamentoRole($legacy['ruolo'] ?? $this->rubrica->tipo_utenza_call),
|
||||
'role_custom' => null,
|
||||
'source' => (string) ($unit['source'] ?? 'riferimento_stabile'),
|
||||
'requires_review' => (bool) ($unit['source'] ?? false),
|
||||
], fn($value) => $value !== null && $value !== '');
|
||||
}
|
||||
|
|
@ -513,8 +513,8 @@ private function parseCollegamentoLegacyMeta(string $note): array
|
|||
{
|
||||
$payload = [
|
||||
'cod_stabile' => null,
|
||||
'cod_cond' => null,
|
||||
'ruolo' => null,
|
||||
'cod_cond' => null,
|
||||
'ruolo' => null,
|
||||
];
|
||||
|
||||
foreach (array_keys($payload) as $key) {
|
||||
|
|
@ -538,7 +538,7 @@ private function resolveCollegamentoStabile(?string $legacyCode, string $referen
|
|||
}
|
||||
|
||||
$trimmed = ltrim($code, '0');
|
||||
$row = Stabile::query()
|
||||
$row = Stabile::query()
|
||||
->when($this->adminId > 0, fn(Builder $query) => $query->where('amministratore_id', $this->adminId))
|
||||
->where(function (Builder $query) use ($code, $trimmed): void {
|
||||
$query->where('codice_stabile', $code)
|
||||
|
|
@ -554,10 +554,10 @@ private function resolveCollegamentoStabile(?string $legacyCode, string $referen
|
|||
|
||||
if ($row) {
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'id' => (int) $row->id,
|
||||
'codice_stabile' => $row->codice_stabile ? (string) $row->codice_stabile : null,
|
||||
'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null,
|
||||
'denominazione' => $row->denominazione ? (string) $row->denominazione : null,
|
||||
'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null,
|
||||
'denominazione' => $row->denominazione ? (string) $row->denominazione : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -574,10 +574,10 @@ private function resolveCollegamentoStabile(?string $legacyCode, string $referen
|
|||
$row = $rows->first();
|
||||
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'id' => (int) $row->id,
|
||||
'codice_stabile' => $row->codice_stabile ? (string) $row->codice_stabile : null,
|
||||
'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null,
|
||||
'denominazione' => $row->denominazione ? (string) $row->denominazione : null,
|
||||
'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null,
|
||||
'denominazione' => $row->denominazione ? (string) $row->denominazione : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -602,10 +602,10 @@ private function resolveCollegamentoUnita(int $stabileId, array $legacy, string
|
|||
$row = $rows->first();
|
||||
|
||||
return [
|
||||
'unit_id' => (int) $row->id,
|
||||
'unit_label' => $this->buildLocalUnitLabel($row),
|
||||
'unit_id' => (int) $row->id,
|
||||
'unit_label' => $this->buildLocalUnitLabel($row),
|
||||
'unit_reference' => $this->buildLocalUnitReference($row),
|
||||
'source' => 'riferimento_unita',
|
||||
'source' => 'riferimento_unita',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -623,8 +623,8 @@ private function resolveCollegamentoUnita(int $stabileId, array $legacy, string
|
|||
foreach ($rows as $row) {
|
||||
$unit = $this->resolveUnitFromImportRow($stabileId, [
|
||||
'nom_cond' => (string) ($row->nom_cond ?? ''),
|
||||
'scala' => (string) ($row->scala ?? ''),
|
||||
'interno' => (string) ($row->interno ?? ''),
|
||||
'scala' => (string) ($row->scala ?? ''),
|
||||
'interno' => (string) ($row->interno ?? ''),
|
||||
]);
|
||||
|
||||
if (! $unit) {
|
||||
|
|
@ -632,10 +632,10 @@ private function resolveCollegamentoUnita(int $stabileId, array $legacy, string
|
|||
}
|
||||
|
||||
$candidates[(int) $unit->unit_id] = [
|
||||
'unit_id' => (int) $unit->unit_id,
|
||||
'unit_label' => $this->buildLocalUnitLabel($unit),
|
||||
'unit_id' => (int) $unit->unit_id,
|
||||
'unit_label' => $this->buildLocalUnitLabel($unit),
|
||||
'unit_reference' => $this->buildLocalUnitReference($unit),
|
||||
'source' => 'gescon_import',
|
||||
'source' => 'gescon_import',
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -690,7 +690,7 @@ private function buildCollegamentoStabileReference(array $stabile): string
|
|||
|
||||
private function buildLocalUnitReference(object $unit): string
|
||||
{
|
||||
$code = trim((string) data_get($unit, 'codice_unita', ''));
|
||||
$code = trim((string) data_get($unit, 'codice_unita', ''));
|
||||
$label = trim((string) data_get($unit, 'denominazione', ''));
|
||||
|
||||
return trim($code . ($label !== '' ? ' - ' . $label : ''));
|
||||
|
|
@ -715,11 +715,11 @@ private function mapCollegamentoRole(?string $value): ?string
|
|||
return match ($normalized) {
|
||||
'c', 'condomino', 'proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'titolare' => 'condomino',
|
||||
'i', 'inquilino', 'conduttore', 'locatario' => 'inquilino',
|
||||
'fornitore' => 'fornitore',
|
||||
'fornitore' => 'fornitore',
|
||||
'amministratore' => 'amministratore',
|
||||
'collaboratore' => 'collaboratore',
|
||||
'altro' => 'altro',
|
||||
default => null,
|
||||
'collaboratore' => 'collaboratore',
|
||||
'altro' => 'altro',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -729,7 +729,7 @@ private function resolveUnitFromImportRow(int $stabileId, array $row): ?object
|
|||
->where('stabile_id', $stabileId)
|
||||
->select(['id as unit_id', 'codice_unita', 'denominazione', 'scala', 'interno']);
|
||||
|
||||
$scala = trim((string) ($row['scala'] ?? ''));
|
||||
$scala = trim((string) ($row['scala'] ?? ''));
|
||||
$interno = trim((string) ($row['interno'] ?? ''));
|
||||
|
||||
if ($scala !== '' || $interno !== '') {
|
||||
|
|
@ -770,7 +770,7 @@ private function hasGesconImportCondomin(): bool
|
|||
|
||||
private function normalizeCollegamentoText(string $value): string
|
||||
{
|
||||
$upper = mb_strtoupper(trim($value));
|
||||
$upper = mb_strtoupper(trim($value));
|
||||
$normalized = preg_replace('/[^A-Z0-9]+/u', '', $upper);
|
||||
|
||||
return is_string($normalized) ? $normalized : '';
|
||||
|
|
@ -1102,7 +1102,7 @@ protected function getHeaderActions(): array
|
|||
->send();
|
||||
}),
|
||||
Action::make('contatti')
|
||||
->label('Contatti')
|
||||
->label('Modifica anagrafica')
|
||||
->icon('heroicon-o-phone')
|
||||
->action(function (): void {
|
||||
$this->startInlineEdit();
|
||||
|
|
@ -1158,7 +1158,7 @@ protected function getHeaderActions(): array
|
|||
Action::make('contatti_avanzati')
|
||||
->label('Contatti avanzati')
|
||||
->icon('heroicon-o-rectangle-stack')
|
||||
->visible(fn(): bool => Schema::hasTable('rubrica_contatto_canali'))
|
||||
->visible(fn(): bool => Schema::hasTable('rubrica_contatti_canali'))
|
||||
->modalWidth('7xl')
|
||||
->form([
|
||||
Repeater::make('canali')
|
||||
|
|
@ -2098,6 +2098,47 @@ protected function getTitoliOptions(): array
|
|||
->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> */
|
||||
public function getUnitaOptions($stabileId): array
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
use App\Models\ChiamataPostIt;
|
||||
use App\Models\Fornitore;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
|
|
@ -496,21 +497,7 @@ private function creaTicketDirettoDaForm(): void
|
|||
|
||||
private function findRubricaByPhone(string $value): ?RubricaUniversale
|
||||
{
|
||||
$normalized = PhoneNumber::normalizeForMatch($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();
|
||||
return app(RubricaPhoneLookupService::class)->findByPhone($value);
|
||||
}
|
||||
|
||||
private function createPostItRecord(): ChiamataPostIt
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use App\Models\RubricaUniversale;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||
use BackedEnum;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
|
|
@ -65,9 +66,12 @@ class PostItGestione extends Page
|
|||
/** @var array<string, array{id:int|null,name:string|null}> */
|
||||
private array $pbxExtensionCache = [];
|
||||
|
||||
/** @var array<int, CommunicationMessage|null> */
|
||||
/** @var array<int|string, CommunicationMessage|null> */
|
||||
private array $postItMessageCache = [];
|
||||
|
||||
/** @var array<int,string> */
|
||||
private array $monitoredResponseGroups = ['601', '603'];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$focus = (int) request()->query('focus_post_it', 0);
|
||||
|
|
@ -325,19 +329,19 @@ public function createRubricaDraftFromGroup(string $modalKey, string $mode = 'op
|
|||
|
||||
$rubrica = RubricaUniversale::query()->create([
|
||||
'amministratore_id' => $adminId > 0 ? $adminId : null,
|
||||
'nome' => $identity['nome'],
|
||||
'cognome' => $identity['cognome'],
|
||||
'ragione_sociale' => $identity['ragione_sociale'],
|
||||
'tipo_contatto' => $identity['tipo_contatto'],
|
||||
'telefono_cellulare' => $this->isLikelyMobilePhone($phoneDigits) ? $phoneDigits : null,
|
||||
'telefono_ufficio' => $this->isLikelyMobilePhone($phoneDigits) ? null : $phoneDigits,
|
||||
'categoria' => 'altro',
|
||||
'stato' => 'attivo',
|
||||
'data_inserimento' => now()->toDateString(),
|
||||
'data_ultima_modifica'=> now()->toDateString(),
|
||||
'creato_da' => Auth::id(),
|
||||
'modificato_da' => Auth::id(),
|
||||
'note_segreteria' => 'Bozza creata da Lavagna chiamate per gestire richiamo e identificazione del contatto.',
|
||||
'nome' => $identity['nome'],
|
||||
'cognome' => $identity['cognome'],
|
||||
'ragione_sociale' => $identity['ragione_sociale'],
|
||||
'tipo_contatto' => $identity['tipo_contatto'],
|
||||
'telefono_cellulare' => $this->isLikelyMobilePhone($phoneDigits) ? $phoneDigits : null,
|
||||
'telefono_ufficio' => $this->isLikelyMobilePhone($phoneDigits) ? null : $phoneDigits,
|
||||
'categoria' => 'altro',
|
||||
'stato' => 'attivo',
|
||||
'data_inserimento' => now()->toDateString(),
|
||||
'data_ultima_modifica' => now()->toDateString(),
|
||||
'creato_da' => Auth::id(),
|
||||
'modificato_da' => Auth::id(),
|
||||
'note_segreteria' => 'Bozza creata da Lavagna chiamate per gestire richiamo e identificazione del contatto.',
|
||||
]);
|
||||
|
||||
$itemIds = collect($group['items'])
|
||||
|
|
@ -686,17 +690,17 @@ private function buildTicketDescriptionFromPostIt(int $postItId, ChiamataPostIt
|
|||
|
||||
private function buildGroupedPostItKey(ChiamataPostIt $postIt): string
|
||||
{
|
||||
$rubricaId = $this->resolveGroupedRubricaId($postIt);
|
||||
if ($rubricaId !== null) {
|
||||
return 'rubrica:' . $rubricaId;
|
||||
}
|
||||
|
||||
$phone = $this->normalizeGroupedPhone((string) ($postIt->telefono ?? ''));
|
||||
if ($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
|
||||
|
|
@ -734,21 +738,38 @@ private function isLegacyFilteredSmdrPostIt(ChiamataPostIt $postIt): bool
|
|||
|
||||
private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage
|
||||
{
|
||||
$originId = (int) ($postIt->origine_id ?? 0);
|
||||
if ($originId <= 0) {
|
||||
$originKey = trim((string) ($postIt->origine_id ?? ''));
|
||||
if ($originKey === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (array_key_exists($originId, $this->postItMessageCache)) {
|
||||
return $this->postItMessageCache[$originId];
|
||||
if (array_key_exists($originKey, $this->postItMessageCache)) {
|
||||
return $this->postItMessageCache[$originKey];
|
||||
}
|
||||
|
||||
$message = CommunicationMessage::query()->find($originId);
|
||||
$this->postItMessageCache[$originId] = $message;
|
||||
$message = null;
|
||||
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;
|
||||
}
|
||||
|
||||
private function normalizeGroupedCallerKey(string $value): string
|
||||
{
|
||||
$normalized = preg_replace('/\s+/', ' ', trim(mb_strtolower($value))) ?? '';
|
||||
|
||||
return $normalized !== '' ? $normalized : 'sconosciuto';
|
||||
}
|
||||
|
||||
private function normalizeGroupedPhone(string $phone): string
|
||||
{
|
||||
$digits = preg_replace('/\D+/', '', $phone) ?: '';
|
||||
|
|
@ -851,22 +872,22 @@ private function buildGroupedCollection($items)
|
|||
return ($right->chiamata_il?->getTimestamp() ?? 0) <=> ($left->chiamata_il?->getTimestamp() ?? 0);
|
||||
});
|
||||
|
||||
$group['latest'] = $this->resolveLatestGroupedPostIt($group['items']);
|
||||
$group['latest'] = $this->resolveLatestGroupedPostIt($group['items']);
|
||||
$group['primary_post_it'] = $this->resolvePrimaryGroupedPostIt($group['items']);
|
||||
$oldest = collect($group['items'])
|
||||
$oldest = collect($group['items'])
|
||||
->sortBy(fn(ChiamataPostIt $item) => $item->chiamata_il?->getTimestamp() ?? 0)
|
||||
->first();
|
||||
$group['first_at'] = optional($oldest?->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
||||
$group['last_at'] = optional($group['latest']->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
||||
$group['caller_label'] = $this->resolveGroupedCallerLabel($group['items']);
|
||||
$group['phone'] = $this->resolveGroupedPhoneLabel($group['items']);
|
||||
$group['rubrica_url'] = $this->getRubricaUrlByPhone((string) $group['phone']);
|
||||
$group['answered_count']= collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isAnsweredPostIt($item))->count();
|
||||
$group['missed_count'] = collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isMissedPostIt($item))->count();
|
||||
$group['can_create_rubrica'] = ! $group['rubrica_url'] && $this->canCreateRubricaFromPhone((string) $group['phone']);
|
||||
$group['first_at'] = optional($oldest?->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
||||
$group['last_at'] = optional($group['latest']->chiamata_il)->format('d/m/Y H:i') ?: '-';
|
||||
$group['caller_label'] = $this->resolveGroupedCallerLabel($group['items']);
|
||||
$group['phone'] = $this->resolveGroupedPhoneLabel($group['items']);
|
||||
$group['rubrica_url'] = $this->getRubricaUrlByPhone((string) $group['phone']);
|
||||
$group['answered_count'] = collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isAnsweredPostIt($item))->count();
|
||||
$group['missed_count'] = collect($group['items'])->filter(fn(ChiamataPostIt $item): bool => $this->isMissedPostIt($item))->count();
|
||||
$group['can_create_rubrica'] = ! $group['rubrica_url'] && $this->canCreateRubricaFromPhone((string) $group['phone']);
|
||||
$group['primary_action_label'] = $group['answered_count'] > 0 ? 'Apri Post-it' : 'Apri richiamo';
|
||||
$group['visible_items'] = collect($group['items'])->take(4)->values()->all();
|
||||
$group['hidden_count'] = max(0, count($group['items']) - count($group['visible_items']));
|
||||
$group['visible_items'] = collect($group['items'])->take(4)->values()->all();
|
||||
$group['hidden_count'] = max(0, count($group['items']) - count($group['visible_items']));
|
||||
|
||||
return $group;
|
||||
})
|
||||
|
|
@ -1024,25 +1045,8 @@ private function resolveRubricaIdByPhone(?string $phone): ?int
|
|||
}
|
||||
|
||||
$adminId = (int) (Auth::user()?->amministratore?->id ?? 0);
|
||||
|
||||
$query = RubricaUniversale::query();
|
||||
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;
|
||||
$result = app(RubricaPhoneLookupService::class)->findByPhone($digits, $adminId)?->id;
|
||||
$result = is_numeric($result) ? (int) $result : null;
|
||||
$this->rubricaPhoneCache[$digits] = $result;
|
||||
|
||||
return $result;
|
||||
|
|
@ -1095,6 +1099,10 @@ private function normalizePostItOutcome(?string $outcome): string
|
|||
|
||||
public function isAnsweredPostIt(ChiamataPostIt $postIt): bool
|
||||
{
|
||||
if ($this->isMissedResponseGroupPostIt($postIt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
||||
|
||||
if (($postIt->durata_secondi ?? 0) > 0) {
|
||||
|
|
@ -1106,6 +1114,10 @@ public function isAnsweredPostIt(ChiamataPostIt $postIt): bool
|
|||
|
||||
private function isMissedPostIt(ChiamataPostIt $postIt): bool
|
||||
{
|
||||
if ($this->isMissedResponseGroupPostIt($postIt)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$outcome = $this->normalizePostItOutcome($postIt->esito);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
if ($this->isMissedPostIt($postIt)) {
|
||||
|
|
|
|||
|
|
@ -834,7 +834,7 @@ public function hydrateNominativiStorici(): void
|
|||
})->all();
|
||||
}
|
||||
|
||||
private function hydrateRecapitiServizio(): void
|
||||
public function hydrateRecapitiServizio(): void
|
||||
{
|
||||
$this->recapitiServizio = [];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use App\Models\CommunicationMessage;
|
||||
use App\Models\PbxClickToCallRequest;
|
||||
use App\Models\RubricaUniversale;
|
||||
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||
use App\Services\Cti\PbxRoutingService;
|
||||
use App\Support\PhoneNumber;
|
||||
use Carbon\CarbonInterface;
|
||||
|
|
@ -491,25 +492,7 @@ private function mirrorToPeerIfEnabled(Request $request, string $endpoint, array
|
|||
|
||||
private function findRubricaByPhone(string $phone): ?RubricaUniversale
|
||||
{
|
||||
$digits = PhoneNumber::normalizeDigits($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();
|
||||
return app(RubricaPhoneLookupService::class)->findByPhone($phone);
|
||||
}
|
||||
|
||||
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\Soggetto;
|
||||
use App\Models\User;
|
||||
use App\Services\Anagrafiche\RubricaPhoneLookupService;
|
||||
|
||||
class LiveIncomingCallService
|
||||
{
|
||||
|
|
@ -43,20 +44,20 @@ public function getForUser(User $user): ?array
|
|||
|
||||
$latest = $candidates
|
||||
->sortByDesc(function (CommunicationMessage $message): int {
|
||||
$metadata = is_array($message->metadata) ? $message->metadata : [];
|
||||
$eventType = strtolower(trim((string) ($metadata['event_type'] ?? '')));
|
||||
$endedAt = trim((string) ($metadata['ended_at'] ?? ''));
|
||||
$metadata = is_array($message->metadata) ? $message->metadata : [];
|
||||
$eventType = strtolower(trim((string) ($metadata['event_type'] ?? '')));
|
||||
$endedAt = trim((string) ($metadata['ended_at'] ?? ''));
|
||||
$isActiveCsta = (string) $message->channel === 'panasonic_csta'
|
||||
&& (string) ($message->status ?? '') !== 'completed'
|
||||
&& $endedAt === ''
|
||||
&& ! in_array($eventType, ['connectioncleared', 'cleared', 'released'], true);
|
||||
&& (string) ($message->status ?? '') !== 'completed'
|
||||
&& $endedAt === ''
|
||||
&& ! in_array($eventType, ['connectioncleared', 'cleared', 'released'], true);
|
||||
|
||||
if ($isActiveCsta) {
|
||||
return 5000000000 + (int) ($message->received_at?->getTimestamp() ?? 0);
|
||||
}
|
||||
|
||||
$isRecentCsta = (string) $message->channel === 'panasonic_csta'
|
||||
&& (int) ($message->received_at?->getTimestamp() ?? 0) >= now()->subSeconds(45)->getTimestamp();
|
||||
&& (int) ($message->received_at?->getTimestamp() ?? 0) >= now()->subSeconds(45)->getTimestamp();
|
||||
|
||||
if ($isRecentCsta) {
|
||||
return 4000000000 + (int) ($message->received_at?->getTimestamp() ?? 0);
|
||||
|
|
@ -119,25 +120,7 @@ private function normalizePhoneDigits(string $value): string
|
|||
|
||||
private function matchRubricaByPhone(string $digits): ?RubricaUniversale
|
||||
{
|
||||
if ($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();
|
||||
return app(RubricaPhoneLookupService::class)->findByPhone($digits);
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
- anno corrente legacy piu recente disponibile
|
||||
|
|
@ -122,6 +199,9 @@ ### E. QA dominio post-import
|
|||
- stabile creato/aggiornato correttamente
|
||||
- unita importate
|
||||
- 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
|
||||
- nessun crash nelle pagine operative principali
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ # Modifiche NetGescon
|
|||
|
||||
| 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 | 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] |
|
||||
|
|
|
|||
|
|
@ -1,17 +1,28 @@
|
|||
## 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).
|
||||
- Carica lo staging `gescon_import.condomin` con mdb-tools:
|
||||
`php artisan gescon:load-mdb --mdb="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --table=condomin --stabile=0021 --anno=0003 --truncate`
|
||||
`php artisan gescon:load-mdb --mdb="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --table=condomin --stabile=0021 --anno=0003 --truncate`
|
||||
- Reimporta le sole unità:
|
||||
`php artisan gescon:import-full --stabile=0021 --anno=3 --mdb-condomin="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --solo=unita`
|
||||
`php artisan gescon:import-full --stabile=0021 --anno=3 --mdb-condomin="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --solo=unita`
|
||||
- Prosegui con soggetti, diritti, millesimi (dopo l’import unità):
|
||||
- `php artisan gescon:import-full --stabile=0021 --anno=3 --mdb-condomin="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --solo=soggetti`
|
||||
- `php artisan gescon:import-full --stabile=0021 --anno=3 --mdb-condomin="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --solo=diritti`
|
||||
- `php artisan gescon:import-full --stabile=0021 --anno=3 --mdb-condomin="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --solo=millesimi`
|
||||
- `php artisan gescon:import-full --stabile=0021 --anno=3 --mdb-condomin="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --solo=soggetti`
|
||||
- `php artisan gescon:import-full --stabile=0021 --anno=3 --mdb-condomin="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --solo=diritti`
|
||||
- `php artisan gescon:import-full --stabile=0021 --anno=3 --mdb-condomin="/mnt/gescon-archives/gescon/0021/0003/singolo_anno.mdb" --solo=millesimi`
|
||||
- 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)
|
||||
|
||||
- `load-mdb condomin`: 268 righe staging
|
||||
- `import-full --solo=unita`: 212 record
|
||||
- `import-full --solo=soggetti`: 55 record
|
||||
|
|
@ -20,22 +31,80 @@ ### 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.
|
||||
|
||||
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
|
||||
### Verifica reale aggiornata (stabile 0021 / snapshot corrente 0004)
|
||||
|
||||
il secondo file è /home/michele/netgescon/docs/02-architettura-laravel/Gescon/dbc/Stabili.mdb qui trovi le indicazioni di tutti gli stabili
|
||||
- `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
|
||||
|
||||
e poi da li trovi tutti gli archivi che sono principalmente 2 dentro le cartelle
|
||||
/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
|
||||
### 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 secondo file è /home/michele/netgescon/docs/02-architettura-laravel/Gescon/dbc/Stabili.mdb qui trovi le indicazioni di tutti gli stabili
|
||||
|
||||
e poi da li trovi tutti gli archivi che sono principalmente 2 dentro le cartelle
|
||||
/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
|
||||
|
||||
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 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.
|
||||
|
||||
|
||||
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
|
||||
$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 {
|
||||
if (! $stabile) {
|
||||
return 'Seleziona stabile';
|
||||
|
|
@ -32,7 +63,7 @@
|
|||
|
||||
$labelGestione = $gestioni[$gestioneAttiva] ?? (string) $gestioneAttiva;
|
||||
|
||||
$haRiscaldamento = (bool) ($stabileAttivo?->riscaldamento_centralizzato ?? false);
|
||||
$haRiscaldamento = $stabileHasRiscaldamento($stabileAttivo);
|
||||
@endphp
|
||||
|
||||
<div class="flex min-w-0 items-center gap-2 ps-3">
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
@if($isInlineEditing)
|
||||
<x-filament::section>
|
||||
<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">
|
||||
<label class="text-sm">
|
||||
|
|
@ -20,11 +20,21 @@
|
|||
|
||||
<label class="text-sm">
|
||||
<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 class="text-sm">
|
||||
<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 class="text-sm">
|
||||
<span class="mb-1 block text-gray-600">Codice fiscale</span>
|
||||
|
|
@ -105,7 +115,12 @@
|
|||
|
||||
<label class="text-sm">
|
||||
<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 class="text-sm">
|
||||
<span class="mb-1 block text-gray-600">Rif. stabile</span>
|
||||
|
|
@ -128,10 +143,12 @@
|
|||
|
||||
<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" 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>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
@else
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
|
|
@ -673,7 +690,15 @@
|
|||
|
||||
<div class="flex flex-col gap-2">
|
||||
<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 type="button" color="gray" wire:click="mountAction('ruoli_legami')">
|
||||
|
|
@ -746,4 +771,5 @@
|
|||
</div>
|
||||
@endif
|
||||
</x-filament::section>
|
||||
@endif
|
||||
</x-filament-panels::page>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user