Isolate supplier archive settings and tighten FE matching

This commit is contained in:
michele 2026-04-14 16:35:26 +00:00
parent 8ce48a7a27
commit 24c2318436
4 changed files with 158 additions and 24 deletions

View File

@ -8,6 +8,7 @@
use App\Models\Stabile; use App\Models\Stabile;
use App\Models\User; use App\Models\User;
use App\Support\ArchivioPaths; use App\Support\ArchivioPaths;
use App\Support\GoogleAccountStore;
use BackedEnum; use BackedEnum;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
@ -130,12 +131,18 @@ public function importTecnoRepairFromArchive(bool $dryRun = false): void
return; return;
} }
$mdbPath = trim($this->tecnorepairMdbPath); $mdbPath = $this->resolveTecnoRepairMdbPath($this->tecnorepairMdbPath);
if ($mdbPath === '' || ! is_file($mdbPath)) { if ($mdbPath === null) {
Notification::make()->title('Percorso MDB non valido')->warning()->send(); Notification::make()
->title('Percorso MDB non valido')
->body('Indica un file .mdb esistente sul server oppure salva prima l\'upload nell\'archivio fornitore.')
->warning()
->send();
return; return;
} }
$this->tecnorepairMdbPath = $mdbPath;
$adminId = (int) ($this->fornitore->amministratore_id ?? 0); $adminId = (int) ($this->fornitore->amministratore_id ?? 0);
if ($adminId <= 0) { if ($adminId <= 0) {
Notification::make()->title('Amministratore del fornitore non trovato')->danger()->send(); Notification::make()->title('Amministratore del fornitore non trovato')->danger()->send();
@ -390,7 +397,7 @@ private function refreshArchiveSummary(): void
$this->loadOperationalWorkflowSettings(); $this->loadOperationalWorkflowSettings();
$this->moduleRows = [ $this->moduleRows = [
['module' => 'Rubrica fornitore', 'status' => 'pronto', 'note' => 'Vista fornitore filtrata ma agganciata all\'anagrafica unica.'], ['module' => 'Rubrica fornitore', 'status' => 'pronto', 'note' => 'Vista fornitore filtrata ma agganciata all\'anagrafica unica.'],
['module' => 'Google Workspace', 'status' => data_get($this->googleWorkspaceStatus, 'connected', false) ? 'collegato' : 'da collegare', 'note' => 'Rubrica e sincronizzazioni smartphone riusano l\'account Google del contesto amministratore.'], ['module' => 'Google Workspace', 'status' => data_get($this->googleWorkspaceStatus, 'connected', false) ? 'collegato' : 'da collegare', 'note' => 'Account dedicato al fornitore, isolato con chiave tecnica separata pur restando governato dal backend amministratore.'],
['module' => 'TecnoRepair MDB', 'status' => ((int) ($stats['total'] ?? 0) > 0) ? 'importato' : 'da importare', 'note' => 'Schede: ' . (int) ($stats['total'] ?? 0) . ' · aperte: ' . (int) ($stats['open'] ?? 0) . ' · chiuse: ' . (int) ($stats['closed'] ?? 0)], ['module' => 'TecnoRepair MDB', 'status' => ((int) ($stats['total'] ?? 0) > 0) ? 'importato' : 'da importare', 'note' => 'Schede: ' . (int) ($stats['total'] ?? 0) . ' · aperte: ' . (int) ($stats['open'] ?? 0) . ' · chiuse: ' . (int) ($stats['closed'] ?? 0)],
['module' => 'Catalogo fornitore', 'status' => 'attivo', 'note' => 'Il catalogo vero resta nella pagina Prodotti.'], ['module' => 'Catalogo fornitore', 'status' => 'attivo', 'note' => 'Il catalogo vero resta nella pagina Prodotti.'],
['module' => 'Chiavi e reperibilità', 'status' => count($this->stabileAccessRows) > 0 ? 'configurato' : 'da configurare', 'note' => 'Istruzioni per accesso, chi suonare e dove recuperare chiavi per ciascuno stabile.'], ['module' => 'Chiavi e reperibilità', 'status' => count($this->stabileAccessRows) > 0 ? 'configurato' : 'da configurare', 'note' => 'Istruzioni per accesso, chi suonare e dove recuperare chiavi per ciascuno stabile.'],
@ -545,16 +552,73 @@ private function resolveGoogleWorkspaceStatus(): ?array
return null; return null;
} }
$oauth = (array) (($amministratore->impostazioni['google']['oauth'] ?? null) ?: []); $account = app(GoogleAccountStore::class)->get($amministratore, $this->getSupplierGoogleAccountKey());
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'] ?? ''),
]; ];
} }
private function getSupplierGoogleAccountKey(): string
{
return 'fornitore-' . (int) ($this->fornitore?->id ?? 0);
}
private function resolveTecnoRepairMdbPath(?string $rawPath): ?string
{
$path = trim((string) $rawPath);
if ($path === '') {
return null;
}
$storageBase = str_replace('\\', '/', storage_path('app'));
$normalizedInput = str_replace('\\', '/', $path);
$candidatePaths = [];
if (is_file($path)) {
$candidatePaths[] = $path;
}
if (str_starts_with($normalizedInput, $storageBase . '/')) {
$relative = ltrim(substr($normalizedInput, strlen($storageBase)), '/');
if ($relative !== '' && Storage::disk('local')->exists($relative)) {
$candidatePaths[] = storage_path('app/' . $relative);
}
}
if (! str_starts_with($normalizedInput, '/')) {
$relative = ltrim($normalizedInput, '/');
if (Storage::disk('local')->exists($relative)) {
$candidatePaths[] = storage_path('app/' . $relative);
}
}
foreach (array_values(array_unique($candidatePaths)) as $candidate) {
$real = realpath($candidate);
if (is_string($real) && $real !== '' && is_file($real)) {
return $real;
}
if (is_file($candidate)) {
return $candidate;
}
}
return null;
}
private function getArchiveRelativeBase(): ?string private function getArchiveRelativeBase(): ?string
{ {
if (! $this->fornitore instanceof Fornitore) { if (! $this->fornitore instanceof Fornitore) {

View File

@ -753,11 +753,19 @@ private function splitTags(string $value): array
private function canonicalizeTag(string $raw): ?string private function canonicalizeTag(string $raw): ?string
{ {
$clean = trim(mb_strtolower($raw)); $clean = trim(mb_strtolower($raw));
if ($clean === 'pc') {
return 'pc';
}
if ($clean === '' || mb_strlen($clean) < 3) { if ($clean === '' || mb_strlen($clean) < 3) {
return null; return null;
} }
$map = [ $map = [
'informat' => 'informatica',
'assist' => 'assistenza',
'computer' => 'pc',
'apple' => 'apple',
'idr' => 'idraulico', 'idr' => 'idraulico',
'idraul' => 'idraulico', 'idraul' => 'idraulico',
'elett' => 'elettricista', 'elett' => 'elettricista',
@ -782,6 +790,7 @@ private function hydrateBoxData(User $user): void
{ {
$this->fattureAssociate = []; $this->fattureAssociate = [];
$this->raVersateRows = []; $this->raVersateRows = [];
$identityCandidates = $this->getSupplierIdentityCandidates();
$this->box = [ $this->box = [
'stabile_id' => null, 'stabile_id' => null,
@ -967,17 +976,8 @@ private function hydrateBoxData(User $user): void
if (Schema::hasTable('fatture_elettroniche')) { if (Schema::hasTable('fatture_elettroniche')) {
$feBase = FatturaElettronica::query() $feBase = FatturaElettronica::query()
->where('stabile_id', $activeStabileId) ->where('stabile_id', $activeStabileId)
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void { ->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($identityCandidates): void {
$q->where('fornitore_id', $this->fornitore->id); $this->applySupplierIdentityFilterToFeQuery($q, $identityCandidates);
if (! empty($ids)) {
$q->orWhere(function (\Illuminate\Database\Eloquent\Builder $qq) use ($ids): void {
foreach ($ids as $id) {
$qq->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$id])
->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$id]);
}
});
}
}); });
$this->box['fe']['count'] = (int) (clone $feBase)->count(); $this->box['fe']['count'] = (int) (clone $feBase)->count();
@ -1008,7 +1008,16 @@ private function hydrateBoxData(User $user): void
if (Schema::hasTable('contabilita_fatture_fornitori')) { if (Schema::hasTable('contabilita_fatture_fornitori')) {
$contabBase = ContabilitaFatturaFornitore::query() $contabBase = ContabilitaFatturaFornitore::query()
->where('stabile_id', $activeStabileId) ->where('stabile_id', $activeStabileId)
->where('fornitore_id', (int) $this->fornitore->id); ->where('fornitore_id', (int) $this->fornitore->id)
->when($identityCandidates !== [], function ($query) use ($identityCandidates): void {
$query->where(function (\Illuminate\Database\Eloquent\Builder $inner) use ($identityCandidates): void {
$inner->whereNull('fattura_elettronica_id')
->orWhereDoesntHave('fatturaElettronica')
->orWhereHas('fatturaElettronica', function (\Illuminate\Database\Eloquent\Builder $feQuery) use ($identityCandidates): void {
$this->applySupplierIdentityFilterToFeQuery($feQuery, $identityCandidates);
});
});
});
// Coerente con la logica della scheda contabile: aperto = totale - pagato. // Coerente con la logica della scheda contabile: aperto = totale - pagato.
$totNetto = (float) (clone $contabBase)->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto'); $totNetto = (float) (clone $contabBase)->selectRaw('COALESCE(SUM(netto_da_pagare), 0) as netto')->value('netto');
@ -1088,6 +1097,65 @@ private function hydrateBoxData(User $user): void
} }
} }
/**
* @return array<int, string>
*/
private function getSupplierIdentityCandidates(): array
{
$values = [];
foreach ([(string) ($this->fornitore->partita_iva ?? ''), (string) ($this->fornitore->codice_fiscale ?? '')] as $raw) {
$normalized = strtoupper(str_replace(' ', '', trim($raw)));
if ($normalized === '' || $normalized === 'ND') {
continue;
}
$values[] = $normalized;
if (str_starts_with($normalized, 'IT') && strlen($normalized) > 2) {
$values[] = substr($normalized, 2);
}
}
$values = array_values(array_unique(array_filter($values, fn(string $value): bool => $value !== '')));
sort($values, SORT_NATURAL | SORT_FLAG_CASE);
return $values;
}
/**
* @param array<int, string> $identityCandidates
*/
private function applySupplierIdentityFilterToFeQuery(\Illuminate\Database\Eloquent\Builder $query, array $identityCandidates): void
{
$fornitoreId = (int) $this->fornitore->id;
if ($identityCandidates === []) {
$query->where('fornitore_id', $fornitoreId);
return;
}
$query->where(function (\Illuminate\Database\Eloquent\Builder $outer) use ($identityCandidates, $fornitoreId): void {
$outer->where(function (\Illuminate\Database\Eloquent\Builder $identityQuery) use ($identityCandidates): void {
foreach ($identityCandidates as $identity) {
$identityQuery->orWhereRaw("REPLACE(UPPER(fornitore_piva), ' ', '') = ?", [$identity])
->orWhereRaw("REPLACE(UPPER(fornitore_cf), ' ', '') = ?", [$identity]);
}
})->orWhere(function (\Illuminate\Database\Eloquent\Builder $fallbackQuery) use ($fornitoreId): void {
$fallbackQuery->where('fornitore_id', $fornitoreId)
->where(function (\Illuminate\Database\Eloquent\Builder $missingPiva): void {
$missingPiva->whereNull('fornitore_piva')
->orWhere('fornitore_piva', '')
->orWhere('fornitore_piva', 'ND');
})
->where(function (\Illuminate\Database\Eloquent\Builder $missingCf): void {
$missingCf->whereNull('fornitore_cf')
->orWhere('fornitore_cf', '')
->orWhere('fornitore_cf', 'ND');
});
});
});
}
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [

View File

@ -1453,7 +1453,9 @@ private function updateExisting(FatturaElettronica $existing, string $xml, ?stri
$fornitorePiva = $existing->fornitore_piva ?: 'ND'; $fornitorePiva = $existing->fornitore_piva ?: 'ND';
} }
$fornitoreId = $this->matchFornitoreId($fornitorePiva, $fornitoreCf); $stabile = Stabile::query()->select(['id', 'amministratore_id'])->find($stabileId);
$adminId = (int) ($stabile?->amministratore_id ?: 0);
$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) {

View File

@ -43,7 +43,7 @@
<div class="rounded-xl border bg-white p-4"> <div class="rounded-xl border bg-white p-4">
<div class="text-sm font-semibold">TecnoRepair MDB</div> <div class="text-sm font-semibold">TecnoRepair MDB</div>
<div class="mt-1 text-xs text-gray-500">Puoi caricare un archivio MDB dalla pagina web oppure indicare un percorso già disponibile sul server, poi lanciare la sincronizzazione verso il fornitore.</div> <div class="mt-1 text-xs text-gray-500">Puoi caricare un archivio MDB dalla pagina web oppure indicare un percorso già disponibile sul server. Sono accettati sia path assoluti sia percorsi relativi dentro <span class="font-mono">storage/app</span>.</div>
<div class="mt-4 grid gap-3 md:grid-cols-2"> <div class="mt-4 grid gap-3 md:grid-cols-2">
<label class="block text-sm md:col-span-2"> <label class="block text-sm md:col-span-2">
@ -248,7 +248,7 @@
</form> </form>
@endif @endif
</div> </div>
<div class="mt-2 text-xs text-gray-500">Questo collegamento prepara la sincronizzazione rubrica lato fornitore e l'uso dei dati anche sul cellulare tramite l'account Google già governato dal backend.</div> <div class="mt-2 text-xs text-gray-500">Questo collegamento usa una chiave account dedicata al fornitore, così rubrica e sincronizzazioni non si mischiano con il profilo amministratore anche se il backend resta sotto lo stesso tenant.</div>
@else @else
<div class="mt-3 text-sm text-gray-500">Contesto amministratore Google non disponibile per questo fornitore.</div> <div class="mt-3 text-sm text-gray-500">Contesto amministratore Google non disponibile per questo fornitore.</div>
@endif @endif