Fix supplier FE access and archive isolation
This commit is contained in:
parent
2810d2c70e
commit
19453c050f
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\FatturaElettronica;
|
use App\Models\FatturaElettronica;
|
||||||
|
|
@ -13,7 +12,8 @@ class GesconFixFattureFornitoriLinkCommand extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'gescon:fatture-fix-fornitore-link
|
protected $signature = 'gescon:fatture-fix-fornitore-link
|
||||||
{stabile_id : ID dello stabile}
|
{stabile_id : ID dello stabile}
|
||||||
{--dry-run : Non scrive, mostra solo cosa aggiornerebbe}';
|
{--dry-run : Non scrive, mostra solo cosa aggiornerebbe}
|
||||||
|
{--fix-mismatches : Corregge anche i fornitore_id gia valorizzati ma errati}';
|
||||||
|
|
||||||
protected $description = 'Backfill: collega le fatture fornitori contabili al fornitore corretto usando PIVA/CF della Fattura Elettronica.';
|
protected $description = 'Backfill: collega le fatture fornitori contabili al fornitore corretto usando PIVA/CF della Fattura Elettronica.';
|
||||||
|
|
||||||
|
|
@ -25,7 +25,8 @@ public function handle(): int
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$dry = (bool) $this->option('dry-run');
|
$dry = (bool) $this->option('dry-run');
|
||||||
|
$fixMismatches = (bool) $this->option('fix-mismatches');
|
||||||
|
|
||||||
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
|
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
|
||||||
if (! $stabile) {
|
if (! $stabile) {
|
||||||
|
|
@ -39,13 +40,23 @@ public function handle(): int
|
||||||
return self::FAILURE;
|
return self::FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$fatture = FatturaFornitore::query()
|
$fattureQuery = FatturaFornitore::query()
|
||||||
->where('stabile_id', $stabileId)
|
->where('stabile_id', $stabileId)
|
||||||
->whereNull('fornitore_id')
|
->whereNotNull('fattura_elettronica_id');
|
||||||
|
|
||||||
|
if (! $fixMismatches) {
|
||||||
|
$fattureQuery->whereNull('fornitore_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fatture = $fattureQuery
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get(['id', 'fattura_elettronica_id', 'fornitore_id']);
|
->get(['id', 'fattura_elettronica_id', 'fornitore_id']);
|
||||||
|
|
||||||
$this->info("Fatture contabili senza fornitore_id: " . $fatture->count());
|
$this->info(
|
||||||
|
$fixMismatches
|
||||||
|
? 'Fatture contabili da verificare: ' . $fatture->count()
|
||||||
|
: 'Fatture contabili senza fornitore_id: ' . $fatture->count()
|
||||||
|
);
|
||||||
|
|
||||||
$updated = 0;
|
$updated = 0;
|
||||||
$skipped = 0;
|
$skipped = 0;
|
||||||
|
|
@ -64,7 +75,7 @@ public function handle(): int
|
||||||
}
|
}
|
||||||
|
|
||||||
$piva = $this->normalizeId($fe->fornitore_piva);
|
$piva = $this->normalizeId($fe->fornitore_piva);
|
||||||
$cf = $this->normalizeId($fe->fornitore_cf);
|
$cf = $this->normalizeId($fe->fornitore_cf);
|
||||||
|
|
||||||
$fornitoreId = $this->matchFornitoreIdForAdmin($adminId, $piva, $cf);
|
$fornitoreId = $this->matchFornitoreIdForAdmin($adminId, $piva, $cf);
|
||||||
if (! $fornitoreId) {
|
if (! $fornitoreId) {
|
||||||
|
|
@ -72,6 +83,11 @@ public function handle(): int
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((int) ($fattura->fornitore_id ?: 0) === $fornitoreId) {
|
||||||
|
$skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$updated++;
|
$updated++;
|
||||||
|
|
||||||
if ($dry) {
|
if ($dry) {
|
||||||
|
|
@ -83,7 +99,7 @@ public function handle(): int
|
||||||
$fattura->fornitore_id = $fornitoreId;
|
$fattura->fornitore_id = $fornitoreId;
|
||||||
$fattura->save();
|
$fattura->save();
|
||||||
|
|
||||||
if ((int) ($fe->fornitore_id ?: 0) <= 0) {
|
if ((int) ($fe->fornitore_id ?: 0) !== $fornitoreId) {
|
||||||
$fe->fornitore_id = $fornitoreId;
|
$fe->fornitore_id = $fornitoreId;
|
||||||
$fe->save();
|
$fe->save();
|
||||||
}
|
}
|
||||||
|
|
@ -92,10 +108,11 @@ public function handle(): int
|
||||||
|
|
||||||
$this->info('✅ Completato' . ($dry ? ' (dry-run)' : ''));
|
$this->info('✅ Completato' . ($dry ? ' (dry-run)' : ''));
|
||||||
$this->line(json_encode([
|
$this->line(json_encode([
|
||||||
'stabile_id' => $stabileId,
|
'stabile_id' => $stabileId,
|
||||||
'amministratore_id' => $adminId,
|
'amministratore_id' => $adminId,
|
||||||
'updated' => $updated,
|
'fix_mismatches' => $fixMismatches,
|
||||||
'skipped' => $skipped,
|
'updated' => $updated,
|
||||||
|
'skipped' => $skipped,
|
||||||
], JSON_UNESCAPED_UNICODE));
|
], JSON_UNESCAPED_UNICODE));
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
|
|
@ -122,7 +139,7 @@ private function matchFornitoreIdForAdmin(int $adminId, string $pivaNorm, string
|
||||||
|
|
||||||
if (str_starts_with($pivaNorm, 'IT') && strlen($pivaNorm) > 2) {
|
if (str_starts_with($pivaNorm, 'IT') && strlen($pivaNorm) > 2) {
|
||||||
$pivaNoIt = substr($pivaNorm, 2);
|
$pivaNoIt = substr($pivaNorm, 2);
|
||||||
$match = (clone $base)
|
$match = (clone $base)
|
||||||
->whereRaw("REPLACE(UPPER(partita_iva), ' ', '') = ?", [$pivaNoIt])
|
->whereRaw("REPLACE(UPPER(partita_iva), ' ', '') = ?", [$pivaNoIt])
|
||||||
->value('id');
|
->value('id');
|
||||||
if ($match) {
|
if ($match) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
namespace App\Filament\Pages\Contabilita;
|
namespace App\Filament\Pages\Contabilita;
|
||||||
|
|
||||||
use App\Filament\Pages\Contabilita\FattureElettronicheArchivio;
|
use App\Filament\Pages\Contabilita\FattureElettronicheArchivio;
|
||||||
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||||
use App\Models\Documento;
|
use App\Models\Documento;
|
||||||
use App\Models\FatturaElettronica;
|
use App\Models\FatturaElettronica;
|
||||||
use App\Models\FeXsdCode;
|
use App\Models\FeXsdCode;
|
||||||
|
|
@ -34,6 +35,8 @@
|
||||||
|
|
||||||
class FatturaElettronicaScheda extends Page
|
class FatturaElettronicaScheda extends Page
|
||||||
{
|
{
|
||||||
|
use ResolvesOperatoreContext;
|
||||||
|
|
||||||
protected static ?string $title = 'Scheda fattura ricevuta';
|
protected static ?string $title = 'Scheda fattura ricevuta';
|
||||||
|
|
||||||
protected static ?string $slug = 'contabilita/fatture-ricevute/{record}';
|
protected static ?string $slug = 'contabilita/fatture-ricevute/{record}';
|
||||||
|
|
@ -133,12 +136,17 @@ public function mount(int | string $record): void
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $user->hasAnyRole(['super-admin', 'admin']) && ! $user->can('contabilita.fatture-elettroniche')) {
|
if (! $user->hasAnyRole(['super-admin', 'admin']) && ! $user->hasRole('fornitore') && ! $user->can('contabilita.fatture-elettroniche')) {
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->fattura = FatturaElettronica::query()->with(['stabile', 'fornitore'])->findOrFail((int) $record);
|
$this->fattura = FatturaElettronica::query()->with(['stabile', 'fornitore'])->findOrFail((int) $record);
|
||||||
|
|
||||||
|
if ($user->hasRole('fornitore')) {
|
||||||
|
[$fornitore] = $this->resolveOperatoreContext();
|
||||||
|
abort_unless($fornitore instanceof Fornitore && (int) $this->fattura->fornitore_id === (int) $fornitore->id, 403);
|
||||||
|
}
|
||||||
|
|
||||||
$this->documento = Documento::query()
|
$this->documento = Documento::query()
|
||||||
->where('documentable_type', FatturaElettronica::class)
|
->where('documentable_type', FatturaElettronica::class)
|
||||||
->where('documentable_id', (int) $this->fattura->id)
|
->where('documentable_id', (int) $this->fattura->id)
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@
|
||||||
namespace App\Filament\Pages\Contabilita;
|
namespace App\Filament\Pages\Contabilita;
|
||||||
|
|
||||||
use App\Filament\Pages\Contabilita\FatturaElettronicaScheda;
|
use App\Filament\Pages\Contabilita\FatturaElettronicaScheda;
|
||||||
use App\Models\Documento;
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||||
use App\Jobs\RunCassettoFiscaleDownload;
|
use App\Jobs\RunCassettoFiscaleDownload;
|
||||||
|
use App\Models\Documento;
|
||||||
use App\Models\FatturaElettronica;
|
use App\Models\FatturaElettronica;
|
||||||
|
use App\Models\Fornitore;
|
||||||
use App\Models\Stabile;
|
use App\Models\Stabile;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Services\Consumi\AcquaPdfTextParser;
|
use App\Services\Consumi\AcquaPdfTextParser;
|
||||||
|
|
@ -47,6 +49,7 @@
|
||||||
class FattureElettronicheP7mRicevute extends Page implements HasTable
|
class FattureElettronicheP7mRicevute extends Page implements HasTable
|
||||||
{
|
{
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
use ResolvesOperatoreContext;
|
||||||
|
|
||||||
public array $quarterDownloads = [];
|
public array $quarterDownloads = [];
|
||||||
|
|
||||||
|
|
@ -125,7 +128,7 @@ private function autoLinkWaterReadingsForInvoice(int $fatturaId, int $userId): a
|
||||||
return ['status' => 'skipped', 'message' => 'Fattura non trovata'];
|
return ['status' => 'skipped', 'message' => 'Fattura non trovata'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$hasPdf = is_string($fattura->allegato_pdf_path ?? null) && trim((string) $fattura->allegato_pdf_path) !== '';
|
$hasPdf = is_string($fattura->allegato_pdf_path ?? null) && trim((string) $fattura->allegato_pdf_path) !== '';
|
||||||
$hasStoredPayload = is_string($fattura->consumo_raw ?? null) && trim((string) $fattura->consumo_raw) !== '';
|
$hasStoredPayload = is_string($fattura->consumo_raw ?? null) && trim((string) $fattura->consumo_raw) !== '';
|
||||||
if (! $hasPdf && ! $hasStoredPayload) {
|
if (! $hasPdf && ! $hasStoredPayload) {
|
||||||
return ['status' => 'skipped', 'message' => 'Nessun PDF/payload acqua disponibile'];
|
return ['status' => 'skipped', 'message' => 'Nessun PDF/payload acqua disponibile'];
|
||||||
|
|
@ -173,15 +176,15 @@ private function autoLinkWaterReadingsForInvoice(int $fatturaId, int $userId): a
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [];
|
$codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [];
|
||||||
$contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [];
|
$contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [];
|
||||||
$consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : [];
|
$consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : [];
|
||||||
$generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [];
|
$generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [];
|
||||||
$finestra = is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : [];
|
$finestra = is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : [];
|
||||||
$letture = is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : [];
|
$letture = is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : [];
|
||||||
$quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [];
|
$quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [];
|
||||||
$tariffe = is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : [];
|
$tariffe = is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : [];
|
||||||
$ivaInfo = is_array($parsed['iva'] ?? null) ? $parsed['iva'] : [];
|
$ivaInfo = is_array($parsed['iva'] ?? null) ? $parsed['iva'] : [];
|
||||||
|
|
||||||
$hasAny = false;
|
$hasAny = false;
|
||||||
foreach ([$codici['utenza'] ?? null, $codici['cliente'] ?? null, $codici['contratto'] ?? null, $contatore['matricola'] ?? null] as $value) {
|
foreach ([$codici['utenza'] ?? null, $codici['cliente'] ?? null, $codici['contratto'] ?? null, $contatore['matricola'] ?? null] as $value) {
|
||||||
|
|
@ -227,15 +230,15 @@ private function autoLinkWaterReadingsForInvoice(int $fatturaId, int $userId): a
|
||||||
'parsed_at' => now()->toISOString(),
|
'parsed_at' => now()->toISOString(),
|
||||||
];
|
];
|
||||||
|
|
||||||
$totalMc = null;
|
$totalMc = null;
|
||||||
$reference = null;
|
$reference = null;
|
||||||
if ($consumi !== []) {
|
if ($consumi !== []) {
|
||||||
$sum = 0.0;
|
$sum = 0.0;
|
||||||
$hasNumeric = false;
|
$hasNumeric = false;
|
||||||
foreach ($consumi as $consumo) {
|
foreach ($consumi as $consumo) {
|
||||||
if (isset($consumo['valore']) && is_numeric($consumo['valore'])) {
|
if (isset($consumo['valore']) && is_numeric($consumo['valore'])) {
|
||||||
$sum += (float) $consumo['valore'];
|
$sum += (float) $consumo['valore'];
|
||||||
$hasNumeric = true;
|
$hasNumeric = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($hasNumeric) {
|
if ($hasNumeric) {
|
||||||
|
|
@ -243,17 +246,17 @@ private function autoLinkWaterReadingsForInvoice(int $fatturaId, int $userId): a
|
||||||
}
|
}
|
||||||
|
|
||||||
$first = $consumi[0] ?? [];
|
$first = $consumi[0] ?? [];
|
||||||
$dal = $first['dal'] ?? null;
|
$dal = $first['dal'] ?? null;
|
||||||
$al = $first['al'] ?? null;
|
$al = $first['al'] ?? null;
|
||||||
if (is_string($dal) && is_string($al) && $dal !== '' && $al !== '') {
|
if (is_string($dal) && is_string($al) && $dal !== '' && $al !== '') {
|
||||||
$reference = $dal . ' - ' . $al;
|
$reference = $dal . ' - ' . $al;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$fattura->consumo_unita = 'mc';
|
$fattura->consumo_unita = 'mc';
|
||||||
$fattura->consumo_valore = $totalMc;
|
$fattura->consumo_valore = $totalMc;
|
||||||
$fattura->consumo_riferimento = $reference;
|
$fattura->consumo_riferimento = $reference;
|
||||||
$fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
$fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
$fattura->save();
|
$fattura->save();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -584,6 +587,10 @@ public static function canAccess(): bool
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('fornitore')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return $user->can('contabilita.fatture-elettroniche');
|
return $user->can('contabilita.fatture-elettroniche');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1171,9 +1178,9 @@ protected function getHeaderActions(): array
|
||||||
$accessibleStabili = StabileContext::accessibleStabili($user)->pluck('id')->map(fn($v) => (int) $v)->all();
|
$accessibleStabili = StabileContext::accessibleStabili($user)->pluck('id')->map(fn($v) => (int) $v)->all();
|
||||||
$accessibleMap = array_fill_keys($accessibleStabili, true);
|
$accessibleMap = array_fill_keys($accessibleStabili, true);
|
||||||
|
|
||||||
$imported = 0;
|
$imported = 0;
|
||||||
$duplicates = 0;
|
$duplicates = 0;
|
||||||
$errors = 0;
|
$errors = 0;
|
||||||
$waterCreated = 0;
|
$waterCreated = 0;
|
||||||
$waterUpdated = 0;
|
$waterUpdated = 0;
|
||||||
|
|
||||||
|
|
@ -1744,6 +1751,17 @@ protected function getTableQuery(): Builder
|
||||||
return FatturaElettronica::query()->whereRaw('1 = 0');
|
return FatturaElettronica::query()->whereRaw('1 = 0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($user->hasRole('fornitore')) {
|
||||||
|
[$fornitore] = $this->resolveOperatoreContext();
|
||||||
|
|
||||||
|
if (! $fornitore instanceof Fornitore) {
|
||||||
|
return FatturaElettronica::query()->whereRaw('1 = 0');
|
||||||
|
}
|
||||||
|
|
||||||
|
return FatturaElettronica::query()
|
||||||
|
->where('fornitore_id', (int) $fornitore->id);
|
||||||
|
}
|
||||||
|
|
||||||
$activeStabileId = StabileContext::resolveActiveStabileId($user);
|
$activeStabileId = StabileContext::resolveActiveStabileId($user);
|
||||||
if (! $activeStabileId) {
|
if (! $activeStabileId) {
|
||||||
return FatturaElettronica::query()->whereRaw('1 = 0');
|
return FatturaElettronica::query()->whereRaw('1 = 0');
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,31 @@
|
||||||
<?php
|
<?php
|
||||||
namespace App\Filament\Pages\Fornitore;
|
namespace App\Filament\Pages\Fornitore;
|
||||||
|
|
||||||
|
use App\Filament\Pages\Contabilita\FattureElettronicheP7mRicevute;
|
||||||
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||||
|
use App\Models\Amministratore;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
|
use App\Models\Stabile;
|
||||||
use App\Models\TicketIntervento;
|
use App\Models\TicketIntervento;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Modules\Contabilita\Models\FatturaFornitore;
|
use App\Modules\Contabilita\Models\FatturaFornitore;
|
||||||
|
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
||||||
|
use App\Services\FattureElettroniche\FatturaElettronicaXmlParser;
|
||||||
|
use App\Services\FattureElettroniche\P7mExtractor;
|
||||||
|
use App\Support\ArchivioPaths;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Forms\Components\FileUpload;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
use ZipArchive;
|
||||||
|
|
||||||
class Contabilita extends Page
|
class Contabilita extends Page
|
||||||
{
|
{
|
||||||
|
|
@ -93,6 +108,121 @@ public function getCollaboratoriUrl(): string
|
||||||
return Collaboratori::getUrl(panel: 'admin-filament');
|
return Collaboratori::getUrl(panel: 'admin-filament');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFattureRicevuteUrl(): string
|
||||||
|
{
|
||||||
|
return FattureElettronicheP7mRicevute::getUrl(panel: 'admin-filament');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getHeaderActions(): array
|
||||||
|
{
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
Action::make('importa_fe_manuale')
|
||||||
|
->label('Importa FE manuale')
|
||||||
|
->icon('heroicon-o-arrow-up-tray')
|
||||||
|
->modalWidth('4xl')
|
||||||
|
->form([
|
||||||
|
FileUpload::make('fe_files')
|
||||||
|
->label('File FE (XML/P7M/ZIP)')
|
||||||
|
->directory(fn(): string => $this->getSupplierFeUploadDirectory())
|
||||||
|
->disk('local')
|
||||||
|
->acceptedFileTypes([
|
||||||
|
'.p7m',
|
||||||
|
'.xml',
|
||||||
|
'.zip',
|
||||||
|
'application/xml',
|
||||||
|
'text/xml',
|
||||||
|
'application/pkcs7-mime',
|
||||||
|
'application/x-pkcs7-mime',
|
||||||
|
'application/pkcs7-signature',
|
||||||
|
'application/x-pkcs7-signature',
|
||||||
|
'application/octet-stream',
|
||||||
|
'application/zip',
|
||||||
|
'application/x-zip-compressed',
|
||||||
|
])
|
||||||
|
->multiple()
|
||||||
|
->required(),
|
||||||
|
Select::make('importa_righe')
|
||||||
|
->label('Import righe')
|
||||||
|
->options([
|
||||||
|
'auto' => 'Auto (segue impostazione fornitore)',
|
||||||
|
'si' => 'Si (forza import righe)',
|
||||||
|
'no' => 'No (non importare righe)',
|
||||||
|
])
|
||||||
|
->default('auto')
|
||||||
|
->required(),
|
||||||
|
Toggle::make('force_update')
|
||||||
|
->label('Reimport / completa se gia presente')
|
||||||
|
->default(false),
|
||||||
|
])
|
||||||
|
->action(function (array $data): void {
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
Notification::make()->title('Fornitore non disponibile')->danger()->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->supplierHasTaxIdentity()) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Import FE bloccato')
|
||||||
|
->body('Il fornitore corrente non ha P.IVA o codice fiscale valorizzati: non posso verificare che l XML appartenga davvero a questa anagrafica.')
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$paths = $data['fe_files'] ?? [];
|
||||||
|
$paths = is_array($paths) ? $paths : [$paths];
|
||||||
|
|
||||||
|
$parser = new FatturaElettronicaXmlParser();
|
||||||
|
$importer = new FatturaElettronicaImporter($parser);
|
||||||
|
$extractor = app(P7mExtractor::class);
|
||||||
|
|
||||||
|
$summary = [
|
||||||
|
'imported' => 0,
|
||||||
|
'duplicates' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'errors' => 0,
|
||||||
|
'messages' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
$result = $this->importSupplierUploadedFeFile(
|
||||||
|
(string) $path,
|
||||||
|
$importer,
|
||||||
|
$parser,
|
||||||
|
$extractor,
|
||||||
|
[
|
||||||
|
'force_update' => (bool) ($data['force_update'] ?? false),
|
||||||
|
'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$summary['imported'] += (int) ($result['imported'] ?? 0);
|
||||||
|
$summary['duplicates'] += (int) ($result['duplicates'] ?? 0);
|
||||||
|
$summary['skipped'] += (int) ($result['skipped'] ?? 0);
|
||||||
|
$summary['errors'] += (int) ($result['errors'] ?? 0);
|
||||||
|
$summary['messages'] = array_merge($summary['messages'], $result['messages'] ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->loadData();
|
||||||
|
|
||||||
|
$body = 'Importate: ' . $summary['imported'] . ' · Duplicate: ' . $summary['duplicates'] . ' · Saltate: ' . $summary['skipped'] . ' · Errori: ' . $summary['errors'];
|
||||||
|
if ($summary['messages'] !== []) {
|
||||||
|
$body .= "\n" . implode("\n", array_slice($summary['messages'], 0, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Import FE manuale completato')
|
||||||
|
->body($body)
|
||||||
|
->{$summary['errors'] > 0 ? 'warning' : 'success'}()
|
||||||
|
->send();
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected function loadData(): void
|
protected function loadData(): void
|
||||||
{
|
{
|
||||||
if (! $this->fornitore instanceof Fornitore) {
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
|
@ -155,4 +285,422 @@ protected function loadData(): void
|
||||||
])
|
])
|
||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $options
|
||||||
|
* @return array{imported:int,duplicates:int,skipped:int,errors:int,messages:array<int,string>}
|
||||||
|
*/
|
||||||
|
private function importSupplierUploadedFeFile(
|
||||||
|
string $path,
|
||||||
|
FatturaElettronicaImporter $importer,
|
||||||
|
FatturaElettronicaXmlParser $parser,
|
||||||
|
P7mExtractor $extractor,
|
||||||
|
array $options,
|
||||||
|
): array {
|
||||||
|
$summary = [
|
||||||
|
'imported' => 0,
|
||||||
|
'duplicates' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'errors' => 0,
|
||||||
|
'messages' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$path = trim($path);
|
||||||
|
if ($path === '') {
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$filename = basename($path);
|
||||||
|
$lower = Str::lower($filename);
|
||||||
|
|
||||||
|
if (Str::endsWith($lower, '.zip')) {
|
||||||
|
$absZip = Storage::disk('local')->path($path);
|
||||||
|
if (! is_file($absZip)) {
|
||||||
|
throw new \RuntimeException('ZIP non trovato');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tmpDir = 'tmp/fornitori-fe/' . now()->format('Y/m') . '/' . (string) Str::uuid();
|
||||||
|
Storage::disk('local')->makeDirectory($tmpDir);
|
||||||
|
$absTmp = Storage::disk('local')->path($tmpDir);
|
||||||
|
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
$open = $zip->open($absZip);
|
||||||
|
if ($open !== true) {
|
||||||
|
throw new \RuntimeException('ZIP non valido');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->extractTo($absTmp);
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($absTmp));
|
||||||
|
foreach ($it as $fileInfo) {
|
||||||
|
if (! $fileInfo instanceof \SplFileInfo || ! $fileInfo->isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$innerName = (string) $fileInfo->getFilename();
|
||||||
|
$innerLower = Str::lower($innerName);
|
||||||
|
if (! (Str::endsWith($innerLower, '.xml') || Str::endsWith($innerLower, '.p7m'))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = file_get_contents($fileInfo->getPathname());
|
||||||
|
if (! is_string($bytes) || $bytes === '') {
|
||||||
|
$summary['errors']++;
|
||||||
|
$summary['messages'][] = $filename . '::' . $innerName . ' - file vuoto';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$isP7m = Str::endsWith($innerLower, '.p7m');
|
||||||
|
$xml = $isP7m ? $extractor->extractXmlFromP7m($fileInfo->getPathname()) : $bytes;
|
||||||
|
$this->mergeSupplierImportResult(
|
||||||
|
$summary,
|
||||||
|
$this->importSupplierParsedXml(
|
||||||
|
is_string($xml) ? $xml : '',
|
||||||
|
$isP7m ? (pathinfo($innerName, PATHINFO_FILENAME) . '.xml') : $innerName,
|
||||||
|
$importer,
|
||||||
|
$parser,
|
||||||
|
$options,
|
||||||
|
$isP7m ? $innerName : null,
|
||||||
|
$isP7m ? $bytes : null
|
||||||
|
),
|
||||||
|
$filename . '::' . $innerName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::disk('local')->deleteDirectory($tmpDir);
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytes = Storage::disk('local')->get($path);
|
||||||
|
if (! is_string($bytes) || $bytes === '') {
|
||||||
|
throw new \RuntimeException('File vuoto');
|
||||||
|
}
|
||||||
|
|
||||||
|
$isP7m = Str::endsWith($lower, '.p7m');
|
||||||
|
$xml = $isP7m ? $extractor->extractXmlFromP7m(Storage::disk('local')->path($path)) : $bytes;
|
||||||
|
|
||||||
|
$this->mergeSupplierImportResult(
|
||||||
|
$summary,
|
||||||
|
$this->importSupplierParsedXml(
|
||||||
|
is_string($xml) ? $xml : '',
|
||||||
|
$isP7m ? (pathinfo($filename, PATHINFO_FILENAME) . '.xml') : $filename,
|
||||||
|
$importer,
|
||||||
|
$parser,
|
||||||
|
$options,
|
||||||
|
$isP7m ? $filename : null,
|
||||||
|
$isP7m ? $bytes : null
|
||||||
|
),
|
||||||
|
$filename
|
||||||
|
);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$summary['errors']++;
|
||||||
|
$summary['messages'][] = basename($path) . ' - ' . $e->getMessage();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
Storage::disk('local')->delete($path);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $options
|
||||||
|
* @return array{status:string,message:string}
|
||||||
|
*/
|
||||||
|
private function importSupplierParsedXml(
|
||||||
|
string $xml,
|
||||||
|
string $xmlFilename,
|
||||||
|
FatturaElettronicaImporter $importer,
|
||||||
|
FatturaElettronicaXmlParser $parser,
|
||||||
|
array $options,
|
||||||
|
?string $p7mFilename = null,
|
||||||
|
?string $p7mBytes = null,
|
||||||
|
): array {
|
||||||
|
$xml = trim($xml);
|
||||||
|
if ($xml === '') {
|
||||||
|
return ['status' => 'error', 'message' => 'XML non valido o assente'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isSdINotificationDocument($xmlFilename, $xml)) {
|
||||||
|
return [
|
||||||
|
'status' => 'skipped',
|
||||||
|
'message' => 'Ricevuta/notifica SdI: non viene importata nella contabilita fornitore',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsed = $parser->parse($xml);
|
||||||
|
if (! $this->supplierMatchesParsedIdentity($parsed)) {
|
||||||
|
return ['status' => 'error', 'message' => 'Il file non appartiene al fornitore corrente'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$stabileId = $this->resolveSupplierStabileIdFromParsed($parsed);
|
||||||
|
if ($stabileId <= 0) {
|
||||||
|
return ['status' => 'error', 'message' => 'Impossibile determinare uno stabile univoco per questa FE'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$unique = (string) Str::uuid();
|
||||||
|
$ym = now()->format('Y/m');
|
||||||
|
$inboxBase = $this->supplierInboxBaseForStabile($stabileId, $ym);
|
||||||
|
$permanentP7mPath = null;
|
||||||
|
|
||||||
|
if ($p7mFilename !== null && is_string($p7mBytes) && $p7mBytes !== '') {
|
||||||
|
$permanentP7mPath = $inboxBase . '/' . $unique . '-' . $p7mFilename;
|
||||||
|
Storage::disk('local')->put($permanentP7mPath, $p7mBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
$permanentXmlPath = $inboxBase . '/' . $unique . '-' . $xmlFilename;
|
||||||
|
Storage::disk('local')->put($permanentXmlPath, $xml);
|
||||||
|
|
||||||
|
$result = $importer->importXml($xml, 0, $xmlFilename, [
|
||||||
|
'xml_path' => $permanentXmlPath,
|
||||||
|
'p7m_path' => $permanentP7mPath,
|
||||||
|
'nome_file_p7m' => $p7mFilename,
|
||||||
|
'force_update' => (bool) ($options['force_update'] ?? false),
|
||||||
|
'auto_repair_duplicate' => true,
|
||||||
|
'importa_righe' => (string) ($options['importa_righe'] ?? 'auto'),
|
||||||
|
'create_fornitore_if_missing' => false,
|
||||||
|
'allow_fallback_stabile' => false,
|
||||||
|
'force_stabile_id' => $stabileId,
|
||||||
|
'force_fornitore_id' => (int) $this->fornitore->id,
|
||||||
|
'skip_fornitore_sync' => true,
|
||||||
|
'user_id' => (int) Auth::id(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$status = (string) ($result['status'] ?? 'error');
|
||||||
|
$message = (string) ($result['message'] ?? 'Import FE non riuscito');
|
||||||
|
|
||||||
|
if ($status === 'duplicate' || $status === 'error') {
|
||||||
|
try {
|
||||||
|
Storage::disk('local')->delete($permanentXmlPath);
|
||||||
|
if ($permanentP7mPath !== null) {
|
||||||
|
Storage::disk('local')->delete($permanentP7mPath);
|
||||||
|
}
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => $status,
|
||||||
|
'message' => $message,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $summary
|
||||||
|
* @param array<string, string> $result
|
||||||
|
*/
|
||||||
|
private function mergeSupplierImportResult(array &$summary, array $result, string $label): void
|
||||||
|
{
|
||||||
|
$status = (string) ($result['status'] ?? 'error');
|
||||||
|
$message = trim((string) ($result['message'] ?? ''));
|
||||||
|
|
||||||
|
if ($status === 'imported') {
|
||||||
|
$summary['imported']++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status === 'duplicate') {
|
||||||
|
$summary['duplicates']++;
|
||||||
|
$summary['messages'][] = $label . ' - ' . ($message !== '' ? $message : 'duplicato');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status === 'skipped') {
|
||||||
|
$summary['skipped']++;
|
||||||
|
$summary['messages'][] = $label . ' - ' . ($message !== '' ? $message : 'saltato');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary['errors']++;
|
||||||
|
$summary['messages'][] = $label . ' - ' . ($message !== '' ? $message : 'errore');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isSdINotificationDocument(string $xmlFilename, string $xml): bool
|
||||||
|
{
|
||||||
|
$filename = strtoupper(trim($xmlFilename));
|
||||||
|
if ($filename !== '' && preg_match('/_(RC|NS|MC|NE|EC|AT|DT|MT|SE|SM)_\d+\.XML$/', $filename) === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'<RicevutaConsegna',
|
||||||
|
'<NotificaScarto',
|
||||||
|
'<NotificaMancataConsegna',
|
||||||
|
'<NotificaEsito',
|
||||||
|
'<MetadatiInvioFile',
|
||||||
|
'<AttestazioneTrasmissioneFattura',
|
||||||
|
'<EsitoCommittente',
|
||||||
|
] as $needle) {
|
||||||
|
if (str_contains($xml, $needle)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getSupplierFeUploadDirectory(): string
|
||||||
|
{
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
return 'tmp/fornitori-fe';
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminCode = (string) ($this->fornitore->amministratore?->codice_amministratore ?: $this->fornitore->amministratore?->codice_univoco ?: 'ID-' . (int) $this->fornitore->amministratore_id);
|
||||||
|
$fornitoreCode = (string) ($this->fornitore->codice_fornitore ?: $this->fornitore->codice_univoco ?: 'ID-' . (int) $this->fornitore->id);
|
||||||
|
|
||||||
|
return 'amministratori/' . $adminCode . '/fornitori/' . $fornitoreCode . '/temp/upload/fatture-fe';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function supplierHasTaxIdentity(): bool
|
||||||
|
{
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->normalizeTaxId((string) ($this->fornitore->partita_iva ?? '')) !== ''
|
||||||
|
|| $this->normalizeTaxId((string) ($this->fornitore->codice_fiscale ?? '')) !== ''
|
||||||
|
|| $this->normalizeTaxId((string) ($this->fornitore->rubrica?->partita_iva ?? '')) !== ''
|
||||||
|
|| $this->normalizeTaxId((string) ($this->fornitore->rubrica?->codice_fiscale ?? '')) !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $parsed
|
||||||
|
*/
|
||||||
|
private function supplierMatchesParsedIdentity(array $parsed): bool
|
||||||
|
{
|
||||||
|
if (! $this->fornitore instanceof Fornitore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$invoiceIds = array_values(array_unique(array_filter([
|
||||||
|
$this->normalizeTaxId((string) ($parsed['fornitore_piva'] ?? '')),
|
||||||
|
$this->normalizeTaxId((string) ($parsed['fornitore_cf'] ?? '')),
|
||||||
|
$this->normalizeTaxId((string) ($parsed['destinatario_piva'] ?? '')),
|
||||||
|
$this->normalizeTaxId((string) ($parsed['destinatario_cf'] ?? '')),
|
||||||
|
])));
|
||||||
|
|
||||||
|
if ($invoiceIds === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$supplierIds = array_values(array_filter([
|
||||||
|
$this->normalizeTaxId((string) ($this->fornitore->partita_iva ?? '')),
|
||||||
|
$this->normalizeTaxId((string) ($this->fornitore->codice_fiscale ?? '')),
|
||||||
|
$this->normalizeTaxId((string) ($this->fornitore->rubrica?->partita_iva ?? '')),
|
||||||
|
$this->normalizeTaxId((string) ($this->fornitore->rubrica?->codice_fiscale ?? '')),
|
||||||
|
]));
|
||||||
|
|
||||||
|
foreach ($invoiceIds as $invoiceId) {
|
||||||
|
if (in_array($invoiceId, $supplierIds, true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $parsed
|
||||||
|
*/
|
||||||
|
private function resolveSupplierStabileIdFromParsed(array $parsed): int
|
||||||
|
{
|
||||||
|
$adminId = (int) ($this->fornitore?->amministratore_id ?? 0);
|
||||||
|
if ($adminId <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cf = $this->normalizeTaxId((string) ($parsed['destinatario_cf'] ?? ''));
|
||||||
|
if ($cf !== '') {
|
||||||
|
$match = Stabile::query()
|
||||||
|
->where('amministratore_id', $adminId)
|
||||||
|
->whereNotNull('codice_fiscale')
|
||||||
|
->whereRaw("REPLACE(UPPER(codice_fiscale), ' ', '') = ?", [$cf])
|
||||||
|
->where('attivo', true)
|
||||||
|
->get();
|
||||||
|
if ($match->count() === 1) {
|
||||||
|
return (int) $match->first()->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$codice = $this->normalizeTaxId((string) ($parsed['codice_destinatario'] ?? ''));
|
||||||
|
if ($codice !== '') {
|
||||||
|
$match = Stabile::query()
|
||||||
|
->where('amministratore_id', $adminId)
|
||||||
|
->whereNotNull('codice_destinatario_sdi')
|
||||||
|
->whereRaw("REPLACE(UPPER(codice_destinatario_sdi), ' ', '') = ?", [$codice])
|
||||||
|
->where('attivo', true)
|
||||||
|
->get();
|
||||||
|
if ($match->count() === 1) {
|
||||||
|
return (int) $match->first()->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pec = trim(strtolower((string) ($parsed['pec_destinatario'] ?? '')));
|
||||||
|
if ($pec !== '') {
|
||||||
|
$match = Stabile::query()
|
||||||
|
->where('amministratore_id', $adminId)
|
||||||
|
->where(function ($query) use ($pec): void {
|
||||||
|
$query->whereRaw('LOWER(pec_condominio) = ?', [$pec]);
|
||||||
|
|
||||||
|
if (Schema::hasColumn('stabili', 'pec_amministratore')) {
|
||||||
|
$query->orWhereRaw('LOWER(pec_amministratore) = ?', [$pec]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->where('attivo', true)
|
||||||
|
->get();
|
||||||
|
if ($match->count() === 1) {
|
||||||
|
return (int) $match->first()->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$destPiva = $this->normalizeTaxId((string) ($parsed['destinatario_piva'] ?? ''));
|
||||||
|
if ($destPiva !== '') {
|
||||||
|
$match = Stabile::query()
|
||||||
|
->select('stabili.id')
|
||||||
|
->join('amministratori', 'amministratori.id', '=', 'stabili.amministratore_id')
|
||||||
|
->where('stabili.amministratore_id', $adminId)
|
||||||
|
->where('stabili.attivo', true)
|
||||||
|
->whereRaw("REPLACE(UPPER(amministratori.partita_iva), ' ', '') = ?", [$destPiva])
|
||||||
|
->get();
|
||||||
|
if ($match->count() === 1) {
|
||||||
|
return (int) $match->first()->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function supplierInboxBaseForStabile(int $stabileId, string $ym): string
|
||||||
|
{
|
||||||
|
$stabile = Stabile::query()
|
||||||
|
->with(['amministratore:id,codice_amministratore,codice_univoco'])
|
||||||
|
->select(['id', 'amministratore_id', 'codice_stabile', 'codice_univoco'])
|
||||||
|
->find($stabileId);
|
||||||
|
|
||||||
|
if ($stabile instanceof Stabile) {
|
||||||
|
$base = ArchivioPaths::stabileBase($stabile, $stabile->amministratore instanceof Amministratore ? $stabile->amministratore : null);
|
||||||
|
if (is_string($base) && $base !== '') {
|
||||||
|
return $base . '/fatture-ricevute/inbox/' . $ym;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminId = (int) ($stabile?->amministratore_id ?? 0);
|
||||||
|
return 'amministratori/ID-' . $adminId . '/stabili/ID-' . $stabileId . '/fatture-ricevute/inbox/' . $ym;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeTaxId(string $value): string
|
||||||
|
{
|
||||||
|
$normalized = strtoupper(str_replace(' ', '', trim($value)));
|
||||||
|
if (str_starts_with($normalized, 'IT') && strlen($normalized) > 11) {
|
||||||
|
$normalized = substr($normalized, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return preg_replace('/[^A-Z0-9]/', '', $normalized) ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@
|
||||||
namespace App\Filament\Pages\Fornitore;
|
namespace App\Filament\Pages\Fornitore;
|
||||||
|
|
||||||
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
||||||
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
|
|
||||||
use App\Models\AssistenzaTecnorepairScheda;
|
use App\Models\AssistenzaTecnorepairScheda;
|
||||||
use App\Models\Fornitore;
|
use App\Models\Fornitore;
|
||||||
use App\Models\FornitoreCliente;
|
use App\Models\FornitoreCliente;
|
||||||
use App\Models\TicketIntervento;
|
use App\Models\TicketIntervento;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Support\GoogleAccountStore;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Pages\Page;
|
use Filament\Pages\Page;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
@ -49,9 +49,9 @@ class RubricaClienti extends Page
|
||||||
|
|
||||||
/** @var array<string, int> */
|
/** @var array<string, int> */
|
||||||
public array $counters = [
|
public array $counters = [
|
||||||
'totali' => 0,
|
'totali' => 0,
|
||||||
'collegati' => 0,
|
'collegati' => 0,
|
||||||
'con_email' => 0,
|
'con_email' => 0,
|
||||||
'con_cellulare' => 0,
|
'con_cellulare' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -98,13 +98,13 @@ public function selectTab(string $tab): void
|
||||||
public function selectCliente(int $clienteId): void
|
public function selectCliente(int $clienteId): void
|
||||||
{
|
{
|
||||||
$this->selectedClienteId = $clienteId > 0 ? $clienteId : null;
|
$this->selectedClienteId = $clienteId > 0 ? $clienteId : null;
|
||||||
$this->activeTab = 'scheda';
|
$this->activeTab = 'scheda';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function refreshRows(): void
|
public function refreshRows(): void
|
||||||
{
|
{
|
||||||
if (! $this->fornitoreId) {
|
if (! $this->fornitoreId) {
|
||||||
$this->rows = [];
|
$this->rows = [];
|
||||||
$this->counters = ['totali' => 0, 'collegati' => 0, 'con_email' => 0, 'con_cellulare' => 0];
|
$this->counters = ['totali' => 0, 'collegati' => 0, 'con_email' => 0, 'con_cellulare' => 0];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -116,7 +116,7 @@ public function refreshRows(): void
|
||||||
->selectRaw('legacy_cliente_id, COUNT(*) as totale_schede, MAX(date_received) as ultima_data')
|
->selectRaw('legacy_cliente_id, COUNT(*) as totale_schede, MAX(date_received) as ultima_data')
|
||||||
->groupBy('legacy_cliente_id')
|
->groupBy('legacy_cliente_id')
|
||||||
->get()
|
->get()
|
||||||
->mapWithKeys(fn(AssistenzaTecnorepairScheda $row): array => [
|
->mapWithKeys(fn(AssistenzaTecnorepairScheda $row): array=> [
|
||||||
(int) ($row->legacy_cliente_id ?? 0) => [
|
(int) ($row->legacy_cliente_id ?? 0) => [
|
||||||
'totale_schede' => (int) ($row->totale_schede ?? 0),
|
'totale_schede' => (int) ($row->totale_schede ?? 0),
|
||||||
'ultima_data' => optional($row->ultima_data)->format('d/m/Y') ?: '-',
|
'ultima_data' => optional($row->ultima_data)->format('d/m/Y') ?: '-',
|
||||||
|
|
@ -141,15 +141,14 @@ public function refreshRows(): void
|
||||||
})
|
})
|
||||||
->orderByRaw('CASE WHEN rubrica_id IS NULL THEN 1 ELSE 0 END')
|
->orderByRaw('CASE WHEN rubrica_id IS NULL THEN 1 ELSE 0 END')
|
||||||
->orderBy('display_name')
|
->orderBy('display_name')
|
||||||
->limit(500)
|
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
if ($items->isEmpty()) {
|
if ($items->isEmpty()) {
|
||||||
$this->rows = $this->buildTicketFallbackRows($term);
|
$this->rows = $this->buildTicketFallbackRows($term);
|
||||||
$this->counters = [
|
$this->counters = [
|
||||||
'totali' => count($this->rows),
|
'totali' => count($this->rows),
|
||||||
'collegati' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['rubrica_id'] ?? null))->count(),
|
'collegati' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['rubrica_id'] ?? null))->count(),
|
||||||
'con_email' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['email'] ?? null))->count(),
|
'con_email' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['email'] ?? null))->count(),
|
||||||
'con_cellulare' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['phone'] ?? null))->count(),
|
'con_cellulare' => (int) collect($this->rows)->filter(fn(array $row): bool => filled($row['phone'] ?? null))->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -161,29 +160,29 @@ public function refreshRows(): void
|
||||||
$legacy = $legacyCounts[(int) ($cliente->legacy_cliente_id ?? 0)] ?? ['totale_schede' => 0, 'ultima_data' => '-'];
|
$legacy = $legacyCounts[(int) ($cliente->legacy_cliente_id ?? 0)] ?? ['totale_schede' => 0, 'ultima_data' => '-'];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $cliente->id,
|
'id' => (int) $cliente->id,
|
||||||
'legacy_cliente_id'=> (int) ($cliente->legacy_cliente_id ?? 0),
|
'legacy_cliente_id' => (int) ($cliente->legacy_cliente_id ?? 0),
|
||||||
'display_name' => (string) ($cliente->display_name ?: 'Cliente TecnoRepair'),
|
'display_name' => (string) ($cliente->display_name ?: 'Cliente TecnoRepair'),
|
||||||
'phone' => (string) ($cliente->phone ?? ''),
|
'phone' => (string) ($cliente->phone ?? ''),
|
||||||
'phone_alt' => (string) ($cliente->phone_alt ?? ''),
|
'phone_alt' => (string) ($cliente->phone_alt ?? ''),
|
||||||
'email' => (string) ($cliente->email ?? ''),
|
'email' => (string) ($cliente->email ?? ''),
|
||||||
'indirizzo' => (string) ($cliente->indirizzo ?? ''),
|
'indirizzo' => (string) ($cliente->indirizzo ?? ''),
|
||||||
'citta' => (string) ($cliente->citta ?? ''),
|
'citta' => (string) ($cliente->citta ?? ''),
|
||||||
'provincia' => (string) ($cliente->provincia ?? ''),
|
'provincia' => (string) ($cliente->provincia ?? ''),
|
||||||
'rubrica_id' => (int) ($cliente->rubrica_id ?? 0),
|
'rubrica_id' => (int) ($cliente->rubrica_id ?? 0),
|
||||||
'rubrica_url' => $cliente->rubrica_id ? RubricaUniversaleScheda::getUrl(['record' => (int) $cliente->rubrica_id], panel: 'admin-filament') : null,
|
'rubrica_url' => $cliente->rubrica_id ? $this->buildRubricaUrlForCurrentUser((int) $cliente->rubrica_id, (string) ($cliente->display_name ?? '')) : null,
|
||||||
'rubrica_label' => (string) ($cliente->rubrica?->categoria ?? ''),
|
'rubrica_label' => (string) ($cliente->rubrica?->categoria ?? ''),
|
||||||
'totale_schede' => (int) ($legacy['totale_schede'] ?? 0),
|
'totale_schede' => (int) ($legacy['totale_schede'] ?? 0),
|
||||||
'ultima_data' => (string) ($legacy['ultima_data'] ?? '-'),
|
'ultima_data' => (string) ($legacy['ultima_data'] ?? '-'),
|
||||||
'updated_at' => optional($cliente->updated_at)->format('d/m/Y H:i') ?: '-',
|
'updated_at' => optional($cliente->updated_at)->format('d/m/Y H:i') ?: '-',
|
||||||
'source' => 'tecnorepair_tclienti',
|
'source' => 'tecnorepair_tclienti',
|
||||||
];
|
];
|
||||||
})->all();
|
})->all();
|
||||||
|
|
||||||
$this->counters = [
|
$this->counters = [
|
||||||
'totali' => count($this->rows),
|
'totali' => count($this->rows),
|
||||||
'collegati' => (int) collect($this->rows)->filter(fn(array $row): bool => (int) ($row['rubrica_id'] ?? 0) > 0)->count(),
|
'collegati' => (int) collect($this->rows)->filter(fn(array $row): bool => (int) ($row['rubrica_id'] ?? 0) > 0)->count(),
|
||||||
'con_email' => (int) collect($this->rows)->filter(fn(array $row): bool => trim((string) ($row['email'] ?? '')) !== '')->count(),
|
'con_email' => (int) collect($this->rows)->filter(fn(array $row): bool => trim((string) ($row['email'] ?? '')) !== '')->count(),
|
||||||
'con_cellulare' => (int) collect($this->rows)->filter(fn(array $row): bool => trim((string) ($row['phone'] ?? '')) !== '')->count(),
|
'con_cellulare' => (int) collect($this->rows)->filter(fn(array $row): bool => trim((string) ($row['phone'] ?? '')) !== '')->count(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -220,15 +219,15 @@ public function getSelectedClienteSchedeProperty(): array
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->limit(30)
|
->limit(30)
|
||||||
->get()
|
->get()
|
||||||
->map(fn(AssistenzaTecnorepairScheda $scheda): array => [
|
->map(fn(AssistenzaTecnorepairScheda $scheda): array=> [
|
||||||
'id' => (int) $scheda->id,
|
'id' => (int) $scheda->id,
|
||||||
'numero' => (string) ($scheda->legacy_numero_scheda ?: ('#' . $scheda->id)),
|
'numero' => (string) ($scheda->legacy_numero_scheda ?: ('#' . $scheda->id)),
|
||||||
'prodotto' => (string) ($scheda->product_model ?: '-'),
|
'prodotto' => (string) ($scheda->product_model ?: '-'),
|
||||||
'codice' => (string) ($scheda->product_code ?: ''),
|
'codice' => (string) ($scheda->product_code ?: ''),
|
||||||
'seriali' => (string) $scheda->serial_label,
|
'seriali' => (string) $scheda->serial_label,
|
||||||
'stato' => (string) ($scheda->status_label ?: '-'),
|
'stato' => (string) ($scheda->status_label ?: '-'),
|
||||||
'status_badge' => (string) $scheda->status_badge_classes,
|
'status_badge' => (string) $scheda->status_badge_classes,
|
||||||
'ingresso' => optional($scheda->date_received)->format('d/m/Y') ?: '-',
|
'ingresso' => optional($scheda->date_received)->format('d/m/Y') ?: '-',
|
||||||
])->all();
|
])->all();
|
||||||
|
|
||||||
$items = TicketIntervento::query()
|
$items = TicketIntervento::query()
|
||||||
|
|
@ -375,7 +374,7 @@ protected function resolveCallerDataForRubrica(TicketIntervento $intervento): ar
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function buildRubricaUrlForCurrentUser(int $rubricaId): ?string
|
protected function buildRubricaUrlForCurrentUser(int $rubricaId, string $search = ''): ?string
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
|
|
@ -387,7 +386,10 @@ protected function buildRubricaUrlForCurrentUser(int $rubricaId): ?string
|
||||||
return \App\Filament\Pages\Gescon\RubricaUniversaleScheda::getUrl(['record' => $rubricaId], panel: 'admin-filament');
|
return \App\Filament\Pages\Gescon\RubricaUniversaleScheda::getUrl(['record' => $rubricaId], panel: 'admin-filament');
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
$url = self::getUrl(['fornitore' => (int) ($this->fornitoreId ?? 0)], panel: 'admin-filament');
|
||||||
|
$search = trim($search);
|
||||||
|
|
||||||
|
return $search !== '' ? ($url . '?search=' . urlencode($search)) : $url;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -406,7 +408,7 @@ private function buildTicketFallbackRows(string $term = ''): array
|
||||||
|
|
||||||
foreach ($items as $intervento) {
|
foreach ($items as $intervento) {
|
||||||
$contact = $this->resolveCallerDataForRubrica($intervento);
|
$contact = $this->resolveCallerDataForRubrica($intervento);
|
||||||
$key = $contact['identity_key'] !== '' ? $contact['identity_key'] : ('ticket-' . (int) $intervento->ticket_id);
|
$key = $contact['identity_key'] !== '' ? $contact['identity_key'] : ('ticket-' . (int) $intervento->ticket_id);
|
||||||
|
|
||||||
if ($term !== '') {
|
if ($term !== '') {
|
||||||
$haystack = Str::lower(implode(' ', [$contact['contatto'], $contact['telefono'], $contact['email'], $contact['stabile'], $contact['origine']]));
|
$haystack = Str::lower(implode(' ', [$contact['contatto'], $contact['telefono'], $contact['email'], $contact['stabile'], $contact['origine']]));
|
||||||
|
|
@ -417,22 +419,22 @@ private function buildTicketFallbackRows(string $term = ''): array
|
||||||
|
|
||||||
if (! isset($grouped[$key])) {
|
if (! isset($grouped[$key])) {
|
||||||
$grouped[$key] = [
|
$grouped[$key] = [
|
||||||
'id' => 0,
|
'id' => 0,
|
||||||
'legacy_cliente_id' => 0,
|
'legacy_cliente_id' => 0,
|
||||||
'display_name' => $contact['contatto'],
|
'display_name' => $contact['contatto'],
|
||||||
'phone' => $contact['telefono'],
|
'phone' => $contact['telefono'],
|
||||||
'phone_alt' => '',
|
'phone_alt' => '',
|
||||||
'email' => $contact['email'],
|
'email' => $contact['email'],
|
||||||
'indirizzo' => '',
|
'indirizzo' => '',
|
||||||
'citta' => '',
|
'citta' => '',
|
||||||
'provincia' => '',
|
'provincia' => '',
|
||||||
'rubrica_id' => 0,
|
'rubrica_id' => 0,
|
||||||
'rubrica_url' => $contact['rubrica_url'],
|
'rubrica_url' => $contact['rubrica_url'],
|
||||||
'rubrica_label' => $contact['rubrica_label'],
|
'rubrica_label' => $contact['rubrica_label'],
|
||||||
'totale_schede' => 1,
|
'totale_schede' => 1,
|
||||||
'ultima_data' => optional($intervento->updated_at)->format('d/m/Y') ?: '-',
|
'ultima_data' => optional($intervento->updated_at)->format('d/m/Y') ?: '-',
|
||||||
'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
'updated_at' => optional($intervento->updated_at)->format('d/m/Y H:i') ?: '-',
|
||||||
'source' => 'ticket_fallback',
|
'source' => 'ticket_fallback',
|
||||||
];
|
];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -453,13 +455,22 @@ protected function resolveGoogleWorkspaceStatus(Fornitore $fornitore): ?array
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$oauth = (array) (($amministratore->impostazioni['google']['oauth'] ?? null) ?: []);
|
$account = app(GoogleAccountStore::class)->get($amministratore, 'fornitore-' . (int) $fornitore->id);
|
||||||
|
|
||||||
|
if (! is_array($account)) {
|
||||||
|
return [
|
||||||
|
'connected' => false,
|
||||||
|
'email' => '',
|
||||||
|
'name' => '',
|
||||||
|
'connected_at' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'connected' => (bool) ($oauth['connected'] ?? false),
|
'connected' => (bool) ($account['connected'] ?? false),
|
||||||
'email' => (string) ($oauth['email'] ?? ''),
|
'email' => (string) ($account['email'] ?? ''),
|
||||||
'name' => (string) ($oauth['name'] ?? ''),
|
'name' => (string) ($account['name'] ?? ''),
|
||||||
'connected_at' => (string) ($oauth['connected_at'] ?? ''),
|
'connected_at' => (string) ($account['connected_at'] ?? ''),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
use Filament\Schemas\Components\Tabs;
|
use Filament\Schemas\Components\Tabs;
|
||||||
use Filament\Schemas\Components\Tabs\Tab;
|
use Filament\Schemas\Components\Tabs\Tab;
|
||||||
use Filament\Schemas\Schema as FilamentSchema;
|
use Filament\Schemas\Schema as FilamentSchema;
|
||||||
|
use Illuminate\Contracts\Encryption\DecryptException;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
@ -89,7 +90,9 @@ class SchedaAmministratore extends Page implements HasForms
|
||||||
/** @var array<int,array<string,mixed>> */
|
/** @var array<int,array<string,mixed>> */
|
||||||
public array $collaboratoreRubricaMatches = [];
|
public array $collaboratoreRubricaMatches = [];
|
||||||
|
|
||||||
public Amministratore $amministratore;
|
protected Amministratore $amministratore;
|
||||||
|
|
||||||
|
public int $amministratoreId = 0;
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
|
|
@ -126,40 +129,30 @@ public function mount(): void
|
||||||
abort(403);
|
abort(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->amministratore = $amministratore;
|
$this->amministratore = $amministratore;
|
||||||
|
$this->amministratoreId = (int) $amministratore->id;
|
||||||
|
|
||||||
$isSuperAdmin = $user->hasAnyRole(['super-admin', 'admin']);
|
$isSuperAdmin = $user->hasAnyRole(['super-admin', 'admin']);
|
||||||
|
|
||||||
$this->getSchema('form')?->fill([
|
$this->getSchema('form')?->fill([
|
||||||
...$this->amministratore->only([
|
...$this->safeAmministratoreFillableState($this->amministratore),
|
||||||
'nome',
|
|
||||||
'cognome',
|
|
||||||
'denominazione_studio',
|
|
||||||
'partita_iva',
|
|
||||||
'codice_fiscale_studio',
|
|
||||||
'indirizzo_studio',
|
|
||||||
'cap_studio',
|
|
||||||
'citta_studio',
|
|
||||||
'provincia_studio',
|
|
||||||
'telefono_studio',
|
|
||||||
'cellulare',
|
|
||||||
'email_studio',
|
|
||||||
'pec_studio',
|
|
||||||
'cartella_dati',
|
|
||||||
'database_attivo',
|
|
||||||
'fe_cassetto_enabled',
|
|
||||||
'fe_cassetto_base_url',
|
|
||||||
]),
|
|
||||||
// Per sicurezza non precompiliamo mai i segreti nel form.
|
// Per sicurezza non precompiliamo mai i segreti nel form.
|
||||||
// La configurazione completa è gestita dal Super Admin.
|
// La configurazione completa è gestita dal Super Admin.
|
||||||
'fe_cassetto_password_script' => $isSuperAdmin ? null : null,
|
'fe_cassetto_password_script' => $isSuperAdmin ? null : null,
|
||||||
'fe_cassetto_username' => $isSuperAdmin ? null : null,
|
'fe_cassetto_username' => $isSuperAdmin ? null : null,
|
||||||
'fe_cassetto_password' => $isSuperAdmin ? null : null,
|
'fe_cassetto_password' => $isSuperAdmin ? null : null,
|
||||||
'fe_cassetto_pin' => $isSuperAdmin ? null : null,
|
'fe_cassetto_pin' => $isSuperAdmin ? null : null,
|
||||||
'impostazioni' => $this->amministratore->impostazioni ?? [],
|
'impostazioni' => $this->safeAmministratoreImpostazioni($this->amministratore),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hydrate(): void
|
||||||
|
{
|
||||||
|
if ($this->amministratoreId > 0) {
|
||||||
|
$this->amministratore = Amministratore::query()->findOrFail($this->amministratoreId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function form(FilamentSchema $schema): FilamentSchema
|
public function form(FilamentSchema $schema): FilamentSchema
|
||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
|
|
@ -1995,7 +1988,16 @@ private function runOpsProcess(array $command, string $successTitle, string $err
|
||||||
|
|
||||||
public function submit(): void
|
public function submit(): void
|
||||||
{
|
{
|
||||||
$state = $this->getSchema('form')?->getState() ?? [];
|
try {
|
||||||
|
$state = $this->getSchema('form')?->getState() ?? [];
|
||||||
|
} catch (DecryptException) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Configurazione cifrata non leggibile')
|
||||||
|
->danger()
|
||||||
|
->body('Uno dei segreti salvati per questo amministratore non è più decifrabile con la chiave applicativa attuale. Ripulisci o reinserisci i campi del cassetto fiscale e poi salva di nuovo.')
|
||||||
|
->send();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// In alcune configurazioni Filament, con statePath('data') getState() può restituire ['data' => ...].
|
// In alcune configurazioni Filament, con statePath('data') getState() può restituire ['data' => ...].
|
||||||
// Normalizziamo per essere robusti.
|
// Normalizziamo per essere robusti.
|
||||||
if (is_array($state['data'] ?? null)) {
|
if (is_array($state['data'] ?? null)) {
|
||||||
|
|
@ -2042,4 +2044,54 @@ public function submit(): void
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function safeAmministratoreFillableState(Amministratore $amministratore): array
|
||||||
|
{
|
||||||
|
$state = [];
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
'nome',
|
||||||
|
'cognome',
|
||||||
|
'denominazione_studio',
|
||||||
|
'partita_iva',
|
||||||
|
'codice_fiscale_studio',
|
||||||
|
'indirizzo_studio',
|
||||||
|
'cap_studio',
|
||||||
|
'citta_studio',
|
||||||
|
'provincia_studio',
|
||||||
|
'telefono_studio',
|
||||||
|
'cellulare',
|
||||||
|
'email_studio',
|
||||||
|
'pec_studio',
|
||||||
|
'cartella_dati',
|
||||||
|
'database_attivo',
|
||||||
|
'fe_cassetto_enabled',
|
||||||
|
'fe_cassetto_base_url',
|
||||||
|
] as $key) {
|
||||||
|
try {
|
||||||
|
$state[$key] = $amministratore->getAttribute($key);
|
||||||
|
} catch (DecryptException) {
|
||||||
|
$state[$key] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function safeAmministratoreImpostazioni(Amministratore $amministratore): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$settings = $amministratore->impostazioni;
|
||||||
|
} catch (DecryptException) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_array($settings) ? $settings : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,16 +94,17 @@ public function callback(): RedirectResponse
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$oauthContext = (array) session()->pull('google_oauth_context', []);
|
$oauthContext = (array) session()->pull('google_oauth_context', []);
|
||||||
|
$redirectTo = $this->resolveRedirectTarget($oauthContext);
|
||||||
$incomingState = (string) request()->query('state', '');
|
$incomingState = (string) request()->query('state', '');
|
||||||
$sessionState = (string) ($oauthContext['state'] ?? '');
|
$sessionState = (string) ($oauthContext['state'] ?? '');
|
||||||
if ($incomingState === '' || $sessionState === '' || ! hash_equals($sessionState, $incomingState)) {
|
if ($incomingState === '' || $sessionState === '' || ! hash_equals($sessionState, $incomingState)) {
|
||||||
return redirect('/admin-filament/impostazioni/scheda-amministratore')
|
return redirect($redirectTo)
|
||||||
->with('error', 'Stato OAuth non valido. Riprova il collegamento Google.');
|
->with('error', 'Stato OAuth non valido. Riprova il collegamento Google.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$code = (string) request()->query('code', '');
|
$code = (string) request()->query('code', '');
|
||||||
if ($code === '') {
|
if ($code === '') {
|
||||||
return redirect('/admin-filament/impostazioni/scheda-amministratore')
|
return redirect($redirectTo)
|
||||||
->with('error', 'Google non ha restituito il codice di autorizzazione.');
|
->with('error', 'Google non ha restituito il codice di autorizzazione.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -117,7 +118,7 @@ public function callback(): RedirectResponse
|
||||||
|
|
||||||
if (! $tokenResponse->successful()) {
|
if (! $tokenResponse->successful()) {
|
||||||
$message = (string) (Arr::get($tokenResponse->json(), 'error_description') ?: Arr::get($tokenResponse->json(), 'error.message') ?: 'token_exchange_failed');
|
$message = (string) (Arr::get($tokenResponse->json(), 'error_description') ?: Arr::get($tokenResponse->json(), 'error.message') ?: 'token_exchange_failed');
|
||||||
return redirect('/admin-filament/impostazioni/scheda-amministratore')
|
return redirect($redirectTo)
|
||||||
->with('error', 'Errore scambio token Google: ' . $message);
|
->with('error', 'Errore scambio token Google: ' . $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,6 +147,8 @@ public function callback(): RedirectResponse
|
||||||
$refreshToken = trim((string) ($existing['refresh_token'] ?? ''));
|
$refreshToken = trim((string) ($existing['refresh_token'] ?? ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$setDefault = $existing === null && ! $store->isScopedAccountKey($accountKey);
|
||||||
|
|
||||||
$store->upsertAccount(
|
$store->upsertAccount(
|
||||||
$amministratore,
|
$amministratore,
|
||||||
$accountKey,
|
$accountKey,
|
||||||
|
|
@ -162,12 +165,10 @@ public function callback(): RedirectResponse
|
||||||
'refreshed_at' => now()->toDateTimeString(),
|
'refreshed_at' => now()->toDateTimeString(),
|
||||||
'scopes' => self::GOOGLE_SCOPES,
|
'scopes' => self::GOOGLE_SCOPES,
|
||||||
],
|
],
|
||||||
$existing === null
|
$setDefault
|
||||||
);
|
);
|
||||||
|
|
||||||
$redirectTo = (string) ($oauthContext['return_to'] ?? '');
|
return redirect($redirectTo)
|
||||||
|
|
||||||
return redirect($redirectTo !== '' ? $redirectTo : '/admin-filament/impostazioni/scheda-amministratore')
|
|
||||||
->with('status', 'Account Google collegato con successo.');
|
->with('status', 'Account Google collegato con successo.');
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
Log::error('Google OAuth callback failed', [
|
Log::error('Google OAuth callback failed', [
|
||||||
|
|
@ -175,7 +176,7 @@ public function callback(): RedirectResponse
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect('/admin-filament/impostazioni/scheda-amministratore')
|
return redirect($this->resolveRedirectTarget((array) session()->get('google_oauth_context', [])))
|
||||||
->with('error', 'Errore durante il collegamento Google: ' . $e->getMessage());
|
->with('error', 'Errore durante il collegamento Google: ' . $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -264,4 +265,16 @@ private function resolveCurrentUserSupplierFromCollaboratore(User $user): ?Forni
|
||||||
|
|
||||||
return Fornitore::query()->find((int) $dipendente->fornitore_id);
|
return Fornitore::query()->find((int) $dipendente->fornitore_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $oauthContext
|
||||||
|
*/
|
||||||
|
private function resolveRedirectTarget(array $oauthContext): string
|
||||||
|
{
|
||||||
|
$returnTo = trim((string) ($oauthContext['return_to'] ?? ''));
|
||||||
|
|
||||||
|
return $returnTo !== '' && str_starts_with($returnTo, '/')
|
||||||
|
? $returnTo
|
||||||
|
: '/admin-filament/impostazioni/scheda-amministratore';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ public function panel(Panel $panel): Panel
|
||||||
->visible(function () {
|
->visible(function () {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof \App\Models\User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}),
|
}),
|
||||||
NavigationItem::make('Cellulare / WebApp')
|
NavigationItem::make('Cellulare / WebApp')
|
||||||
->group('NetGescon')
|
->group('NetGescon')
|
||||||
|
|
@ -69,8 +69,8 @@ public function panel(Panel $panel): Panel
|
||||||
->visible(function () {
|
->visible(function () {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof \App\Models\User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}),
|
}),
|
||||||
NavigationItem::make('Ticket Mobile')
|
NavigationItem::make('Ticket Mobile')
|
||||||
->group('NetGescon')
|
->group('NetGescon')
|
||||||
|
|
@ -79,8 +79,8 @@ public function panel(Panel $panel): Panel
|
||||||
->visible(function () {
|
->visible(function () {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof \App\Models\User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}),
|
}),
|
||||||
NavigationItem::make('Scheda Stabile')
|
NavigationItem::make('Scheda Stabile')
|
||||||
->group('NetGescon')
|
->group('NetGescon')
|
||||||
|
|
@ -89,8 +89,8 @@ public function panel(Panel $panel): Panel
|
||||||
->visible(function () {
|
->visible(function () {
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
return $user instanceof \App\Models\User
|
return $user instanceof User
|
||||||
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
||||||
}),
|
}),
|
||||||
NavigationItem::make('Catalogo Fornitore')
|
NavigationItem::make('Catalogo Fornitore')
|
||||||
->group('Fornitore')
|
->group('Fornitore')
|
||||||
|
|
@ -171,5 +171,4 @@ protected function resolveSupplierCatalogUrl(): string
|
||||||
|
|
||||||
return TicketOperativi::getUrl(panel: 'admin-filament');
|
return TicketOperativi::getUrl(panel: 'admin-filament');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,11 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
|
||||||
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
|
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
|
||||||
$amministratoreId = (int) ($stabile?->amministratore_id ?: 0);
|
$amministratoreId = (int) ($stabile?->amministratore_id ?: 0);
|
||||||
|
|
||||||
$fornitoreId = $this->matchFornitoreIdForAmministratore($amministratoreId, $fornitorePiva, $fornitoreCf);
|
$forcedFornitoreId = (int) ($extra['force_fornitore_id'] ?? 0);
|
||||||
|
|
||||||
|
$fornitoreId = $forcedFornitoreId > 0
|
||||||
|
? $forcedFornitoreId
|
||||||
|
: $this->matchFornitoreIdForAmministratore($amministratoreId, $fornitorePiva, $fornitoreCf);
|
||||||
if (! $fornitoreId && ! empty($extra['create_fornitore_if_missing']) && $amministratoreId > 0) {
|
if (! $fornitoreId && ! empty($extra['create_fornitore_if_missing']) && $amministratoreId > 0) {
|
||||||
try {
|
try {
|
||||||
$fornitoreId = $this->ensureFornitoreFromFeData($amministratoreId, $data, $extra);
|
$fornitoreId = $this->ensureFornitoreFromFeData($amministratoreId, $data, $extra);
|
||||||
|
|
@ -194,7 +198,7 @@ public function importXml(string $xml, int $fallbackStabileId, ?string $original
|
||||||
|
|
||||||
// Best-effort: se il fornitore esiste, aggiorna i suoi dati base dalla FE (P.IVA/CF/denominazione, contatti, pagamenti).
|
// Best-effort: se il fornitore esiste, aggiorna i suoi dati base dalla FE (P.IVA/CF/denominazione, contatti, pagamenti).
|
||||||
try {
|
try {
|
||||||
if ($fornitoreId) {
|
if ($fornitoreId && empty($extra['skip_fornitore_sync'])) {
|
||||||
$this->syncFornitoreFromFeData((int) $fornitoreId, $data, $extra);
|
$this->syncFornitoreFromFeData((int) $fornitoreId, $data, $extra);
|
||||||
}
|
}
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
|
|
@ -1453,9 +1457,9 @@ private function updateExisting(FatturaElettronica $existing, string $xml, ?stri
|
||||||
$fornitorePiva = $existing->fornitore_piva ?: 'ND';
|
$fornitorePiva = $existing->fornitore_piva ?: 'ND';
|
||||||
}
|
}
|
||||||
|
|
||||||
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
|
$stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
|
||||||
$adminId = (int) ($stabile?->amministratore_id ?: 0);
|
$adminId = (int) ($stabile?->amministratore_id ?: 0);
|
||||||
$fornitoreId = $this->matchFornitoreIdForAmministratore($adminId, $fornitorePiva, $fornitoreCf);
|
$fornitoreId = $this->matchFornitoreIdForAmministratore($adminId, $fornitorePiva, $fornitoreCf);
|
||||||
$importRighe = $this->shouldImportRighe($fornitoreId, $extra);
|
$importRighe = $this->shouldImportRighe($fornitoreId, $extra);
|
||||||
|
|
||||||
return DB::transaction(function () use ($existing, $data, $xml, $originalFilename, $extra, $stabileId, $fornitoreId, $fornitorePiva, $importRighe) {
|
return DB::transaction(function () use ($existing, $data, $xml, $originalFilename, $extra, $stabileId, $fornitoreId, $fornitorePiva, $importRighe) {
|
||||||
|
|
|
||||||
|
|
@ -55,10 +55,7 @@ public function all(Amministratore $amministratore): array
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$defaultKey = $this->normalizeAccountKey((string) Arr::get($google, 'default_account_key', ''));
|
$defaultKey = $this->resolveDefaultAccountKey($accounts, $this->normalizeAccountKey((string) Arr::get($google, 'default_account_key', '')));
|
||||||
if ($defaultKey === '' && $accounts !== []) {
|
|
||||||
$defaultKey = (string) array_key_first($accounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($accounts as $key => $account) {
|
foreach ($accounts as $key => $account) {
|
||||||
$accounts[$key]['is_default'] = $key === $defaultKey;
|
$accounts[$key]['is_default'] = $key === $defaultKey;
|
||||||
|
|
@ -126,6 +123,23 @@ public function normalizeAccountKey(?string $value): string
|
||||||
return (string) Str::of($raw)->lower()->replace('@', '-at-')->slug('-');
|
return (string) Str::of($raw)->lower()->replace('@', '-at-')->slug('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isScopedAccountKey(?string $accountKey): bool
|
||||||
|
{
|
||||||
|
$key = $this->normalizeAccountKey($accountKey);
|
||||||
|
|
||||||
|
if ($key === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['fornitore-', 'dipendente-', 'stabile-'] as $prefix) {
|
||||||
|
if (str_starts_with($key, $prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $payload
|
* @param array<string, mixed> $payload
|
||||||
*/
|
*/
|
||||||
|
|
@ -145,18 +159,15 @@ public function upsertAccount(Amministratore $amministratore, string $accountKey
|
||||||
$accounts[$key] = $account;
|
$accounts[$key] = $account;
|
||||||
$google['accounts'] = $accounts;
|
$google['accounts'] = $accounts;
|
||||||
|
|
||||||
$defaultKey = $this->normalizeAccountKey((string) Arr::get($google, 'default_account_key', ''));
|
$defaultKey = $this->resolveDefaultAccountKey($accounts, $this->normalizeAccountKey((string) Arr::get($google, 'default_account_key', '')));
|
||||||
if ($setDefault || $defaultKey === '') {
|
$canBecomeDefault = ! $this->isScopedAccountKey($key);
|
||||||
|
|
||||||
|
if ($setDefault && $canBecomeDefault) {
|
||||||
$defaultKey = $key;
|
$defaultKey = $key;
|
||||||
}
|
}
|
||||||
|
|
||||||
$google['default_account_key'] = $defaultKey;
|
$google['default_account_key'] = $defaultKey;
|
||||||
if ($defaultKey === $key) {
|
$this->syncLegacyOauthState($google, $accounts, $defaultKey);
|
||||||
$google['oauth'] = $this->toLegacyOauthPayload($account);
|
|
||||||
if (trim((string) ($account['email'] ?? '')) !== '') {
|
|
||||||
$google['workspace_email'] = trim((string) $account['email']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$impostazioni['google'] = $google;
|
$impostazioni['google'] = $google;
|
||||||
$amministratore->impostazioni = $impostazioni;
|
$amministratore->impostazioni = $impostazioni;
|
||||||
|
|
@ -187,13 +198,9 @@ public function disconnectAccount(Amministratore $amministratore, ?string $accou
|
||||||
'disconnected_at' => now()->toDateTimeString(),
|
'disconnected_at' => now()->toDateTimeString(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$google['accounts'] = $accounts;
|
$google['accounts'] = $accounts;
|
||||||
if ($this->normalizeAccountKey((string) Arr::get($google, 'default_account_key', '')) === $key) {
|
$google['default_account_key'] = $this->resolveDefaultAccountKey($accounts, $this->normalizeAccountKey((string) Arr::get($google, 'default_account_key', '')));
|
||||||
$google['oauth'] = [
|
$this->syncLegacyOauthState($google, $accounts, (string) $google['default_account_key']);
|
||||||
'connected' => false,
|
|
||||||
'disconnected_at' => now()->toDateTimeString(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$impostazioni['google'] = $google;
|
$impostazioni['google'] = $google;
|
||||||
$amministratore->impostazioni = $impostazioni;
|
$amministratore->impostazioni = $impostazioni;
|
||||||
|
|
@ -332,4 +339,50 @@ private function hasUsableOauthPayload(array $oauth): bool
|
||||||
|| trim((string) ($oauth['refresh_token'] ?? '')) !== ''
|
|| trim((string) ($oauth['refresh_token'] ?? '')) !== ''
|
||||||
|| trim((string) ($oauth['access_token'] ?? '')) !== '';
|
|| trim((string) ($oauth['access_token'] ?? '')) !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, mixed>> $accounts
|
||||||
|
*/
|
||||||
|
private function resolveDefaultAccountKey(array $accounts, ?string $preferredKey = null): string
|
||||||
|
{
|
||||||
|
$candidate = $this->normalizeAccountKey($preferredKey);
|
||||||
|
if ($candidate !== '' && isset($accounts[$candidate]) && ! $this->isScopedAccountKey($candidate)) {
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($accounts as $key => $account) {
|
||||||
|
if (! $this->isScopedAccountKey($key)) {
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $google
|
||||||
|
* @param array<string, array<string, mixed>> $accounts
|
||||||
|
*/
|
||||||
|
private function syncLegacyOauthState(array &$google, array $accounts, string $defaultKey): void
|
||||||
|
{
|
||||||
|
$defaultKey = $this->normalizeAccountKey($defaultKey);
|
||||||
|
|
||||||
|
if ($defaultKey !== '' && isset($accounts[$defaultKey]) && ! $this->isScopedAccountKey($defaultKey)) {
|
||||||
|
$account = $accounts[$defaultKey];
|
||||||
|
$google['oauth'] = $this->toLegacyOauthPayload($account);
|
||||||
|
|
||||||
|
$email = trim((string) ($account['email'] ?? ''));
|
||||||
|
if ($email !== '') {
|
||||||
|
$google['workspace_email'] = $email;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$google['oauth'] = [
|
||||||
|
'connected' => false,
|
||||||
|
'disconnected_at' => now()->toDateTimeString(),
|
||||||
|
];
|
||||||
|
unset($google['workspace_email']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<div class="rounded-md border border-sky-200 bg-sky-50 px-3 py-1.5 text-xs text-sky-900">
|
||||||
|
Import FE manuale disponibile dal pulsante in alto. Le fatture gia presenti finiscono come duplicate nell'archivio FE; le ricevute/notifiche SdI vengono saltate e non caricate in contabilita.
|
||||||
|
</div>
|
||||||
|
<a href="{{ $this->getFattureRicevuteUrl() }}" class="inline-flex items-center rounded-md bg-emerald-700 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-600">Fatture ricevute</a>
|
||||||
<a href="{{ $this->getTicketsUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Ticket</a>
|
<a href="{{ $this->getTicketsUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Ticket</a>
|
||||||
<a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Collaboratori</a>
|
<a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Collaboratori</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<div class="text-lg font-semibold">Rubrica clienti fornitore</div>
|
<div class="text-lg font-semibold">Rubrica clienti fornitore</div>
|
||||||
<div class="text-sm text-gray-600">
|
<div class="text-sm text-gray-600">
|
||||||
@if($this->fornitoreLabel)
|
@if($this->fornitoreLabel)
|
||||||
Elenco clienti del singolo fornitore, con sorgente TecnoRepair `TClienti` e collegamento alla scheda condivisa di anagrafica unica quando disponibile.
|
Elenco clienti del singolo fornitore, con sorgente TecnoRepair `TClienti` e collegamento sicuro alla stessa rubrica fornitore quando esiste una scheda condivisa già agganciata.
|
||||||
@else
|
@else
|
||||||
Seleziona un fornitore per aprire la rubrica clienti.
|
Seleziona un fornitore per aprire la rubrica clienti.
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
<div class="rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
||||||
Questa vista resta stretta sul fornitore, ma la scheda definitiva resta quella condivisa di anagrafica unica. Prima importa i clienti con <span class="font-mono text-xs">{{ $this->importCommandHint }}</span>, poi usa questa pagina per lavorare in modo compatto su elenco, recapiti e storico apparecchi.
|
Questa vista resta confinata al perimetro del fornitore. Prima importa i clienti con <span class="font-mono text-xs">{{ $this->importCommandHint }}</span>, poi usa questa pagina per lavorare in modo compatto su elenco, recapiti e storico apparecchi senza uscire nel pannello anagrafico admin.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($this->missingAdminContext)
|
@if($this->missingAdminContext)
|
||||||
|
|
@ -84,7 +84,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="border px-2 py-2 align-top">
|
<td class="border px-2 py-2 align-top">
|
||||||
@if($row['rubrica_url'])
|
@if($row['rubrica_url'])
|
||||||
<a href="{{ $row['rubrica_url'] }}" class="inline-flex items-center rounded-md bg-white px-2 py-1 text-[11px] font-medium text-indigo-700 ring-1 ring-inset ring-indigo-300 hover:bg-indigo-50">Scheda condivisa</a>
|
<a href="{{ $row['rubrica_url'] }}" class="inline-flex items-center rounded-md bg-white px-2 py-1 text-[11px] font-medium text-indigo-700 ring-1 ring-inset ring-indigo-300 hover:bg-indigo-50">Vai al cliente</a>
|
||||||
@else
|
@else
|
||||||
<span class="text-[11px] text-amber-700">Non collegato</span>
|
<span class="text-[11px] text-amber-700">Non collegato</span>
|
||||||
@endif
|
@endif
|
||||||
|
|
@ -140,9 +140,9 @@
|
||||||
<div class="mt-1">{{ $selected->partita_iva ?: '-' }}</div>
|
<div class="mt-1">{{ $selected->partita_iva ?: '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if($selected->rubrica_id)
|
@if($selected->rubrica_id && $selected->rubrica_url)
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<a href="{{ \App\Filament\Pages\Gescon\RubricaUniversaleScheda::getUrl(['record' => (int) $selected->rubrica_id], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500">Apri scheda anagrafica condivisa</a>
|
<a href="{{ $selected->rubrica_url }}" class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500">Apri vista cliente collegata</a>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -186,7 +186,7 @@
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div class="rounded-xl border bg-white p-4">
|
<div class="rounded-xl border bg-white p-4">
|
||||||
<div class="text-sm font-semibold">Google workspace</div>
|
<div class="text-sm font-semibold">Google workspace fornitore</div>
|
||||||
@if($googleWorkspaceStatus)
|
@if($googleWorkspaceStatus)
|
||||||
<div class="mt-3 space-y-2 text-sm">
|
<div class="mt-3 space-y-2 text-sm">
|
||||||
<div><span class="font-medium">Stato:</span> {{ $googleWorkspaceStatus['connected'] ? 'Collegato' : 'Non collegato' }}</div>
|
<div><span class="font-medium">Stato:</span> {{ $googleWorkspaceStatus['connected'] ? 'Collegato' : 'Non collegato' }}</div>
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="mt-3 text-sm text-gray-500">Fornitore senza amministratore collegato: stato Google non disponibile.</div>
|
<div class="mt-3 text-sm text-gray-500">Stato Google del fornitore non disponibile.</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user