Supporto/fornitori sync and Google OAuth refresh hardening

This commit is contained in:
michele 2026-04-08 16:04:23 +00:00
parent a9204e125b
commit 432f0565ab
28 changed files with 816 additions and 184 deletions

View File

@ -0,0 +1,399 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class NetgesconQaMillesimiSpeseCommand extends Command
{
protected $signature = 'netgescon:qa-millesimi-spese
{--stabile= : Codice stabile legacy/domino}
{--stabile_id= : ID stabile dominio}
{--anno= : Legacy year staging (es. 0001) per filtrare il confronto}
{--show-missing=20 : Massimo numero di codici mancanti da mostrare}';
protected $description = 'QA su tabelle millesimali, voci spesa e frazionamenti, evitando fallback vuoti e verificando l allineamento con nominativi/unita.';
public function handle(): int
{
if (! Schema::hasTable('stabili')) {
$this->error('Tabella stabili non disponibile.');
return self::FAILURE;
}
[$stabileId, $codiceStabile] = $this->resolveStabileScope();
if (! $stabileId || $codiceStabile === '') {
$this->error('Serve --stabile=CODICE oppure --stabile_id=ID valido.');
return self::FAILURE;
}
$legacyYear = $this->normalizeOption($this->option('anno'));
$showMissing = max(1, (int) ($this->option('show-missing') ?: 20));
$this->info('QA millesimi/spese per stabile ' . $codiceStabile . ' (#' . $stabileId . ')' . ($legacyYear ? ' anno ' . $legacyYear : ''));
$activeUnitsQuery = DB::table('unita_immobiliari')
->where('stabile_id', $stabileId)
->whereNull('deleted_at');
$activeUnits = (clone $activeUnitsQuery)->count();
$unitsWithNominativi = 0;
if (Schema::hasTable('unita_immobiliare_nominativi')) {
$unitsWithNominativi = DB::table('unita_immobiliare_nominativi as uin')
->join('unita_immobiliari as ui', 'ui.id', '=', 'uin.unita_immobiliare_id')
->where('ui.stabile_id', $stabileId)
->whereNull('ui.deleted_at')
->distinct('uin.unita_immobiliare_id')
->count('uin.unita_immobiliare_id');
}
$unitsWithoutNominativi = max(0, $activeUnits - $unitsWithNominativi);
$tabelleCount = Schema::hasTable('tabelle_millesimali')
? DB::table('tabelle_millesimali')->where('stabile_id', $stabileId)->count()
: 0;
$detailRows = 0;
$detailZeroRows = 0;
$detailZeroOnUnitsWithoutNominativi = 0;
$tablesWithoutPositiveRows = collect();
if (Schema::hasTable('dettaglio_millesimi') && Schema::hasTable('tabelle_millesimali')) {
$detailBase = DB::table('dettaglio_millesimi as dm')
->join('tabelle_millesimali as tm', 'tm.id', '=', 'dm.tabella_millesimale_id')
->where('tm.stabile_id', $stabileId);
$detailRows = (clone $detailBase)->count();
$detailZeroRows = (clone $detailBase)
->where(function ($q): void {
$q->whereNull('dm.millesimi')->orWhere('dm.millesimi', '<=', 0);
})
->count();
if (Schema::hasTable('unita_immobiliare_nominativi')) {
$detailZeroOnUnitsWithoutNominativi = (clone $detailBase)
->join('unita_immobiliari as ui', 'ui.id', '=', 'dm.unita_immobiliare_id')
->where(function ($q): void {
$q->whereNull('dm.millesimi')->orWhere('dm.millesimi', '<=', 0);
})
->whereNotExists(function ($sub): void {
$sub->selectRaw('1')
->from('unita_immobiliare_nominativi as uin')
->whereColumn('uin.unita_immobiliare_id', 'dm.unita_immobiliare_id');
})
->count();
}
$tablesWithoutPositiveRows = DB::table('tabelle_millesimali as tm')
->leftJoin('dettaglio_millesimi as dm', function ($join): void {
$join->on('dm.tabella_millesimale_id', '=', 'tm.id')
->where('dm.millesimi', '>', 0);
})
->where('tm.stabile_id', $stabileId)
->groupBy('tm.id', 'tm.codice_tabella', 'tm.nome_tabella', 'tm.denominazione')
->havingRaw('COUNT(dm.id) = 0')
->selectRaw('COALESCE(tm.codice_tabella, tm.nome_tabella, tm.denominazione, CAST(tm.id AS CHAR)) as codice')
->pluck('codice');
}
$vociCount = Schema::hasTable('voci_spesa')
? DB::table('voci_spesa')->where('stabile_id', $stabileId)->count()
: 0;
$vociSenzaTabella = Schema::hasTable('voci_spesa')
? DB::table('voci_spesa')
->where('stabile_id', $stabileId)
->where(function ($q) : void {
$q->whereNull('tabella_millesimale_default_id')->orWhere('tabella_millesimale_default_id', 0);
})
->count()
: 0;
$vociConTabellaOrfana = (Schema::hasTable('voci_spesa') && Schema::hasTable('tabelle_millesimali'))
? DB::table('voci_spesa as vs')
->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'vs.tabella_millesimale_default_id')
->where('vs.stabile_id', $stabileId)
->whereNotNull('vs.tabella_millesimale_default_id')
->whereNull('tm.id')
->count()
: 0;
$staging = $this->collectStagingMetrics($stabileId, $codiceStabile, $legacyYear);
$this->line('Unita attive: ' . $activeUnits);
$this->line('Unita con nominativi: ' . $unitsWithNominativi);
$this->line('Unita senza nominativi: ' . $unitsWithoutNominativi);
$this->line('Tabelle millesimali dominio: ' . $tabelleCount);
$this->line('Righe dettaglio millesimi: ' . $detailRows);
$this->line('Righe dettaglio a zero/null: ' . $detailZeroRows);
$this->line('Righe a zero su unita senza nominativi: ' . $detailZeroOnUnitsWithoutNominativi);
$this->line('Voci spesa dominio: ' . $vociCount);
$this->line('Voci senza tabella default: ' . $vociSenzaTabella);
$this->line('Voci con tabella orfana: ' . $vociConTabellaOrfana);
if ($staging['available']) {
$this->line('Staging tabelle legacy: ' . $staging['headers_count'] . ' intestazioni, ' . $staging['detail_codes_count'] . ' codici con dettagli');
$this->line('Staging voci legacy: ' . $staging['voci_count']);
$this->line('Staging straordinarie legacy: ' . $staging['straord_count']);
$this->line('Staging frazionamenti: Fraz_gen=' . $staging['fraz_gen_count'] . ', fraz_dett=' . $staging['fraz_dett_count']);
} else {
$this->warn('Connessione/tabelle staging non disponibili per il confronto completo.');
}
$exitCode = self::SUCCESS;
if ($tablesWithoutPositiveRows->isNotEmpty()) {
$exitCode = self::INVALID;
$this->warn('Tabelle senza alcuna riga millesimale positiva: ' . $tablesWithoutPositiveRows->take($showMissing)->implode(', '));
}
if ($staging['available']) {
if (! empty($staging['missing_table_codes'])) {
$exitCode = self::INVALID;
$this->warn('Codici tabella presenti in staging ma mancanti nel dominio: ' . implode(', ', array_slice($staging['missing_table_codes'], 0, $showMissing)));
}
if (! empty($staging['missing_voci_codes'])) {
$exitCode = self::INVALID;
$this->warn('Codici voce presenti in staging ma mancanti nel dominio: ' . implode(', ', array_slice($staging['missing_voci_codes'], 0, $showMissing)));
}
if ($staging['fraz_gen_count'] > 0 || $staging['fraz_dett_count'] > 0) {
$this->warn('Archivio frazionamenti legacy presente: verificare Z01/Z02 prima del via libera contabile.');
if (($staging['z_fraz_voci_missing'] ?? []) !== []) {
$exitCode = self::INVALID;
$this->warn('Voci Z con frazionamento presenti in staging ma non allineate nel dominio: ' . implode(', ', array_slice($staging['z_fraz_voci_missing'], 0, $showMissing)));
}
}
}
if ($unitsWithoutNominativi > 0 || $detailZeroOnUnitsWithoutNominativi > 0 || $vociConTabellaOrfana > 0) {
$exitCode = self::INVALID;
}
if ($exitCode === self::SUCCESS) {
$this->info('QA completato senza anomalie bloccanti.');
} else {
$this->warn('QA completato con anomalie da correggere prima del riallineamento definitivo.');
}
return $exitCode;
}
private function resolveStabileScope(): array
{
$stabileId = (int) ($this->option('stabile_id') ?: 0);
if ($stabileId > 0) {
$codice = DB::table('stabili')->where('id', $stabileId)->value('codice_stabile');
return [$stabileId, $this->normalizeOption($codice) ?? ''];
}
$codiceInput = $this->normalizeCode((string) ($this->option('stabile') ?? ''));
if ($codiceInput === '') {
return [0, ''];
}
$trimmed = ltrim($codiceInput, '0');
$stabile = DB::table('stabili')
->where(function ($q) use ($codiceInput, $trimmed): void {
$q->where('codice_stabile', $codiceInput);
if ($trimmed !== '' && $trimmed !== $codiceInput) {
$q->orWhere('codice_stabile', $trimmed);
}
})
->select('id', 'codice_stabile')
->first();
if (! $stabile) {
return [0, ''];
}
return [(int) $stabile->id, (string) $stabile->codice_stabile];
}
private function collectStagingMetrics(int $stabileId, string $codiceStabile, ?string $legacyYear): array
{
$result = [
'available' => false,
'headers_count' => 0,
'detail_codes_count' => 0,
'voci_count' => 0,
'straord_count' => 0,
'fraz_gen_count' => 0,
'fraz_dett_count' => 0,
'missing_table_codes' => [],
'missing_voci_codes' => [],
'z_fraz_voci_missing' => [],
];
if (! config('database.connections.gescon_import')) {
return $result;
}
$result['available'] = true;
$code = $this->normalizeCode($codiceStabile);
$domainTableCodes = Schema::hasTable('tabelle_millesimali')
? DB::table('tabelle_millesimali')
->where('stabile_id', $stabileId)
->selectRaw('UPPER(TRIM(COALESCE(codice_tabella, nome_tabella, denominazione))) as codice')
->pluck('codice')
->filter()
->unique()
->values()
->all()
: [];
$domainVoceCodes = Schema::hasTable('voci_spesa')
? DB::table('voci_spesa')
->where('stabile_id', $stabileId)
->selectRaw('UPPER(TRIM(codice)) as codice')
->pluck('codice')
->filter()
->unique()
->values()
->all()
: [];
if (Schema::hasTable('voci_spesa') && Schema::hasColumn('voci_spesa', 'legacy_codice')) {
$domainLegacyVoceCodes = DB::table('voci_spesa')
->where('stabile_id', $stabileId)
->selectRaw('UPPER(TRIM(legacy_codice)) as codice')
->pluck('codice')
->filter()
->unique()
->values()
->all();
$domainVoceCodes = array_values(array_unique(array_merge($domainVoceCodes, $domainLegacyVoceCodes)));
}
if (Schema::connection('gescon_import')->hasTable('tabelle_millesimali')) {
$headersQuery = DB::connection('gescon_import')->table('tabelle_millesimali');
if (Schema::connection('gescon_import')->hasColumn('tabelle_millesimali', 'cod_stabile')) {
$headersQuery->where('cod_stabile', $code);
}
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('tabelle_millesimali', 'legacy_year')) {
$headersQuery->where('legacy_year', $legacyYear);
}
$headerCodes = $headersQuery
->selectRaw('UPPER(TRIM(COALESCE(cod_tabella, denominazione, descrizione))) as codice')
->pluck('codice')
->filter()
->unique()
->values()
->all();
$result['headers_count'] = count($headerCodes);
$result['missing_table_codes'] = array_values(array_diff($headerCodes, $domainTableCodes));
}
if (Schema::connection('gescon_import')->hasTable('dett_tab')) {
$detailQuery = DB::connection('gescon_import')->table('dett_tab as d');
if (Schema::connection('gescon_import')->hasTable('condomin')) {
$detailQuery->join('condomin as c', 'c.cod_cond', '=', 'd.id_cond')
->where('c.cod_stabile', $code);
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_year')) {
$detailQuery->where('c.legacy_year', $legacyYear);
}
}
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('dett_tab', 'legacy_year')) {
$detailQuery->where('d.legacy_year', $legacyYear);
}
$detailCodes = $detailQuery
->selectRaw('UPPER(TRIM(d.cod_tab)) as codice')
->pluck('codice')
->filter()
->unique()
->values()
->all();
$result['detail_codes_count'] = count($detailCodes);
if ($result['missing_table_codes'] === []) {
$result['missing_table_codes'] = array_values(array_diff($detailCodes, $domainTableCodes));
}
}
if (Schema::connection('gescon_import')->hasTable('voc_spe')) {
$vociQuery = DB::connection('gescon_import')->table('voc_spe');
if (Schema::connection('gescon_import')->hasColumn('voc_spe', 'cod_stabile')) {
$vociQuery->where('cod_stabile', $code);
}
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('voc_spe', 'legacy_year')) {
$vociQuery->where('legacy_year', $legacyYear);
}
$vocCodes = $vociQuery
->selectRaw('UPPER(TRIM(cod)) as codice')
->pluck('codice')
->filter()
->unique()
->values()
->all();
$result['voci_count'] = count($vocCodes);
$result['missing_voci_codes'] = array_values(array_diff($vocCodes, $domainVoceCodes));
$zFractionsQuery = DB::connection('gescon_import')->table('voc_spe')
->where('cod_stabile', $code);
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('voc_spe', 'legacy_year')) {
$zFractionsQuery->where('legacy_year', $legacyYear);
}
$zFractions = $zFractionsQuery
->selectRaw('UPPER(TRIM(cod)) as codice')
->whereIn('cod', ['Z01', 'Z02'])
->pluck('codice')
->filter()
->unique()
->values()
->all();
$result['z_fraz_voci_missing'] = array_values(array_diff($zFractions, $domainVoceCodes));
}
if (Schema::connection('gescon_import')->hasTable('straordinarie')) {
$straordQuery = DB::connection('gescon_import')->table('straordinarie');
if (Schema::connection('gescon_import')->hasColumn('straordinarie', 'cod_stabile')) {
$straordQuery->where('cod_stabile', $code);
}
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('straordinarie', 'legacy_year')) {
$straordQuery->where('legacy_year', $legacyYear);
}
$result['straord_count'] = $straordQuery->count();
}
if (Schema::connection('gescon_import')->hasTable('fraz_gen')) {
$query = DB::connection('gescon_import')->table('fraz_gen');
if (Schema::connection('gescon_import')->hasColumn('fraz_gen', 'cod_stabile')) {
$query->where('cod_stabile', $code);
}
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('fraz_gen', 'legacy_year')) {
$query->where('legacy_year', $legacyYear);
}
$result['fraz_gen_count'] = $query->count();
}
if (Schema::connection('gescon_import')->hasTable('fraz_dett')) {
$query = DB::connection('gescon_import')->table('fraz_dett');
if ($legacyYear !== null && Schema::connection('gescon_import')->hasColumn('fraz_dett', 'legacy_year')) {
$query->where('legacy_year', $legacyYear);
}
$result['fraz_dett_count'] = $query->count();
}
return $result;
}
private function normalizeOption(mixed $value): ?string
{
if (! is_string($value) && ! is_numeric($value)) {
return null;
}
$normalized = trim((string) $value);
return $normalized === '' ? null : $normalized;
}
private function normalizeCode(string $code): string
{
$code = trim($code);
if ($code === '') {
return '';
}
if (is_numeric($code)) {
return str_pad((string) ((int) $code), 4, '0', STR_PAD_LEFT);
}
return strtoupper($code);
}
}

View File

@ -198,7 +198,7 @@ public function handle(): int
'source_port' => $port, 'source_port' => $port,
'assigned_user_id' => $assignedUserId, 'assigned_user_id' => $assignedUserId,
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
'amministratore_id'=> $amministratoreId, 'amministratore_id' => $amministratoreId,
'studio_role' => $routing['studio_role'], 'studio_role' => $routing['studio_role'],
'studio_mode' => $routing['studio_mode'], 'studio_mode' => $routing['studio_mode'],
'smdr' => [ 'smdr' => [
@ -245,6 +245,8 @@ public function handle(): int
try { try {
$lineSuffix = ! empty($parsed['co']) ? ' - linea ' . (string) $parsed['co'] : ''; $lineSuffix = ! empty($parsed['co']) ? ' - linea ' . (string) $parsed['co'] : '';
$isMissedResponseGroup = $this->isMissedResponseGroupCall($parsed, $routing);
ChiamataPostIt::query()->create([ ChiamataPostIt::query()->create([
'telefono' => $phone, 'telefono' => $phone,
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
@ -253,7 +255,7 @@ public function handle(): int
'oggetto' => 'Chiamata in entrata da SMDR' . $lineSuffix, 'oggetto' => 'Chiamata in entrata da SMDR' . $lineSuffix,
'nota' => $line, 'nota' => $line,
'direzione' => 'in_arrivo', 'direzione' => 'in_arrivo',
'esito' => ($durationSeconds ?? 0) > 0 ? 'answered' : 'missed', 'esito' => $isMissedResponseGroup || ($durationSeconds ?? 0) <= 0 ? 'missed' : 'answered',
'origine' => 'smdr', 'origine' => 'smdr',
'origine_id' => $fingerprint, 'origine_id' => $fingerprint,
'durata_secondi' => $durationSeconds, 'durata_secondi' => $durationSeconds,
@ -518,4 +520,18 @@ private function shouldCreateOperationalPostIt(array $parsed): bool
return ! empty($parsed['co']); return ! empty($parsed['co']);
} }
/** @param array<string,mixed> $parsed */
private function isMissedResponseGroupCall(array $parsed, array $routing): bool
{
$groupExtensions = ['601', '603'];
$groupExtension = preg_replace('/\D+/', '', (string) ($parsed['extension'] ?? '')) ?: '';
if (! in_array($groupExtension, $groupExtensions, true)) {
return false;
}
$targetExtension = preg_replace('/\D+/', '', (string) ($routing['target_extension'] ?? '')) ?: '';
return $targetExtension === '' || in_array($targetExtension, $groupExtensions, true);
}
} }

View File

@ -0,0 +1,103 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class SyncGesconFornitoriLegacyCommand extends Command
{
protected $signature = 'gescon:sync-fornitori-legacy
{amministratore_id : ID amministratore a cui associare i fornitori}
{--mdb=/mnt/gescon-archives/gescon/dbc/Fornitori.mdb : Percorso file Fornitori.mdb}
{--table=Fornitori : Nome tabella Access}
{--truncate-staging : Svuota la tabella staging prima dell\'import}
{--skip-tags : Salta l\'aggiornamento tag legacy}
{--dry-run : Esegue preview staging e dry-run dell\'import locale senza scrivere}
{--preview-limit=10 : Limite righe preview staging in dry-run}';
protected $description = 'Esegue la sync completa fornitori legacy -> staging -> archivio locale -> tag legacy.';
public function handle(): int
{
$adminId = (int) $this->argument('amministratore_id');
$mdb = (string) $this->option('mdb');
$table = (string) $this->option('table');
$dryRun = (bool) $this->option('dry-run');
$skipTags = (bool) $this->option('skip-tags');
$truncateStaging = (bool) $this->option('truncate-staging');
$previewLimit = max(1, (int) ($this->option('preview-limit') ?: 10));
if ($adminId <= 0) {
$this->error('amministratore_id non valido.');
return self::FAILURE;
}
$this->info('Sync fornitori legacy per amministratore #' . $adminId . ($dryRun ? ' [DRY-RUN]' : ''));
$stageParams = [
'tenant' => (string) $adminId,
'--mdb' => $mdb,
'--table' => $table,
];
if ($dryRun) {
$stageParams['--preview'] = true;
$stageParams['--limit'] = $previewLimit;
}
if ($truncateStaging && ! $dryRun) {
$stageParams['--truncate'] = true;
}
$stageExit = Artisan::call('import:gescon-fornitori', $stageParams);
$stageOutput = trim((string) Artisan::output());
$this->line('Step staging exit=' . $stageExit);
if ($stageOutput !== '') {
$this->line($stageOutput);
}
if ($stageExit !== 0) {
return self::FAILURE;
}
$importParams = [
'amministratore_id' => (string) $adminId,
'--mdb' => $mdb,
'--table' => $table,
];
if ($dryRun) {
$importParams['--dry-run'] = true;
}
$importExit = Artisan::call('gescon:import-fornitori-legacy', $importParams);
$importOutput = trim((string) Artisan::output());
$this->line('Step archivio locale exit=' . $importExit);
if ($importOutput !== '') {
$this->line($importOutput);
}
if ($importExit !== 0) {
return self::FAILURE;
}
if (! $skipTags) {
$tagParams = [];
if ($dryRun) {
$tagParams['--dry-run'] = true;
}
$tagExit = Artisan::call('fornitori:import-legacy-tags', $tagParams);
$tagOutput = trim((string) Artisan::output());
$this->line('Step tag legacy exit=' . $tagExit);
if ($tagOutput !== '') {
$this->line($tagOutput);
}
if ($tagExit !== 0) {
return self::FAILURE;
}
}
$this->info($dryRun
? 'Dry-run sync fornitori legacy completato.'
: 'Sync fornitori legacy completato.');
return self::SUCCESS;
}
}

View File

@ -106,11 +106,9 @@ public function refreshRows(): void
$this->rows = $products->map(function (Product $product): array { $this->rows = $products->map(function (Product $product): array {
$identifier = $product->identifiers $identifier = $product->identifiers
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitoreId) ->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitoreId) ?? $product->identifiers->first();
?? $product->identifiers->first();
$offer = $product->offers $offer = $product->offers
->first(fn(ProductOffer $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitoreId) ->first(fn(ProductOffer $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitoreId) ?? $product->offers->first();
?? $product->offers->first();
return [ return [
'id' => (int) $product->id, 'id' => (int) $product->id,

View File

@ -57,7 +57,7 @@ class TicketInterventoScheda extends Page
'sorgente' => '', 'sorgente' => '',
'riferimento' => '', 'riferimento' => '',
'rubrica_url' => null, 'rubrica_url' => null,
'estratto_url'=> null, 'estratto_url' => null,
'stabile_url' => null, 'stabile_url' => null,
'unita_label' => '', 'unita_label' => '',
]; ];
@ -1101,8 +1101,8 @@ protected function resolveCallerData(?Ticket $ticket): array
'sorgente' => 'Descrizione ticket', 'sorgente' => 'Descrizione ticket',
'riferimento' => $this->buildCallerReference($ticket, (string) ($parsed['riferimento'] ?? '')), 'riferimento' => $this->buildCallerReference($ticket, (string) ($parsed['riferimento'] ?? '')),
'rubrica_url' => null, 'rubrica_url' => null,
'estratto_url'=> null, 'estratto_url' => null,
'stabile_url' => $ticket?->stabile ? StabileScheda::getUrl(['record' => (int) $ticket->stabile->id], panel: 'admin-filament') : null, 'stabile_url' => $ticket?->stabile ? StabileScheda::getUrl(['record' => (int) $ticket->stabile->id], panel : 'admin-filament'): null,
'unita_label' => $ticket?->unitaImmobiliare ? $this->formatUnitaLabel($ticket->unitaImmobiliare) : '', 'unita_label' => $ticket?->unitaImmobiliare ? $this->formatUnitaLabel($ticket->unitaImmobiliare) : '',
]; ];

View File

@ -717,29 +717,23 @@ public function aggiornaFornitoriGescon(): void
} }
try { try {
$exit1 = Artisan::call('import:gescon-fornitori', [ $exit = Artisan::call('gescon:sync-fornitori-legacy', [
'tenant' => (string) $amm->id,
'--mdb' => $mdb,
]);
$out1 = trim((string) Artisan::output());
$exit2 = Artisan::call('gescon:import-fornitori-legacy', [
'amministratore_id' => (string) $amm->id, 'amministratore_id' => (string) $amm->id,
'--mdb' => $mdb, '--mdb' => $mdb,
]); ]);
$out2 = trim((string) Artisan::output()); $out = trim((string) Artisan::output());
} catch (\Throwable $e) { } catch (\Throwable $e) {
Notification::make()->title('Aggiornamento fornitori fallito')->body($e->getMessage())->danger()->send(); Notification::make()->title('Aggiornamento fornitori fallito')->body($e->getMessage())->danger()->send();
return; return;
} }
if ($exit1 !== 0 || $exit2 !== 0) { if ($exit !== 0) {
$snippet = $out2 !== '' ? $out2 : $out1; $snippet = $out !== '' ? $out : '';
$snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : ''; $snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : '';
Notification::make() Notification::make()
->title('Aggiornamento fornitori: errore') ->title('Aggiornamento fornitori: errore')
->body('Exit staging=' . $exit1 . ' | exit import=' . $exit2 . ($snippet !== '' ? "\n\n" . $snippet : '')) ->body('Exit sync=' . $exit . ($snippet !== '' ? "\n\n" . $snippet : ''))
->danger() ->danger()
->send(); ->send();
return; return;
@ -792,24 +786,12 @@ public function refreshImportTagLegacyFornitoriGlobale(): void
} }
try { try {
$exit1 = Artisan::call('import:gescon-fornitori', [ $exit = Artisan::call('gescon:sync-fornitori-legacy', [
'tenant' => (string) $amm->id,
'--mdb' => $mdb,
]);
$out1 = trim((string) Artisan::output());
$exit2 = Artisan::call('gescon:import-fornitori-legacy', [
'amministratore_id' => (string) $amm->id, 'amministratore_id' => (string) $amm->id,
'--mdb' => $mdb, '--mdb' => $mdb,
'--dry-run' => $dryRun,
]); ]);
$out2 = trim((string) Artisan::output()); $out = trim((string) Artisan::output());
$tagParams = [];
if ($dryRun) {
$tagParams['--dry-run'] = true;
}
$exit3 = Artisan::call('fornitori:import-legacy-tags', $tagParams);
$out3 = trim((string) Artisan::output());
} catch (\Throwable $e) { } catch (\Throwable $e) {
Notification::make() Notification::make()
->title('Refresh + import TAG fallito') ->title('Refresh + import TAG fallito')
@ -819,13 +801,13 @@ public function refreshImportTagLegacyFornitoriGlobale(): void
return; return;
} }
if ($exit1 !== 0 || $exit2 !== 0 || $exit3 !== 0) { if ($exit !== 0) {
$snippet = $out3 !== '' ? $out3 : ($out2 !== '' ? $out2 : $out1); $snippet = $out !== '' ? $out : '';
$snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : ''; $snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : '';
Notification::make() Notification::make()
->title('Refresh + import TAG: errore') ->title('Refresh + import TAG: errore')
->body('Exit staging=' . $exit1 . ' | import=' . $exit2 . ' | tags=' . $exit3 . ($snippet !== '' ? "\n\n" . $snippet : '')) ->body('Exit sync=' . $exit . ($snippet !== '' ? "\n\n" . $snippet : ''))
->danger() ->danger()
->send(); ->send();
return; return;
@ -1013,22 +995,16 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
if (! empty($options['with_operazioni'])) { if (! empty($options['with_operazioni'])) {
try { try {
$exit1 = Artisan::call('import:gescon-fornitori', [ $exit = Artisan::call('gescon:sync-fornitori-legacy', [
'tenant' => (string) $amm->id,
'--mdb' => (string) ($state['fornitori_mdb'] ?? rtrim($options['path'], '/') . '/dbc/Fornitori.mdb'),
]);
$outputs[] = trim((string) Artisan::output());
$exit2 = Artisan::call('gescon:import-fornitori-legacy', [
'amministratore_id' => (string) $amm->id, 'amministratore_id' => (string) $amm->id,
'--mdb' => (string) ($state['fornitori_mdb'] ?? rtrim($options['path'], '/') . '/dbc/Fornitori.mdb'), '--mdb' => (string) ($state['fornitori_mdb'] ?? rtrim($options['path'], '/') . '/dbc/Fornitori.mdb'),
]); ]);
$outputs[] = trim((string) Artisan::output()); $outputs[] = trim((string) Artisan::output());
if ($exit1 !== 0 || $exit2 !== 0) { if ($exit !== 0) {
Notification::make() Notification::make()
->title('Aggiornamento fornitori: errore') ->title('Aggiornamento fornitori: errore')
->body('Exit staging=' . $exit1 . ' | exit import=' . $exit2) ->body('Exit sync=' . $exit)
->danger() ->danger()
->send(); ->send();
return; return;

View File

@ -612,7 +612,7 @@ public function creaTicketRapido(): void
$ticket = Ticket::query()->create([ $ticket = Ticket::query()->create([
'stabile_id' => $stabileId, 'stabile_id' => $stabileId,
'unita_immobiliare_id'=> $callerContext['unita_id'] ?? null, 'unita_immobiliare_id' => $callerContext['unita_id'] ?? null,
'soggetto_richiedente_id' => $callerContext['soggetto_id'] ?? null, 'soggetto_richiedente_id' => $callerContext['soggetto_id'] ?? null,
'aperto_da_user_id' => $user->id, 'aperto_da_user_id' => $user->id,
'titolo' => (string) $this->newTicketTitolo, 'titolo' => (string) $this->newTicketTitolo,
@ -1213,7 +1213,7 @@ private function resolveCallerTicketContext(?RubricaUniversale $caller, ?int $fa
return [ return [
'stabile_id' => $stabile?->id, 'stabile_id' => $stabile?->id,
'stabile_label' => $stabile ? (string) ($stabile->denominazione ?: ('Stabile #' . $stabile->id)) : '', 'stabile_label' => $stabile ? (string) ($stabile->denominazione ?: ('Stabile #' . $stabile->id)): '',
'soggetto_id' => $soggetto?->id, 'soggetto_id' => $soggetto?->id,
'unita_id' => $unita?->id, 'unita_id' => $unita?->id,
'unita_label' => $unita ? $this->formatUnitaLabel($unita) : '', 'unita_label' => $unita ? $this->formatUnitaLabel($unita) : '',

View File

@ -35,6 +35,7 @@ public function redirect(): RedirectResponse
} }
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []); $google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$oauth = Arr::get($google, 'oauth', []);
$clientId = (string) ($google['client_id'] ?? config('services.google.client_id')); $clientId = (string) ($google['client_id'] ?? config('services.google.client_id'));
$clientSecret = (string) ($google['client_secret'] ?? config('services.google.client_secret')); $clientSecret = (string) ($google['client_secret'] ?? config('services.google.client_secret'));
@ -48,16 +49,21 @@ public function redirect(): RedirectResponse
$state = bin2hex(random_bytes(20)); $state = bin2hex(random_bytes(20));
session()->put('google_oauth_state', $state); session()->put('google_oauth_state', $state);
$query = http_build_query([ $queryParams = [
'client_id' => $clientId, 'client_id' => $clientId,
'redirect_uri' => $redirectUri, 'redirect_uri' => $redirectUri,
'response_type' => 'code', 'response_type' => 'code',
'scope' => implode(' ', self::GOOGLE_SCOPES), 'scope' => implode(' ', self::GOOGLE_SCOPES),
'access_type' => 'offline', 'access_type' => 'offline',
'prompt' => 'consent',
'include_granted_scopes' => 'true', 'include_granted_scopes' => 'true',
'state' => $state, 'state' => $state,
]); ];
if (trim((string) Arr::get($oauth, 'refresh_token', '')) === '') {
$queryParams['prompt'] = 'consent';
}
$query = http_build_query($queryParams);
return redirect()->away('https://accounts.google.com/o/oauth2/v2/auth?' . $query); return redirect()->away('https://accounts.google.com/o/oauth2/v2/auth?' . $query);
} }
@ -71,6 +77,7 @@ public function callback(): RedirectResponse
} }
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []); $google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
$existingOauth = Arr::get($google, 'oauth', []);
$clientId = (string) ($google['client_id'] ?? config('services.google.client_id')); $clientId = (string) ($google['client_id'] ?? config('services.google.client_id'));
$clientSecret = (string) ($google['client_secret'] ?? config('services.google.client_secret')); $clientSecret = (string) ($google['client_secret'] ?? config('services.google.client_secret'));
$redirectUri = (string) ($google['redirect_uri'] ?? config('services.google.redirect')); $redirectUri = (string) ($google['redirect_uri'] ?? config('services.google.redirect'));
@ -106,6 +113,9 @@ public function callback(): RedirectResponse
$payload = $tokenResponse->json(); $payload = $tokenResponse->json();
$accessToken = (string) Arr::get($payload, 'access_token', ''); $accessToken = (string) Arr::get($payload, 'access_token', '');
$refreshToken = (string) Arr::get($payload, 'refresh_token', ''); $refreshToken = (string) Arr::get($payload, 'refresh_token', '');
if ($refreshToken === '') {
$refreshToken = (string) Arr::get($existingOauth, 'refresh_token', '');
}
$expiresIn = (int) Arr::get($payload, 'expires_in', 3600); $expiresIn = (int) Arr::get($payload, 'expires_in', 3600);
$profileResponse = Http::withToken($accessToken) $profileResponse = Http::withToken($accessToken)
@ -128,6 +138,7 @@ public function callback(): RedirectResponse
'refresh_token' => $refreshToken, 'refresh_token' => $refreshToken,
'expires_in' => $expiresIn, 'expires_in' => $expiresIn,
'connected_at' => now()->toDateTimeString(), 'connected_at' => now()->toDateTimeString(),
'refreshed_at' => now()->toDateTimeString(),
], ],
]); ]);

View File

@ -119,7 +119,7 @@ public function resolveByIncomingLine(?string $line): array
if ($mappedUser instanceof User) { if ($mappedUser instanceof User) {
$targetRouting = [ $targetRouting = [
'user_id' => (int) $mappedUser->id, 'user_id' => (int) $mappedUser->id,
'stabile_id' => method_exists($mappedUser, 'stabiliAssegnati') ? ((int) ($mappedUser->stabiliAssegnati()->value('stabili.id') ?? 0) ?: null) : null, 'stabile_id' => method_exists($mappedUser, 'stabiliAssegnati') ? ((int) ($mappedUser->stabiliAssegnati()->value('stabili.id') ?? 0) ?: null): null,
'user' => $mappedUser, 'user' => $mappedUser,
]; ];
} }

View File

@ -3,6 +3,7 @@ # NetGescon — TODO Copilot Tracker
Data aggiornamento: 6 aprile 2026 Data aggiornamento: 6 aprile 2026
Questo file tiene traccia di: Questo file tiene traccia di:
- cosa è stato fatto (implementato e/o corretto) - cosa è stato fatto (implementato e/o corretto)
- cosa resta da fare - cosa resta da fare
- priorità e dipendenze - priorità e dipendenze
@ -12,22 +13,27 @@ # NetGescon — TODO Copilot Tracker
## 1) Fatto (stato attuale) ## 1) Fatto (stato attuale)
### UX / Filament (navigazione “web-like”) ### UX / Filament (navigazione “web-like”)
- Breadcrumbs compatibili Filament: wrapper view per `<x-filament.components.page-breadcrumbs>`. - Breadcrumbs compatibili Filament: wrapper view per `<x-filament.components.page-breadcrumbs>`.
- Pagine Filament con back/breadcrumb coerenti (dove già applicato). - Pagine Filament con back/breadcrumb coerenti (dove già applicato).
### Anagrafica ### Anagrafica
- `Persona`: gestione multi-ruolo via JSON `persone.ruoli` + UI di editing in Filament. - `Persona`: gestione multi-ruolo via JSON `persone.ruoli` + UI di editing in Filament.
### Contabilità — base ### Contabilità — base
- Tabelle contabilità e migrazioni sbloccate (incl. `contabilita_regole_prima_nota`, movimenti banca). - Tabelle contabilità e migrazioni sbloccate (incl. `contabilita_regole_prima_nota`, movimenti banca).
- Import movimenti banca UniCredit `.wri` con dedup (`row_hash`). - Import movimenti banca UniCredit `.wri` con dedup (`row_hash`).
- Azione Filament su movimenti banca per generare registrazione/movimenti contabili da regole prima nota (scaffold funzionante). - Azione Filament su movimenti banca per generare registrazione/movimenti contabili da regole prima nota (scaffold funzionante).
### Fatture ricevute XML (FatturaPA) ### Fatture ricevute XML (FatturaPA)
- Archivio + scheda Filament per fatture ricevute XML. - Archivio + scheda Filament per fatture ricevute XML.
- Import multiplo XML con dedup (hash + chiave logica) e persistenza su `fatture_elettroniche`. - Import multiplo XML con dedup (hash + chiave logica) e persistenza su `fatture_elettroniche`.
### Scheda Stabile (riabilitata) ### Scheda Stabile (riabilitata)
- Ripristinato accesso dalla Filament “Cruscotto stabile” alla scheda stabile completa (UI classica) con: - Ripristinato accesso dalla Filament “Cruscotto stabile” alla scheda stabile completa (UI classica) con:
- Dati stabile - Dati stabile
- Banche/dati finanziari - Banche/dati finanziari
@ -37,10 +43,12 @@ ### Scheda Stabile (riabilitata)
- Aggiunto deep-link `?tab=...` sulla scheda stabile UI classica per aprire direttamente una tab specifica. - Aggiunto deep-link `?tab=...` sulla scheda stabile UI classica per aprire direttamente una tab specifica.
### “Vista Excel” (riabilitata) ### “Vista Excel” (riabilitata)
- Ripristinato accesso immediato dalla Filament “Cruscotto stabile” alla tab “Tabelle millesimali” (UI classica) tramite deep-link. - Ripristinato accesso immediato dalla Filament “Cruscotto stabile” alla tab “Tabelle millesimali” (UI classica) tramite deep-link.
- Esiste anche la Filament “Tabelle millesimali · Prospetto” che è già una vista tabellare/Excel-like. - Esiste anche la Filament “Tabelle millesimali · Prospetto” che è già una vista tabellare/Excel-like.
### Nuovo lavoro già avviato (base dati + UI) ### Nuovo lavoro già avviato (base dati + UI)
- Nuove tabelle per “fatture fornitori” (testata + righe, importi `_euro`, ritenuta, tab pagamento). - Nuove tabelle per “fatture fornitori” (testata + righe, importi `_euro`, ritenuta, tab pagamento).
- Prime pagine Filament: - Prime pagine Filament:
- Archivio fatture fornitori - Archivio fatture fornitori
@ -51,17 +59,21 @@ ### Nuovo lavoro già avviato (base dati + UI)
## 2) In corso (priorità alta) ## 2) In corso (priorità alta)
### A) Riallineare le “Gestioni” per la contabilità ### A) Riallineare le “Gestioni” per la contabilità
Obiettivo: avere gestione ordinaria/riscaldamento/straordinaria *coerenti* e selezionabili per stabile, perché la contabilità deve agganciarsi a: Obiettivo: avere gestione ordinaria/riscaldamento/straordinaria *coerenti* e selezionabili per stabile, perché la contabilità deve agganciarsi a:
- anno gestione / esercizio - anno gestione / esercizio
- tipo gestione - tipo gestione
- conti/casse/banca - conti/casse/banca
- registrazioni/movimenti - registrazioni/movimenti
Output atteso: Output atteso:
- schermate Filament “Gestione ordinaria / riscaldamento / straordinaria” che leggono e usano la gestione attiva (non solo lo stabile attivo) - schermate Filament “Gestione ordinaria / riscaldamento / straordinaria” che leggono e usano la gestione attiva (non solo lo stabile attivo)
- link e contesti coerenti tra: Prima Nota, Casse e banche, Fatture, Movimenti banca - link e contesti coerenti tra: Prima Nota, Casse e banche, Fatture, Movimenti banca
Dipendenze: Dipendenze:
- definire/standardizzare “gestione attiva” (session/key, o tabella/relazione) - definire/standardizzare “gestione attiva” (session/key, o tabella/relazione)
--- ---
@ -69,6 +81,7 @@ ### A) Riallineare le “Gestioni” per la contabilità
## 3) Da fare (funzionalità richieste, in ordine consigliato) ## 3) Da fare (funzionalità richieste, in ordine consigliato)
### 3.0 Area fornitore/manutentore 2026 ### 3.0 Area fornitore/manutentore 2026
- Unificare la rubrica operativa usando la stessa maschera principale condivisa tra amministratore, fornitore e moduli collegati; differenziare accesso e persistenza per ruolo, non l impianto della view. - Unificare la rubrica operativa usando la stessa maschera principale condivisa tra amministratore, fornitore e moduli collegati; differenziare accesso e persistenza per ruolo, non l impianto della view.
- Aggiungere una pagina ad hoc per i ticket interni del fornitore con elenco unico di lavorazioni: ticket passati dall amministratore + lavorazioni interne + storico operativo. - Aggiungere una pagina ad hoc per i ticket interni del fornitore con elenco unico di lavorazioni: ticket passati dall amministratore + lavorazioni interne + storico operativo.
- Estendere `fornitore/collaboratori` con gestione orari di lavoro, disponibilita e futura assegnazione lavori ricorrenti. - Estendere `fornitore/collaboratori` con gestione orari di lavoro, disponibilita e futura assegnazione lavori ricorrenti.
@ -77,6 +90,7 @@ ### 3.0 Area fornitore/manutentore 2026
- Portare avanti catalogo prodotti unificato con vari fornitori, listini acquisto/vendita multipli, seriali, varianti modello/colore, foto, allegati e riferimenti esterni tipo ICECAT. - Portare avanti catalogo prodotti unificato con vari fornitori, listini acquisto/vendita multipli, seriali, varianti modello/colore, foto, allegati e riferimenti esterni tipo ICECAT.
### 3.1 Contabilità — fatture fornitori “alla Gescon” ### 3.1 Contabilità — fatture fornitori “alla Gescon”
- Rendere la maschera fattura fornitore più fedele allo screenshot (nomi/ordine tab + colonna azioni). - Rendere la maschera fattura fornitore più fedele allo screenshot (nomi/ordine tab + colonna azioni).
- Aggancio/creazione fornitore (contabilità + anagrafica unica/rubrica) direttamente dalla maschera. - Aggancio/creazione fornitore (contabilità + anagrafica unica/rubrica) direttamente dalla maschera.
- Contabilizzazione: - Contabilizzazione:
@ -85,6 +99,7 @@ ### 3.1 Contabilità — fatture fornitori “alla Gescon”
- RA (ritenuta) come debito separato da versare - RA (ritenuta) come debito separato da versare
### 3.2 Ritenute dacconto + F24 ### 3.2 Ritenute dacconto + F24
- Gestione codici tributo: 1019/1020 e 1040. - Gestione codici tributo: 1019/1020 e 1040.
- Regole: - Regole:
- RA si versa con F24 entro il 16 del mese successivo al pagamento. - RA si versa con F24 entro il 16 del mese successivo al pagamento.
@ -94,6 +109,7 @@ ### 3.2 Ritenute dacconto + F24
- (fase successiva) aggregazione per mese/tributo e stampa/distinta - (fase successiva) aggregazione per mese/tributo e stampa/distinta
### 3.3 Import MDB (GESCON) → contabilità operativa ### 3.3 Import MDB (GESCON) → contabilità operativa
- Predisporre import da `singolo_stabile.MDB` e `generale_stabile.mdb`: - Predisporre import da `singolo_stabile.MDB` e `generale_stabile.mdb`:
- operazioni/fatture/fornitori - operazioni/fatture/fornitori
- rispettare sempre i campi `*_euro` - rispettare sempre i campi `*_euro`
@ -103,6 +119,7 @@ ### 3.3 Import MDB (GESCON) → contabilità operativa
- collegamenti a tabelle millesimali/voci spesa - collegamenti a tabelle millesimali/voci spesa
### 3.4 XML multiplo → passaggio in contabilità ### 3.4 XML multiplo → passaggio in contabilità
- Oggi: import XML alimenta archivio `fatture_elettroniche`. - Oggi: import XML alimenta archivio `fatture_elettroniche`.
- Da fare: - Da fare:
- creazione/aggancio fornitore automatico (rubrica + fornitore) - creazione/aggancio fornitore automatico (rubrica + fornitore)
@ -113,31 +130,35 @@ ### 3.4 XML multiplo → passaggio in contabilità
## 4) Parti discusse ma non ancora implementate (da non perdere) ## 4) Parti discusse ma non ancora implementate (da non perdere)
### Ticket ### Ticket
- Esiste base legacy (controller/CRUD e categorie ticket). Da definire: - Esiste base legacy (controller/CRUD e categorie ticket). Da definire:
- UI Filament per ticket (lista, scheda, stati, priorità) - UI Filament per ticket (lista, scheda, stati, priorità)
- permessi/ruoli - permessi/ruoli
- collegamento ticket ↔ stabile ↔ documenti - collegamento ticket ↔ stabile ↔ documenti
### Gestione documentale ### Gestione documentale
- Esiste sistema “Documenti collegati” (con rotte e API statistiche/scadenze). Da definire: - Esiste sistema “Documenti collegati” (con rotte e API statistiche/scadenze). Da definire:
- UI Filament per documenti del condominio - UI Filament per documenti del condominio
- workflow scadenze/rinnovi - workflow scadenze/rinnovi
- permessi e visibilità per ruolo - permessi e visibilità per ruolo
### Protocollo documentazione ### Protocollo documentazione
- Da definire: - Da definire:
- numerazione protocolli per stabile/anno - numerazione protocolli per stabile/anno
- registrazione (data, mittente/destinatario, oggetto) - registrazione (data, mittente/destinatario, oggetto)
- collegamento a documenti/pec - collegamento a documenti/pec
### Consumi / acqua / contatori ### Consumi / acqua / contatori
- Backend (tabelle contatori/letture/algoritmi) esiste. - Backend (tabelle contatori/letture/algoritmi) esiste.
- UI Filament non ancora esposta: oggi la gestione è in view legacy. - UI Filament non ancora esposta: oggi la gestione è in view legacy.
--- ---
## 5) Note operative / rischi ## 5) Note operative / rischi
- Coesistenza Filament + UI classica: va mantenuta, ma i punti chiave devono essere raggiungibili dal “Cruscotto stabile”. - Coesistenza Filament + UI classica: va mantenuta, ma i punti chiave devono essere raggiungibili dal “Cruscotto stabile”.
- “Gestione attiva” è il nodo per rendere contabilità consistente (oltre a “stabile attivo”). - “Gestione attiva” è il nodo per rendere contabilità consistente (oltre a “stabile attivo”).
- Import MDB richiede mapping rigoroso (soprattutto `_euro`) per evitare contabilità sbilanciata. - Import MDB richiede mapping rigoroso (soprattutto `_euro`) per evitare contabilità sbilanciata.

View File

@ -6,4 +6,5 @@ ## Topics
- [Fatture Fornitori](topics/fatture-fornitori.md) - [Fatture Fornitori](topics/fatture-fornitori.md)
- [Filo Logico Contabilita](topics/filo-logico-contabilita.md) - [Filo Logico Contabilita](topics/filo-logico-contabilita.md)
- [Import Unita Cod Cond](topics/import-unita-cod-cond.md)
- [Affitti ISTAT e Contratti](topics/affitti-istat-contratti.md) - [Affitti ISTAT e Contratti](topics/affitti-istat-contratti.md)

View File

@ -1 +1 @@
Filo Logico Contabilita Import Unita Cod Cond

View File

@ -0,0 +1,57 @@
# Topic: Import Unita Cod Cond
## Obiettivo
- Rendere l'import legacy di unita, nominativi, millesimi e voci coerente con la realta archivistica Gescon, evitando fallback vuoti o identificazioni deboli.
## Regole vincolanti
- L'ultimo anno disponibile e la fotografia autorevole dello stato corrente.
- La chiave certa dell'unita e `condomin.cod_cond`.
- `scala`, `interno`, `catasto_sub` e il codice mnemonico servono per riconciliazione e visualizzazione, non come chiave inventata in fallback.
- Il codice visibile all'operatore deve restare leggibile in forma `Palazzina/Scala/Interno`.
- Gli anni precedenti servono a costruire la cronologia dei soggetti collegati all'unita, non a sostituire il nominativo corrente trovato nell'ultimo anno.
- Non creare righe di completamento artificiale a zero per millesimi o nominativi se la sorgente non le fornisce.
## Modello archivistico chiarito dall'utente
- `condomin.cod_cond` identifica l'unita legacy in modo stabile.
- `comproprietari` integra la storia proprietari e quote.
- `emes_gen` definisce il contesto di emissione e l'anno gestionale.
- `emes_det.cod_cond` collega la rata emessa all'unita.
- `incassi.cod_cond` collega l'incasso all'unita.
- `cond_inq` distingue il ruolo storico: condomino/proprietario (`C`) o inquilino (`I`).
## Stato attuale del codice
- `app/Console/Commands/ImportGesconFullPipeline.php`
- fixato il riempimento automatico di dettagli millesimali a zero: ora e opt-in con `--fill-missing-millesimi`
- fixato il fallback di aggancio tabella per `stepVoci()`
- fixato il preload staging per `legacy_year`
- `app/Console/Commands/NetgesconQaMillesimiSpeseCommand.php`
- aggiunto per QA su allineamento unita/nominativi/millesimi/voci
## Evidenze validate su 0021
- Lo staging dell'ultimo anno utile non veniva caricato correttamente finche il preflight controllava solo `cod_stabile`.
- Dopo il fix sul preload, `0021/0004` viene caricato e reso disponibile per confronto/import.
- Le unita ancora senza nominativo corrente non vanno risolte con fallback automatici: almeno i casi `A/0`, `A/42`, `A/43`, `A/215`, `A/216` non hanno corrispondenza trovata nel controllo staging eseguito.
## Prossimi step operativi
1. Rifattorizzare la costruzione snapshot corrente unita/nominativi a partire dall'ultimo anno disponibile.
2. Usare `cod_cond` come chiave di merge tra staging e dominio.
3. Inserire gli anni precedenti solo come variazioni storiche in `unita_immobiliare_nominativi`.
4. Verificare che rate, incassi e ruoli `C`/`I` restino riconciliabili per anno e unita.
5. Gestire le unita dominio senza sorgente corrente con una decisione esplicita di review/disattivazione, non con dati inventati.
## Comandi utili gia usati
- `php artisan netgescon:qa-millesimi-spese --stabile=0021 --anno=0003`
- `php artisan netgescon:qa-unita-nominativi --stabile_id=25`
- `php artisan gescon:import-align --stabile=0021 --path=/mnt/gescon-archives/gescon --years=2025,2026 --water-years=2025,2026 --open-years=2025,2026`
- `php artisan gescon:import-full --stabile=0021 --solo=unita --anno=0004 --dry-run`
## Vincolo di continuita
- Le vecchie note in `docs/ai` o nei chat export che propongono fallback tipo unita inventate, `interno=0` come riempitivo o codici sintetici non fondati sulla sorgente vanno considerate superate se confliggono con questo topic.

View File

@ -121,6 +121,16 @@ ## Rilascio operativo 2026-04-06
Questo e il blocco che puo essere aggiornato adesso con la procedura Git-first senza correzioni manuali extra su staging. Questo e il blocco che puo essere aggiornato adesso con la procedura Git-first senza correzioni manuali extra su staging.
### Sync fornitori legacy 2026-04-08
- La procedura fornitori va eseguita con un solo entry point: `php artisan gescon:sync-fornitori-legacy <amministratore_id> --mdb=/mnt/gescon-archives/gescon/dbc/Fornitori.mdb`
- La sync esegue in sequenza:
- import `Fornitori.mdb` in staging
- aggiornamento archivio locale `fornitori` + `rubrica_universale`
- aggiornamento TAG da descrizione legacy
- In `--dry-run` la staging resta in preview e i passaggi locali non persistono, cosi il legacy resta la sorgente da verificare prima del consolidamento.
- Regola operativa: per i fornitori il legacy aggiorna l archivio locale, non il contrario.
Incluso nel rilascio: Incluso nel rilascio:
- Ticket Mobile: anteprima immediata locale di foto e allegati prima dell invio definitivo. - Ticket Mobile: anteprima immediata locale di foto e allegati prima dell invio definitivo.
@ -136,10 +146,11 @@ ## Rilascio operativo 2026-04-06
3. usare `Aggiorna staging da Gitea` 3. usare `Aggiorna staging da Gitea`
4. aspettare la conclusione del sync Git e della QA sicura inclusa 4. aspettare la conclusione del sync Git e della QA sicura inclusa
5. validare subito i punti visibili: 5. validare subito i punti visibili:
- Ticket Mobile mostra l anteprima prima di inviare
- Ticket Gestione mostra `Tab 4 - Assicurazioni` - Ticket Mobile mostra l anteprima prima di inviare
- `/admin-filament/fornitore/tickets/{id}` non produce 403 per i casi operativi normali - Ticket Gestione mostra `Tab 4 - Assicurazioni`
- la lista ticket fornitore apre anche la modal rapida - `/admin-filament/fornitore/tickets/{id}` non produce 403 per i casi operativi normali
- la lista ticket fornitore apre anche la modal rapida
Regola per evitare nuovi problemi: Regola per evitare nuovi problemi:

View File

@ -3,6 +3,37 @@
/** @var \App\Models\Stabile|null $stabileAttivo */ /** @var \App\Models\Stabile|null $stabileAttivo */
$activeId = $stabileAttivo?->id; $activeId = $stabileAttivo?->id;
$stabileHasRiscaldamento = static function (?\App\Models\Stabile $stabile): bool {
if (! $stabile) {
return false;
}
if ((bool) ($stabile->riscaldamento_centralizzato ?? false)) {
return true;
}
$rateRiscaldamento = $stabile->rate_riscaldamento_mesi ?? [];
if (is_string($rateRiscaldamento)) {
$rateRiscaldamento = json_decode($rateRiscaldamento, true);
}
if (is_array($rateRiscaldamento) && $rateRiscaldamento !== []) {
return true;
}
$config = $stabile->configurazione_avanzata ?? [];
if (is_string($config)) {
$config = json_decode($config, true);
}
if (is_array($config) && array_key_exists('mostra_riscaldamento', $config) && $config['mostra_riscaldamento']) {
return true;
}
return \Illuminate\Support\Facades\DB::table('gestioni_contabili')
->where('stabile_id', $stabile->id)
->where('tipo_gestione', 'riscaldamento')
->exists();
};
$stabileLabel = static function (?\App\Models\Stabile $stabile): string { $stabileLabel = static function (?\App\Models\Stabile $stabile): string {
if (! $stabile) { if (! $stabile) {
return ''; return '';
@ -28,7 +59,7 @@
</div> </div>
<div class="text-xs text-gray-500"> <div class="text-xs text-gray-500">
Ordinari: {{ ($stabileAttivo->numero_rate_ordinarie ?? 0) > 0 ? 'Sì' : 'No' }} · Ordinari: {{ ($stabileAttivo->numero_rate_ordinarie ?? 0) > 0 ? 'Sì' : 'No' }} ·
Riscaldamento: {{ (bool) ($stabileAttivo->riscaldamento_centralizzato ?? false) ? 'Sì' : 'No' }} Riscaldamento: {{ $stabileHasRiscaldamento($stabileAttivo) ? 'Sì' : 'No' }}
</div> </div>
</div> </div>
@endif @endif

View File

@ -1,6 +1,8 @@
<x-filament-panels::page> <x-filament-panels::page>
@php($supportUpdateAuthUser = auth()->user()) @php
@php($supportCanRunUpdate = $supportUpdateAuthUser && method_exists($supportUpdateAuthUser, 'hasAnyRole') && $supportUpdateAuthUser->hasAnyRole(['super-admin', 'admin', 'amministratore'])) $supportUpdateAuthUser = auth()->user();
$supportCanRunUpdate = $supportUpdateAuthUser && method_exists($supportUpdateAuthUser, 'hasAnyRole') && $supportUpdateAuthUser->hasAnyRole(['super-admin', 'admin', 'amministratore']);
@endphp
<div class="space-y-4 text-xs"> <div class="space-y-4 text-xs">
<div class="rounded-xl border border-sky-200 bg-sky-50 p-4"> <div class="rounded-xl border border-sky-200 bg-sky-50 p-4">

View File

@ -88,7 +88,9 @@
</div> </div>
<div class="rounded-xl border bg-white p-4"> <div class="rounded-xl border bg-white p-4">
@php($selected = $this->selectedScheda) @php
$selected = $this->selectedScheda;
@endphp
@if(! $selected) @if(! $selected)
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800"> <div class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
Seleziona una riga dall'elenco per aprire la scheda tecnica legacy con cliente, seriali, allegati e note operative. Seleziona una riga dall'elenco per aprire la scheda tecnica legacy con cliente, seriali, allegati e note operative.
@ -135,7 +137,9 @@
<div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Fornitore e allegati</div> <div class="text-[11px] font-semibold uppercase tracking-wide text-slate-500">Fornitore e allegati</div>
<div class="mt-1 text-sm text-slate-800"> <div class="mt-1 text-sm text-slate-800">
@if($selected->fornitore) @if($selected->fornitore)
@php($fornitoreUrl = $this->getFornitoreUrl((int) $selected->fornitore->id)) @php
$fornitoreUrl = $this->getFornitoreUrl((int) $selected->fornitore->id);
@endphp
@if($fornitoreUrl) @if($fornitoreUrl)
<a href="{{ $fornitoreUrl }}" class="font-medium text-sky-700 hover:text-sky-600">{{ $selected->fornitore->ragione_sociale ?: ('Fornitore #' . $selected->fornitore->id) }}</a> <a href="{{ $fornitoreUrl }}" class="font-medium text-sky-700 hover:text-sky-600">{{ $selected->fornitore->ragione_sociale ?: ('Fornitore #' . $selected->fornitore->id) }}</a>
@else @else

View File

@ -18,7 +18,9 @@
Ticket non selezionato o non accessibile. Apri questa pagina da Gestione Ticket. Ticket non selezionato o non accessibile. Apri questa pagina da Gestione Ticket.
</div> </div>
@else @else
@php($ticket = $this->ticket) @php
$ticket = $this->ticket;
@endphp
<div class="rounded-xl border bg-white p-4"> <div class="rounded-xl border bg-white p-4">
<div class="text-lg font-semibold">#{{ $ticket->id }} - {{ $ticket->titolo }}</div> <div class="text-lg font-semibold">#{{ $ticket->id }} - {{ $ticket->titolo }}</div>
<div class="mt-1 text-xs text-gray-600"> <div class="mt-1 text-xs text-gray-600">