Supporto/fornitori sync and Google OAuth refresh hardening
This commit is contained in:
parent
a9204e125b
commit
432f0565ab
|
|
@ -67,10 +67,10 @@ public function handle(): int
|
|||
}
|
||||
|
||||
$stats = [
|
||||
'processed' => 0,
|
||||
'processed' => 0,
|
||||
'persona_updated' => 0,
|
||||
'rubrica_updated' => 0,
|
||||
'no_match' => 0,
|
||||
'no_match' => 0,
|
||||
'phone_conflicts' => 0,
|
||||
];
|
||||
|
||||
|
|
@ -100,9 +100,9 @@ public function handle(): int
|
|||
}
|
||||
|
||||
/** @var RubricaUniversale $best */
|
||||
$best = $rubriche->first();
|
||||
$best = $rubriche->first();
|
||||
$canonicalRubricaId = $this->extractRubricaIdFromPersonaNote((string) ($row->note ?? ''));
|
||||
$canonicalRubrica = $canonicalRubricaId > 0 ? RubricaUniversale::query()->find($canonicalRubricaId) : null;
|
||||
$canonicalRubrica = $canonicalRubricaId > 0 ? RubricaUniversale::query()->find($canonicalRubricaId) : null;
|
||||
if (! $canonicalRubrica instanceof RubricaUniversale) {
|
||||
$canonicalRubrica = $best;
|
||||
}
|
||||
|
|
@ -223,8 +223,8 @@ private function buildPersonaUpdates(object $row, RubricaUniversale $best, array
|
|||
|
||||
$bestMobile = $this->normalizePhone((string) ($best->telefono_cellulare ?: $best->telefono_ufficio ?: $best->telefono_casa));
|
||||
$bestOffice = $this->normalizePhone((string) ($best->telefono_ufficio ?: $best->telefono_casa));
|
||||
$bestEmail = $this->normalizeEmail((string) ($best->email ?? ''));
|
||||
$bestPec = $this->normalizeEmail((string) ($best->pec ?? ''));
|
||||
$bestEmail = $this->normalizeEmail((string) ($best->email ?? ''));
|
||||
$bestPec = $this->normalizeEmail((string) ($best->pec ?? ''));
|
||||
|
||||
if (! filled($row->telefono_principale) && $bestMobile !== null && ! $this->phoneExistsOnOtherPersona($bestMobile, (int) $row->persona_id)) {
|
||||
$updates['telefono_principale'] = $bestMobile;
|
||||
|
|
@ -267,4 +267,4 @@ private function buildRubricaUpdates(RubricaUniversale $canonical, RubricaUniver
|
|||
|
||||
return $updates;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
399
app/Console/Commands/NetgesconQaMillesimiSpeseCommand.php
Normal file
399
app/Console/Commands/NetgesconQaMillesimiSpeseCommand.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -193,15 +193,15 @@ public function handle(): int
|
|||
'status' => 'received',
|
||||
'received_at' => $receivedAt,
|
||||
'metadata' => [
|
||||
'raw_line' => $line,
|
||||
'source_host' => $host,
|
||||
'source_port' => $port,
|
||||
'assigned_user_id' => $assignedUserId,
|
||||
'stabile_id' => $stabileId,
|
||||
'amministratore_id'=> $amministratoreId,
|
||||
'studio_role' => $routing['studio_role'],
|
||||
'studio_mode' => $routing['studio_mode'],
|
||||
'smdr' => [
|
||||
'raw_line' => $line,
|
||||
'source_host' => $host,
|
||||
'source_port' => $port,
|
||||
'assigned_user_id' => $assignedUserId,
|
||||
'stabile_id' => $stabileId,
|
||||
'amministratore_id' => $amministratoreId,
|
||||
'studio_role' => $routing['studio_role'],
|
||||
'studio_mode' => $routing['studio_mode'],
|
||||
'smdr' => [
|
||||
'date' => $parsed['date'],
|
||||
'time' => $parsed['time'],
|
||||
'extension' => $parsed['extension'],
|
||||
|
|
@ -245,6 +245,8 @@ public function handle(): int
|
|||
try {
|
||||
$lineSuffix = ! empty($parsed['co']) ? ' - linea ' . (string) $parsed['co'] : '';
|
||||
|
||||
$isMissedResponseGroup = $this->isMissedResponseGroupCall($parsed, $routing);
|
||||
|
||||
ChiamataPostIt::query()->create([
|
||||
'telefono' => $phone,
|
||||
'stabile_id' => $stabileId,
|
||||
|
|
@ -253,7 +255,7 @@ public function handle(): int
|
|||
'oggetto' => 'Chiamata in entrata da SMDR' . $lineSuffix,
|
||||
'nota' => $line,
|
||||
'direzione' => 'in_arrivo',
|
||||
'esito' => ($durationSeconds ?? 0) > 0 ? 'answered' : 'missed',
|
||||
'esito' => $isMissedResponseGroup || ($durationSeconds ?? 0) <= 0 ? 'missed' : 'answered',
|
||||
'origine' => 'smdr',
|
||||
'origine_id' => $fingerprint,
|
||||
'durata_secondi' => $durationSeconds,
|
||||
|
|
@ -518,4 +520,18 @@ private function shouldCreateOperationalPostIt(array $parsed): bool
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
103
app/Console/Commands/SyncGesconFornitoriLegacyCommand.php
Normal file
103
app/Console/Commands/SyncGesconFornitoriLegacyCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -192,10 +192,10 @@ protected function buildInterventoRow(TicketIntervento $intervento): array
|
|||
*/
|
||||
protected function extractCallerData(string $descrizione): array
|
||||
{
|
||||
$contatto = '-';
|
||||
$telefono = '';
|
||||
$problema = '';
|
||||
$email = '';
|
||||
$contatto = '-';
|
||||
$telefono = '';
|
||||
$problema = '';
|
||||
$email = '';
|
||||
$riferimento = '';
|
||||
|
||||
$lines = preg_split('/\r\n|\r|\n/', $descrizione) ?: [];
|
||||
|
|
@ -230,7 +230,7 @@ protected function extractCallerData(string $descrizione): array
|
|||
}
|
||||
|
||||
if (str_starts_with($trim, 'Riferimento unità:')) {
|
||||
$unita = trim((string) str_replace('Riferimento unità:', '', $trim));
|
||||
$unita = trim((string) str_replace('Riferimento unità:', '', $trim));
|
||||
$riferimento = $riferimento !== '' ? ($riferimento . ' · ' . $unita) : $unita;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ public static function canAccess(): bool
|
|||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
|
|
@ -58,7 +58,7 @@ public function mount(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$this->fornitoreId = (int) $fornitore->id;
|
||||
$this->fornitoreId = (int) $fornitore->id;
|
||||
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
||||
$this->refreshRows();
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ public function refreshRows(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$term = trim($this->search);
|
||||
$term = trim($this->search);
|
||||
$normalized = preg_replace('/[^A-Za-z0-9]+/', '', strtoupper($term)) ?? '';
|
||||
|
||||
$products = Product::query()
|
||||
|
|
@ -106,20 +106,18 @@ public function refreshRows(): void
|
|||
|
||||
$this->rows = $products->map(function (Product $product): array {
|
||||
$identifier = $product->identifiers
|
||||
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitoreId)
|
||||
?? $product->identifiers->first();
|
||||
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitoreId) ?? $product->identifiers->first();
|
||||
$offer = $product->offers
|
||||
->first(fn(ProductOffer $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitoreId)
|
||||
?? $product->offers->first();
|
||||
->first(fn(ProductOffer $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitoreId) ?? $product->offers->first();
|
||||
|
||||
return [
|
||||
'id' => (int) $product->id,
|
||||
'id' => (int) $product->id,
|
||||
'internal_code' => (string) ($product->internal_code ?? ''),
|
||||
'label' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))),
|
||||
'barcode' => (string) ($identifier->code_value ?? ''),
|
||||
'source' => (string) ($offer?->source_name ?: 'Catalogo interno'),
|
||||
'price' => $offer?->price_amount !== null ? number_format((float) $offer->price_amount, 2, ',', '.') . ' ' . (string) ($offer->currency ?? 'EUR') : '-',
|
||||
'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)),
|
||||
'label' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))),
|
||||
'barcode' => (string) ($identifier->code_value ?? ''),
|
||||
'source' => (string) ($offer?->source_name ?: 'Catalogo interno'),
|
||||
'price' => $offer?->price_amount !== null ? number_format((float) $offer->price_amount, 2, ',', '.') . ' ' . (string) ($offer->currency ?? 'EUR') : '-',
|
||||
'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)),
|
||||
];
|
||||
})->all();
|
||||
}
|
||||
|
|
@ -144,4 +142,4 @@ protected function buildAmazonSearchUrl(string $term): string
|
|||
|
||||
return 'https://www.amazon.it/s?' . http_build_query($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public static function canAccess(): bool
|
|||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
|
|
@ -60,9 +60,9 @@ public function mount(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$this->fornitoreId = (int) $fornitore->id;
|
||||
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
||||
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus($fornitore);
|
||||
$this->fornitoreId = (int) $fornitore->id;
|
||||
$this->fornitoreLabel = $this->getFornitoreLabel($fornitore);
|
||||
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus($fornitore);
|
||||
$this->refreshRows();
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +91,7 @@ public function refreshRows(): void
|
|||
|
||||
foreach ($items as $intervento) {
|
||||
$contact = $this->resolveCallerDataForRubrica($intervento);
|
||||
$key = $contact['identity_key'];
|
||||
$key = $contact['identity_key'];
|
||||
|
||||
if ($key === '') {
|
||||
$key = 'ticket-' . (int) $intervento->ticket_id;
|
||||
|
|
@ -113,14 +113,14 @@ public function refreshRows(): void
|
|||
|
||||
if (! isset($grouped[$key])) {
|
||||
$grouped[$key] = [
|
||||
'contatto' => $contact['contatto'],
|
||||
'telefono' => $contact['telefono'],
|
||||
'email' => $contact['email'],
|
||||
'stabile' => $contact['stabile'],
|
||||
'origine' => $contact['origine'],
|
||||
'ultimo_ticket_id' => (int) $intervento->ticket_id,
|
||||
'contatto' => $contact['contatto'],
|
||||
'telefono' => $contact['telefono'],
|
||||
'email' => $contact['email'],
|
||||
'stabile' => $contact['stabile'],
|
||||
'origine' => $contact['origine'],
|
||||
'ultimo_ticket_id' => (int) $intervento->ticket_id,
|
||||
'ultima_scheda_url' => TicketInterventoScheda::getUrl(['record' => (int) $intervento->id], panel: 'admin-filament'),
|
||||
'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
||||
'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
||||
'totale_interventi' => 1,
|
||||
];
|
||||
continue;
|
||||
|
|
@ -162,19 +162,19 @@ protected function resolveCallerDataForRubrica(TicketIntervento $intervento): ar
|
|||
|
||||
$contatto = $parsed['contatto'];
|
||||
$telefono = $parsed['telefono'];
|
||||
$email = '';
|
||||
$origine = 'Descrizione ticket';
|
||||
$email = '';
|
||||
$origine = 'Descrizione ticket';
|
||||
|
||||
if ($ticket?->soggettoRichiedente) {
|
||||
$soggetto = $ticket->soggettoRichiedente;
|
||||
$contatto = trim((string) ($soggetto->ragione_sociale ?: trim(($soggetto->nome ?? '') . ' ' . ($soggetto->cognome ?? '')))) ?: $contatto;
|
||||
$telefono = trim((string) ($soggetto->telefono ?? '')) ?: $telefono;
|
||||
$email = trim((string) ($soggetto->email ?? ''));
|
||||
$origine = 'Richiedente ticket';
|
||||
$email = trim((string) ($soggetto->email ?? ''));
|
||||
$origine = 'Richiedente ticket';
|
||||
} elseif ($ticket?->apertoDaUser) {
|
||||
$contatto = trim((string) ($ticket->apertoDaUser->name ?? '')) ?: $contatto;
|
||||
$email = trim((string) ($ticket->apertoDaUser->email ?? ''));
|
||||
$origine = 'Utente apertura ticket';
|
||||
$email = trim((string) ($ticket->apertoDaUser->email ?? ''));
|
||||
$origine = 'Utente apertura ticket';
|
||||
}
|
||||
|
||||
$identityKey = '';
|
||||
|
|
@ -187,11 +187,11 @@ protected function resolveCallerDataForRubrica(TicketIntervento $intervento): ar
|
|||
}
|
||||
|
||||
return [
|
||||
'contatto' => $contatto !== '' ? $contatto : 'Contatto non identificato',
|
||||
'telefono' => $telefono,
|
||||
'email' => $email,
|
||||
'origine' => $origine,
|
||||
'stabile' => (string) ($ticket?->stabile?->denominazione ?? '-'),
|
||||
'contatto' => $contatto !== '' ? $contatto : 'Contatto non identificato',
|
||||
'telefono' => $telefono,
|
||||
'email' => $email,
|
||||
'origine' => $origine,
|
||||
'stabile' => (string) ($ticket?->stabile?->denominazione ?? '-'),
|
||||
'identity_key' => $identityKey,
|
||||
];
|
||||
}
|
||||
|
|
@ -209,10 +209,10 @@ protected function resolveGoogleWorkspaceStatus(Fornitore $fornitore): ?array
|
|||
$oauth = (array) (($amministratore->impostazioni['google']['oauth'] ?? null) ?: []);
|
||||
|
||||
return [
|
||||
'connected' => (bool) ($oauth['connected'] ?? false),
|
||||
'email' => (string) ($oauth['email'] ?? ''),
|
||||
'name' => (string) ($oauth['name'] ?? ''),
|
||||
'connected' => (bool) ($oauth['connected'] ?? false),
|
||||
'email' => (string) ($oauth['email'] ?? ''),
|
||||
'name' => (string) ($oauth['name'] ?? ''),
|
||||
'connected_at' => (string) ($oauth['connected_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,16 +50,16 @@ class TicketInterventoScheda extends Page
|
|||
|
||||
/** @var array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string,rubrica_url:?string,estratto_url:?string,stabile_url:?string,unita_label:string} */
|
||||
public array $caller = [
|
||||
'contatto' => '-',
|
||||
'telefono' => '',
|
||||
'email' => '',
|
||||
'problema' => '',
|
||||
'sorgente' => '',
|
||||
'riferimento' => '',
|
||||
'rubrica_url' => null,
|
||||
'estratto_url'=> null,
|
||||
'stabile_url' => null,
|
||||
'unita_label' => '',
|
||||
'contatto' => '-',
|
||||
'telefono' => '',
|
||||
'email' => '',
|
||||
'problema' => '',
|
||||
'sorgente' => '',
|
||||
'riferimento' => '',
|
||||
'rubrica_url' => null,
|
||||
'estratto_url' => null,
|
||||
'stabile_url' => null,
|
||||
'unita_label' => '',
|
||||
];
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
|
|
@ -1094,16 +1094,16 @@ protected function resolveCallerData(?Ticket $ticket): array
|
|||
$parsed = $this->extractCallerData((string) ($ticket?->descrizione ?? ''));
|
||||
|
||||
$caller = [
|
||||
'contatto' => $parsed['contatto'],
|
||||
'telefono' => $parsed['telefono'],
|
||||
'email' => $parsed['email'] ?? '',
|
||||
'problema' => $parsed['problema'] !== '' ? $parsed['problema'] : (string) ($ticket?->titolo ?? ''),
|
||||
'sorgente' => 'Descrizione ticket',
|
||||
'riferimento' => $this->buildCallerReference($ticket, (string) ($parsed['riferimento'] ?? '')),
|
||||
'rubrica_url' => null,
|
||||
'estratto_url'=> null,
|
||||
'stabile_url' => $ticket?->stabile ? StabileScheda::getUrl(['record' => (int) $ticket->stabile->id], panel: 'admin-filament') : null,
|
||||
'unita_label' => $ticket?->unitaImmobiliare ? $this->formatUnitaLabel($ticket->unitaImmobiliare) : '',
|
||||
'contatto' => $parsed['contatto'],
|
||||
'telefono' => $parsed['telefono'],
|
||||
'email' => $parsed['email'] ?? '',
|
||||
'problema' => $parsed['problema'] !== '' ? $parsed['problema'] : (string) ($ticket?->titolo ?? ''),
|
||||
'sorgente' => 'Descrizione ticket',
|
||||
'riferimento' => $this->buildCallerReference($ticket, (string) ($parsed['riferimento'] ?? '')),
|
||||
'rubrica_url' => null,
|
||||
'estratto_url' => null,
|
||||
'stabile_url' => $ticket?->stabile ? StabileScheda::getUrl(['record' => (int) $ticket->stabile->id], panel : 'admin-filament'): null,
|
||||
'unita_label' => $ticket?->unitaImmobiliare ? $this->formatUnitaLabel($ticket->unitaImmobiliare) : '',
|
||||
];
|
||||
|
||||
if (($caller['contatto'] ?? '-') === '-' || trim((string) ($caller['contatto'] ?? '')) === '') {
|
||||
|
|
@ -1120,9 +1120,9 @@ protected function resolveCallerData(?Ticket $ticket): array
|
|||
$caller['contatto'] = $label;
|
||||
}
|
||||
|
||||
$caller['telefono'] = trim((string) ($soggetto->telefono ?? '')) ?: $caller['telefono'];
|
||||
$caller['email'] = trim((string) ($soggetto->email ?? '')) ?: $caller['email'];
|
||||
$caller['sorgente'] = 'Richiedente collegato al ticket';
|
||||
$caller['telefono'] = trim((string) ($soggetto->telefono ?? '')) ?: $caller['telefono'];
|
||||
$caller['email'] = trim((string) ($soggetto->email ?? '')) ?: $caller['email'];
|
||||
$caller['sorgente'] = 'Richiedente collegato al ticket';
|
||||
$caller['estratto_url'] = EstrattoContoSoggetto::getUrl(['record' => (int) $soggetto->id], panel: 'admin-filament');
|
||||
|
||||
$rubrica = $this->resolveCallerRubrica($ticket, $caller);
|
||||
|
|
@ -1204,7 +1204,7 @@ private function resolveCallerRubrica(?Ticket $ticket, array $caller): ?RubricaU
|
|||
{
|
||||
$adminId = (int) ($ticket?->stabile?->amministratore_id ?? 0);
|
||||
|
||||
$queries = [];
|
||||
$queries = [];
|
||||
$soggetto = $ticket?->soggettoRichiedente;
|
||||
if ($soggetto && filled($soggetto->codice_fiscale)) {
|
||||
$queries[] = fn() => RubricaUniversale::query()->where('codice_fiscale', strtoupper(trim((string) $soggetto->codice_fiscale)));
|
||||
|
|
|
|||
|
|
@ -717,29 +717,23 @@ public function aggiornaFornitoriGescon(): void
|
|||
}
|
||||
|
||||
try {
|
||||
$exit1 = Artisan::call('import:gescon-fornitori', [
|
||||
'tenant' => (string) $amm->id,
|
||||
'--mdb' => $mdb,
|
||||
]);
|
||||
$out1 = trim((string) Artisan::output());
|
||||
|
||||
$exit2 = Artisan::call('gescon:import-fornitori-legacy', [
|
||||
$exit = Artisan::call('gescon:sync-fornitori-legacy', [
|
||||
'amministratore_id' => (string) $amm->id,
|
||||
'--mdb' => $mdb,
|
||||
]);
|
||||
$out2 = trim((string) Artisan::output());
|
||||
$out = trim((string) Artisan::output());
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()->title('Aggiornamento fornitori fallito')->body($e->getMessage())->danger()->send();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($exit1 !== 0 || $exit2 !== 0) {
|
||||
$snippet = $out2 !== '' ? $out2 : $out1;
|
||||
if ($exit !== 0) {
|
||||
$snippet = $out !== '' ? $out : '';
|
||||
$snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : '';
|
||||
|
||||
Notification::make()
|
||||
->title('Aggiornamento fornitori: errore')
|
||||
->body('Exit staging=' . $exit1 . ' | exit import=' . $exit2 . ($snippet !== '' ? "\n\n" . $snippet : ''))
|
||||
->body('Exit sync=' . $exit . ($snippet !== '' ? "\n\n" . $snippet : ''))
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
|
|
@ -792,24 +786,12 @@ public function refreshImportTagLegacyFornitoriGlobale(): void
|
|||
}
|
||||
|
||||
try {
|
||||
$exit1 = Artisan::call('import:gescon-fornitori', [
|
||||
'tenant' => (string) $amm->id,
|
||||
'--mdb' => $mdb,
|
||||
]);
|
||||
$out1 = trim((string) Artisan::output());
|
||||
|
||||
$exit2 = Artisan::call('gescon:import-fornitori-legacy', [
|
||||
$exit = Artisan::call('gescon:sync-fornitori-legacy', [
|
||||
'amministratore_id' => (string) $amm->id,
|
||||
'--mdb' => $mdb,
|
||||
'--dry-run' => $dryRun,
|
||||
]);
|
||||
$out2 = trim((string) Artisan::output());
|
||||
|
||||
$tagParams = [];
|
||||
if ($dryRun) {
|
||||
$tagParams['--dry-run'] = true;
|
||||
}
|
||||
$exit3 = Artisan::call('fornitori:import-legacy-tags', $tagParams);
|
||||
$out3 = trim((string) Artisan::output());
|
||||
$out = trim((string) Artisan::output());
|
||||
} catch (\Throwable $e) {
|
||||
Notification::make()
|
||||
->title('Refresh + import TAG fallito')
|
||||
|
|
@ -819,13 +801,13 @@ public function refreshImportTagLegacyFornitoriGlobale(): void
|
|||
return;
|
||||
}
|
||||
|
||||
if ($exit1 !== 0 || $exit2 !== 0 || $exit3 !== 0) {
|
||||
$snippet = $out3 !== '' ? $out3 : ($out2 !== '' ? $out2 : $out1);
|
||||
if ($exit !== 0) {
|
||||
$snippet = $out !== '' ? $out : '';
|
||||
$snippet = $snippet !== '' ? mb_substr($snippet, 0, 800) : '';
|
||||
|
||||
Notification::make()
|
||||
->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()
|
||||
->send();
|
||||
return;
|
||||
|
|
@ -1013,22 +995,16 @@ public function eseguiImportSelezionato(EssentialImportService $service): void
|
|||
|
||||
if (! empty($options['with_operazioni'])) {
|
||||
try {
|
||||
$exit1 = Artisan::call('import:gescon-fornitori', [
|
||||
'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', [
|
||||
$exit = Artisan::call('gescon:sync-fornitori-legacy', [
|
||||
'amministratore_id' => (string) $amm->id,
|
||||
'--mdb' => (string) ($state['fornitori_mdb'] ?? rtrim($options['path'], '/') . '/dbc/Fornitori.mdb'),
|
||||
]);
|
||||
$outputs[] = trim((string) Artisan::output());
|
||||
|
||||
if ($exit1 !== 0 || $exit2 !== 0) {
|
||||
if ($exit !== 0) {
|
||||
Notification::make()
|
||||
->title('Aggiornamento fornitori: errore')
|
||||
->body('Exit staging=' . $exit1 . ' | exit import=' . $exit2)
|
||||
->body('Exit sync=' . $exit)
|
||||
->danger()
|
||||
->send();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -19,4 +19,4 @@ class AggiornamentoLauncher extends Modifiche
|
|||
protected static ?string $slug = 'supporto/aggiornamento-nodo';
|
||||
|
||||
protected string $view = 'filament.pages.supporto.aggiornamento-launcher';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class GestioneAssicurazioni extends Page
|
|||
public array $totals = [
|
||||
'aperte' => 0,
|
||||
'chiuse' => 0,
|
||||
'tutte' => 0,
|
||||
'tutte' => 0,
|
||||
];
|
||||
|
||||
public static function canAccess(): bool
|
||||
|
|
@ -44,12 +44,12 @@ public static function canAccess(): bool
|
|||
$user = Auth::user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||
}
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->status = (string) request()->query('status', 'aperte');
|
||||
$this->status = (string) request()->query('status', 'aperte');
|
||||
$this->focusTicketId = (int) request()->query('ticket', 0) ?: null;
|
||||
$this->refreshData();
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ public function refreshData(): void
|
|||
{
|
||||
$stabileIds = $this->resolveAccessibleStabileIds();
|
||||
if ($stabileIds === []) {
|
||||
$this->rows = [];
|
||||
$this->rows = [];
|
||||
$this->totals = ['aperte' => 0, 'chiuse' => 0, 'tutte' => 0];
|
||||
return;
|
||||
}
|
||||
|
|
@ -96,23 +96,23 @@ public function refreshData(): void
|
|||
->get()
|
||||
->map(function (InsuranceClaim $claim): array {
|
||||
return [
|
||||
'id' => (int) $claim->id,
|
||||
'ticket_id' => (int) $claim->ticket_id,
|
||||
'ticket_title' => (string) ($claim->ticket->titolo ?? '-'),
|
||||
'stabile' => (string) ($claim->ticket->stabile->denominazione ?? '-'),
|
||||
'claim_number' => (string) ($claim->claim_number ?? ''),
|
||||
'policy_reference' => (string) ($claim->policy_reference ?? ''),
|
||||
'policy_label' => trim(implode(' · ', array_filter([
|
||||
'id' => (int) $claim->id,
|
||||
'ticket_id' => (int) $claim->ticket_id,
|
||||
'ticket_title' => (string) ($claim->ticket->titolo ?? '-'),
|
||||
'stabile' => (string) ($claim->ticket->stabile->denominazione ?? '-'),
|
||||
'claim_number' => (string) ($claim->claim_number ?? ''),
|
||||
'policy_reference' => (string) ($claim->policy_reference ?? ''),
|
||||
'policy_label' => trim(implode(' · ', array_filter([
|
||||
(string) ($claim->insurancePolicy->company_name ?? ''),
|
||||
(string) ($claim->insurancePolicy->policy_number ?? ''),
|
||||
]))),
|
||||
'status' => (string) ($claim->status ?? 'aperta'),
|
||||
'opened_at' => optional($claim->opened_at)->format('d/m/Y H:i') ?: '-',
|
||||
'next_action' => (string) (data_get($claim->metadata, 'next_action') ?? ''),
|
||||
'status' => (string) ($claim->status ?? 'aperta'),
|
||||
'opened_at' => optional($claim->opened_at)->format('d/m/Y H:i') ?: '-',
|
||||
'next_action' => (string) (data_get($claim->metadata, 'next_action') ?? ''),
|
||||
'documents_sent_at' => filled(data_get($claim->metadata, 'documents_sent_at')) ? (string) data_get($claim->metadata, 'documents_sent_at') : '',
|
||||
'ticket_url' => TicketGestione::getUrl([
|
||||
'ticket_url' => TicketGestione::getUrl([
|
||||
'ticket' => (int) $claim->ticket_id,
|
||||
'tab' => 'assicurazione',
|
||||
'tab' => 'assicurazione',
|
||||
], panel: 'admin-filament'),
|
||||
];
|
||||
})
|
||||
|
|
@ -121,7 +121,7 @@ public function refreshData(): void
|
|||
$this->totals = [
|
||||
'aperte' => (clone $base)->whereNotIn('status', ['chiusa', 'chiuso', 'archiviata'])->count(),
|
||||
'chiuse' => (clone $base)->whereIn('status', ['chiusa', 'chiuso', 'archiviata'])->count(),
|
||||
'tutte' => (clone $base)->count(),
|
||||
'tutte' => (clone $base)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -142,4 +142,4 @@ private function resolveAccessibleStabileIds(): array
|
|||
->values()
|
||||
->all();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,8 +231,8 @@ public function canRunUpdate(): bool
|
|||
$user = Auth::user();
|
||||
|
||||
return is_object($user)
|
||||
&& method_exists($user, 'hasAnyRole')
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore']);
|
||||
&& method_exists($user, 'hasAnyRole')
|
||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore']);
|
||||
}
|
||||
|
||||
public function reloadDashboardData(): void
|
||||
|
|
|
|||
|
|
@ -610,18 +610,18 @@ public function creaTicketRapido(): void
|
|||
$descrizione .= "\nTipo intervento: " . $label;
|
||||
}
|
||||
|
||||
$ticket = Ticket::query()->create([
|
||||
'stabile_id' => $stabileId,
|
||||
'unita_immobiliare_id'=> $callerContext['unita_id'] ?? null,
|
||||
$ticket = Ticket::query()->create([
|
||||
'stabile_id' => $stabileId,
|
||||
'unita_immobiliare_id' => $callerContext['unita_id'] ?? null,
|
||||
'soggetto_richiedente_id' => $callerContext['soggetto_id'] ?? null,
|
||||
'aperto_da_user_id' => $user->id,
|
||||
'titolo' => (string) $this->newTicketTitolo,
|
||||
'descrizione' => $descrizione,
|
||||
'categoria_ticket_id' => $this->newTicketCategoriaId,
|
||||
'luogo_intervento' => $this->newTicketLuogo,
|
||||
'data_apertura' => now(),
|
||||
'stato' => 'Aperto',
|
||||
'priorita' => $this->newTicketPriorita,
|
||||
'aperto_da_user_id' => $user->id,
|
||||
'titolo' => (string) $this->newTicketTitolo,
|
||||
'descrizione' => $descrizione,
|
||||
'categoria_ticket_id' => $this->newTicketCategoriaId,
|
||||
'luogo_intervento' => $this->newTicketLuogo,
|
||||
'data_apertura' => now(),
|
||||
'stato' => 'Aperto',
|
||||
'priorita' => $this->newTicketPriorita,
|
||||
]);
|
||||
|
||||
$savedAttachments = $this->salvaAllegatiTicket($ticket, $user->id);
|
||||
|
|
@ -1178,7 +1178,7 @@ public function excerpt(?string $text, int $limit = 160): string
|
|||
|
||||
public function getCallerStabileLabel(int $rubricaId): ?string
|
||||
{
|
||||
$caller = RubricaUniversale::query()->find($rubricaId);
|
||||
$caller = RubricaUniversale::query()->find($rubricaId);
|
||||
$stabile = $caller instanceof RubricaUniversale
|
||||
? $this->resolveCallerStabile($caller)
|
||||
: null;
|
||||
|
|
@ -1213,7 +1213,7 @@ private function resolveCallerTicketContext(?RubricaUniversale $caller, ?int $fa
|
|||
|
||||
return [
|
||||
'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,
|
||||
'unita_id' => $unita?->id,
|
||||
'unita_label' => $unita ? $this->formatUnitaLabel($unita) : '',
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ public function redirect(): RedirectResponse
|
|||
}
|
||||
|
||||
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
|
||||
$oauth = Arr::get($google, 'oauth', []);
|
||||
|
||||
$clientId = (string) ($google['client_id'] ?? config('services.google.client_id'));
|
||||
$clientSecret = (string) ($google['client_secret'] ?? config('services.google.client_secret'));
|
||||
|
|
@ -48,16 +49,21 @@ public function redirect(): RedirectResponse
|
|||
$state = bin2hex(random_bytes(20));
|
||||
session()->put('google_oauth_state', $state);
|
||||
|
||||
$query = http_build_query([
|
||||
$queryParams = [
|
||||
'client_id' => $clientId,
|
||||
'redirect_uri' => $redirectUri,
|
||||
'response_type' => 'code',
|
||||
'scope' => implode(' ', self::GOOGLE_SCOPES),
|
||||
'access_type' => 'offline',
|
||||
'prompt' => 'consent',
|
||||
'include_granted_scopes' => 'true',
|
||||
'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);
|
||||
}
|
||||
|
|
@ -71,6 +77,7 @@ public function callback(): RedirectResponse
|
|||
}
|
||||
|
||||
$google = Arr::get($amministratore->impostazioni ?? [], 'google', []);
|
||||
$existingOauth = Arr::get($google, 'oauth', []);
|
||||
$clientId = (string) ($google['client_id'] ?? config('services.google.client_id'));
|
||||
$clientSecret = (string) ($google['client_secret'] ?? config('services.google.client_secret'));
|
||||
$redirectUri = (string) ($google['redirect_uri'] ?? config('services.google.redirect'));
|
||||
|
|
@ -106,6 +113,9 @@ public function callback(): RedirectResponse
|
|||
$payload = $tokenResponse->json();
|
||||
$accessToken = (string) Arr::get($payload, 'access_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);
|
||||
|
||||
$profileResponse = Http::withToken($accessToken)
|
||||
|
|
@ -128,6 +138,7 @@ public function callback(): RedirectResponse
|
|||
'refresh_token' => $refreshToken,
|
||||
'expires_in' => $expiresIn,
|
||||
'connected_at' => now()->toDateTimeString(),
|
||||
'refreshed_at' => now()->toDateTimeString(),
|
||||
],
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -77,4 +77,4 @@ public function closedByUser()
|
|||
{
|
||||
return $this->belongsTo(User::class, 'closed_by_user_id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,17 +109,17 @@ public function resolveByIncomingLine(?string $line): array
|
|||
$targetRouting = $targetExtension !== ''
|
||||
? $this->resolveByExtension($targetExtension)
|
||||
: ($routeGroup !== '' ? $this->resolveByExtension($routeGroup) : [
|
||||
'user_id' => null,
|
||||
'stabile_id' => null,
|
||||
'user' => null,
|
||||
]);
|
||||
'user_id' => null,
|
||||
'stabile_id' => null,
|
||||
'user' => null,
|
||||
]);
|
||||
|
||||
if (($targetRouting['user'] ?? null) === null) {
|
||||
$mappedUser = $this->findUserByLineMapping($amministratore, $normalized);
|
||||
if ($mappedUser instanceof User) {
|
||||
$targetRouting = [
|
||||
'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,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,8 +83,8 @@ private function shouldOptimizeImage(array $metadata, array $options = []): bool
|
|||
private function hasEmbeddedImageMetadata(array $metadata): bool
|
||||
{
|
||||
return filled($metadata['exif_datetime'] ?? null)
|
||||
|| filled($metadata['device'] ?? null)
|
||||
|| isset($metadata['gps_decimal'])
|
||||
|| filled($metadata['device'] ?? null)
|
||||
|| isset($metadata['gps_decimal'])
|
||||
|| ((int) ($metadata['orientation'] ?? 1)) !== 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,4 +57,4 @@ public function down(): void
|
|||
Schema::dropIfExists('ticket_intervento_sessioni');
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ # NetGescon — TODO Copilot Tracker
|
|||
Data aggiornamento: 6 aprile 2026
|
||||
|
||||
Questo file tiene traccia di:
|
||||
|
||||
- cosa è stato fatto (implementato e/o corretto)
|
||||
- cosa resta da fare
|
||||
- priorità e dipendenze
|
||||
|
|
@ -12,22 +13,27 @@ # NetGescon — TODO Copilot Tracker
|
|||
## 1) Fatto (stato attuale)
|
||||
|
||||
### UX / Filament (navigazione “web-like”)
|
||||
|
||||
- Breadcrumbs compatibili Filament: wrapper view per `<x-filament.components.page-breadcrumbs>`.
|
||||
- Pagine Filament con back/breadcrumb coerenti (dove già applicato).
|
||||
|
||||
### Anagrafica
|
||||
|
||||
- `Persona`: gestione multi-ruolo via JSON `persone.ruoli` + UI di editing in Filament.
|
||||
|
||||
### Contabilità — base
|
||||
|
||||
- Tabelle contabilità e migrazioni sbloccate (incl. `contabilita_regole_prima_nota`, movimenti banca).
|
||||
- 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).
|
||||
|
||||
### Fatture ricevute XML (FatturaPA)
|
||||
|
||||
- Archivio + scheda Filament per fatture ricevute XML.
|
||||
- Import multiplo XML con dedup (hash + chiave logica) e persistenza su `fatture_elettroniche`.
|
||||
|
||||
### Scheda Stabile (riabilitata)
|
||||
|
||||
- Ripristinato accesso dalla Filament “Cruscotto stabile” alla scheda stabile completa (UI classica) con:
|
||||
- Dati stabile
|
||||
- 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.
|
||||
|
||||
### “Vista Excel” (riabilitata)
|
||||
|
||||
- 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.
|
||||
|
||||
### Nuovo lavoro già avviato (base dati + UI)
|
||||
|
||||
- Nuove tabelle per “fatture fornitori” (testata + righe, importi `_euro`, ritenuta, tab pagamento).
|
||||
- Prime pagine Filament:
|
||||
- Archivio fatture fornitori
|
||||
|
|
@ -51,17 +59,21 @@ ### Nuovo lavoro già avviato (base dati + UI)
|
|||
## 2) In corso (priorità alta)
|
||||
|
||||
### A) Riallineare le “Gestioni” per la contabilità
|
||||
|
||||
Obiettivo: avere gestione ordinaria/riscaldamento/straordinaria *coerenti* e selezionabili per stabile, perché la contabilità deve agganciarsi a:
|
||||
|
||||
- anno gestione / esercizio
|
||||
- tipo gestione
|
||||
- conti/casse/banca
|
||||
- registrazioni/movimenti
|
||||
|
||||
Output atteso:
|
||||
|
||||
- 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
|
||||
|
||||
Dipendenze:
|
||||
|
||||
- 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.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.
|
||||
- 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.
|
||||
|
|
@ -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.
|
||||
|
||||
### 3.1 Contabilità — fatture fornitori “alla Gescon”
|
||||
|
||||
- Rendere la maschera fattura fornitore più fedele allo screenshot (nomi/ordine tab + colonna azioni).
|
||||
- Aggancio/creazione fornitore (contabilità + anagrafica unica/rubrica) direttamente dalla maschera.
|
||||
- Contabilizzazione:
|
||||
|
|
@ -85,6 +99,7 @@ ### 3.1 Contabilità — fatture fornitori “alla Gescon”
|
|||
- RA (ritenuta) come debito separato da versare
|
||||
|
||||
### 3.2 Ritenute d’acconto + F24
|
||||
|
||||
- Gestione codici tributo: 1019/1020 e 1040.
|
||||
- Regole:
|
||||
- RA si versa con F24 entro il 16 del mese successivo al pagamento.
|
||||
|
|
@ -94,6 +109,7 @@ ### 3.2 Ritenute d’acconto + F24
|
|||
- (fase successiva) aggregazione per mese/tributo e stampa/distinta
|
||||
|
||||
### 3.3 Import MDB (GESCON) → contabilità operativa
|
||||
|
||||
- Predisporre import da `singolo_stabile.MDB` e `generale_stabile.mdb`:
|
||||
- operazioni/fatture/fornitori
|
||||
- rispettare sempre i campi `*_euro`
|
||||
|
|
@ -103,6 +119,7 @@ ### 3.3 Import MDB (GESCON) → contabilità operativa
|
|||
- collegamenti a tabelle millesimali/voci spesa
|
||||
|
||||
### 3.4 XML multiplo → passaggio in contabilità
|
||||
|
||||
- Oggi: import XML alimenta archivio `fatture_elettroniche`.
|
||||
- Da fare:
|
||||
- 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)
|
||||
|
||||
### Ticket
|
||||
|
||||
- Esiste base legacy (controller/CRUD e categorie ticket). Da definire:
|
||||
- UI Filament per ticket (lista, scheda, stati, priorità)
|
||||
- permessi/ruoli
|
||||
- collegamento ticket ↔ stabile ↔ documenti
|
||||
|
||||
### Gestione documentale
|
||||
|
||||
- Esiste sistema “Documenti collegati” (con rotte e API statistiche/scadenze). Da definire:
|
||||
- UI Filament per documenti del condominio
|
||||
- workflow scadenze/rinnovi
|
||||
- permessi e visibilità per ruolo
|
||||
|
||||
### Protocollo documentazione
|
||||
|
||||
- Da definire:
|
||||
- numerazione protocolli per stabile/anno
|
||||
- registrazione (data, mittente/destinatario, oggetto)
|
||||
- collegamento a documenti/pec
|
||||
|
||||
### Consumi / acqua / contatori
|
||||
|
||||
- Backend (tabelle contatori/letture/algoritmi) esiste.
|
||||
- UI Filament non ancora esposta: oggi la gestione è in view legacy.
|
||||
|
||||
---
|
||||
|
||||
## 5) Note operative / rischi
|
||||
|
||||
- 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”).
|
||||
- Import MDB richiede mapping rigoroso (soprattutto `_euro`) per evitare contabilità sbilanciata.
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ ## Topics
|
|||
|
||||
- [Fatture Fornitori](topics/fatture-fornitori.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)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Filo Logico Contabilita
|
||||
Import Unita Cod Cond
|
||||
|
|
|
|||
57
docs/ai/topics/import-unita-cod-cond.md
Normal file
57
docs/ai/topics/import-unita-cod-cond.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
||||
### 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:
|
||||
|
||||
- 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`
|
||||
4. aspettare la conclusione del sync Git e della QA sicura inclusa
|
||||
5. validare subito i punti visibili:
|
||||
- Ticket Mobile mostra l anteprima prima di inviare
|
||||
- Ticket Gestione mostra `Tab 4 - Assicurazioni`
|
||||
- `/admin-filament/fornitore/tickets/{id}` non produce 403 per i casi operativi normali
|
||||
- la lista ticket fornitore apre anche la modal rapida
|
||||
|
||||
- Ticket Mobile mostra l anteprima prima di inviare
|
||||
- Ticket Gestione mostra `Tab 4 - Assicurazioni`
|
||||
- `/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:
|
||||
|
||||
|
|
|
|||
|
|
@ -162,4 +162,4 @@ ## 10. Prossimi slice consigliati
|
|||
2. estendere checkpoint presenza con QR/BLE/NFC dove il device/browser lo consente davvero
|
||||
3. estensione collaboratori con disponibilita/orari
|
||||
4. ticket interni/lavorazioni ricorrenti dentro `Lavorazioni operative`
|
||||
5. staging SQL TecnoRepair per rubrica, schede e seriali
|
||||
5. staging SQL TecnoRepair per rubrica, schede e seriali
|
||||
|
|
|
|||
|
|
@ -3,6 +3,37 @@
|
|||
/** @var \App\Models\Stabile|null $stabileAttivo */
|
||||
$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 {
|
||||
if (! $stabile) {
|
||||
return '';
|
||||
|
|
@ -28,7 +59,7 @@
|
|||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
Ordinari: {{ ($stabileAttivo->numero_rate_ordinarie ?? 0) > 0 ? 'Sì' : 'No' }} ·
|
||||
Riscaldamento: {{ (bool) ($stabileAttivo->riscaldamento_centralizzato ?? false) ? 'Sì' : 'No' }}
|
||||
Riscaldamento: {{ $stabileHasRiscaldamento($stabileAttivo) ? 'Sì' : 'No' }}
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<x-filament-panels::page>
|
||||
@php($supportUpdateAuthUser = auth()->user())
|
||||
@php($supportCanRunUpdate = $supportUpdateAuthUser && method_exists($supportUpdateAuthUser, 'hasAnyRole') && $supportUpdateAuthUser->hasAnyRole(['super-admin', 'admin', 'amministratore']))
|
||||
@php
|
||||
$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="rounded-xl border border-sky-200 bg-sky-50 p-4">
|
||||
|
|
|
|||
|
|
@ -88,7 +88,9 @@
|
|||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
@php($selected = $this->selectedScheda)
|
||||
@php
|
||||
$selected = $this->selectedScheda;
|
||||
@endphp
|
||||
@if(! $selected)
|
||||
<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.
|
||||
|
|
@ -135,7 +137,9 @@
|
|||
<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">
|
||||
@if($selected->fornitore)
|
||||
@php($fornitoreUrl = $this->getFornitoreUrl((int) $selected->fornitore->id))
|
||||
@php
|
||||
$fornitoreUrl = $this->getFornitoreUrl((int) $selected->fornitore->id);
|
||||
@endphp
|
||||
@if($fornitoreUrl)
|
||||
<a href="{{ $fornitoreUrl }}" class="font-medium text-sky-700 hover:text-sky-600">{{ $selected->fornitore->ragione_sociale ?: ('Fornitore #' . $selected->fornitore->id) }}</a>
|
||||
@else
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@
|
|||
Ticket non selezionato o non accessibile. Apri questa pagina da Gestione Ticket.
|
||||
</div>
|
||||
@else
|
||||
@php($ticket = $this->ticket)
|
||||
@php
|
||||
$ticket = $this->ticket;
|
||||
@endphp
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<div class="text-lg font-semibold">#{{ $ticket->id }} - {{ $ticket->titolo }}</div>
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user