Isolate supplier archive settings and tighten FE matching
This commit is contained in:
parent
8ce48a7a27
commit
24c2318436
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 [
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user