Compare commits
4 Commits
2639c0a0e3
...
24c2318436
| Author | SHA1 | Date | |
|---|---|---|---|
| 24c2318436 | |||
| 8ce48a7a27 | |||
| a509c71189 | |||
| fe9e8adb72 |
|
|
@ -223,6 +223,7 @@ public function handle(): int
|
|||
|
||||
$fornitorePayload = [
|
||||
'amministratore_id' => $amministratoreId,
|
||||
'cod_forn' => $codForn,
|
||||
'rubrica_id' => $rubrica->id,
|
||||
'codice_univoco' => (is_string($rubricaCode) && $rubricaCode !== '') ? $rubricaCode : null,
|
||||
'ragione_sociale' => $ragione ?? $rubrica->ragione_sociale,
|
||||
|
|
@ -256,6 +257,12 @@ public function handle(): int
|
|||
->where('old_id', $legacyId)
|
||||
->first();
|
||||
}
|
||||
if (! $existing && $codForn !== null && Schema::hasColumn('fornitori', 'cod_forn')) {
|
||||
$existing = Fornitore::query()
|
||||
->where('amministratore_id', $amministratoreId)
|
||||
->where('cod_forn', $codForn)
|
||||
->first();
|
||||
}
|
||||
if (! $existing && ! empty($fornitorePayload['rubrica_id'])) {
|
||||
$existing = Fornitore::query()
|
||||
->where('amministratore_id', $amministratoreId)
|
||||
|
|
|
|||
|
|
@ -250,6 +250,11 @@ private function resolveLocalFornitore(object $legacy): ?Fornitore
|
|||
$cf = trim((string) ($legacy->codice_fiscale ?? ''));
|
||||
$ragione = trim((string) ($legacy->ragione_sociale ?? ''));
|
||||
|
||||
$byLegacyCode = $this->resolveLocalFornitoreByLegacyIds($legacyId, $legacyCod, $piva, $cf, $this->normalizeComparableString($ragione));
|
||||
if ($byLegacyCode instanceof Fornitore) {
|
||||
return $byLegacyCode;
|
||||
}
|
||||
|
||||
if ($piva !== '') {
|
||||
$found = Fornitore::query()->where('partita_iva', $piva)->first();
|
||||
if ($found instanceof Fornitore) {
|
||||
|
|
@ -280,20 +285,49 @@ private function resolveLocalFornitore(object $legacy): ?Fornitore
|
|||
}
|
||||
}
|
||||
|
||||
return $this->resolveLocalFornitoreByLegacyIds($legacyId, $legacyCod, $piva, $cf, $normalizedRagione);
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveLocalFornitoreByLegacyIds(int $legacyId, string $legacyCod, string $piva, string $cf, string $normalizedRagione): ?Fornitore
|
||||
{
|
||||
$candidateIds = collect([
|
||||
$legacyId > 0 ? $legacyId : null,
|
||||
$candidateCodes = collect([
|
||||
($legacyCod !== '' && ctype_digit($legacyCod)) ? (int) $legacyCod : null,
|
||||
$legacyId > 0 ? $legacyId : null,
|
||||
])
|
||||
->filter(fn($value): bool => is_int($value) && $value > 0)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
foreach ($candidateIds as $candidateId) {
|
||||
if (Schema::hasColumn('fornitori', 'cod_forn')) {
|
||||
foreach ($candidateCodes as $candidateCode) {
|
||||
$matches = Fornitore::query()
|
||||
->where('cod_forn', (int) $candidateCode)
|
||||
->get(['id', 'cod_forn', 'partita_iva', 'codice_fiscale', 'ragione_sociale']);
|
||||
|
||||
if ($matches->count() === 1) {
|
||||
return $matches->first();
|
||||
}
|
||||
|
||||
$filtered = $matches->filter(function (Fornitore $fornitore) use ($piva, $cf, $normalizedRagione): bool {
|
||||
if ($piva !== '' && trim((string) ($fornitore->partita_iva ?? '')) === $piva) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($cf !== '' && trim((string) ($fornitore->codice_fiscale ?? '')) === $cf) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $normalizedRagione !== ''
|
||||
&& $this->normalizeComparableString((string) ($fornitore->ragione_sociale ?? '')) === $normalizedRagione;
|
||||
})->values();
|
||||
|
||||
if ($filtered->count() === 1) {
|
||||
return $filtered->first();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidateCodes as $candidateId) {
|
||||
$matches = Fornitore::query()
|
||||
->where('old_id', (int) $candidateId)
|
||||
->get(['id', 'partita_iva', 'codice_fiscale', 'ragione_sociale']);
|
||||
|
|
@ -361,6 +395,11 @@ private function extractKeywordTags(string $input): array
|
|||
}
|
||||
|
||||
$keywords = [
|
||||
'informat' => 'informatica',
|
||||
'assist' => 'assistenza',
|
||||
'computer' => 'pc',
|
||||
' pc ' => 'pc',
|
||||
'apple' => 'apple',
|
||||
'idr' => 'idraulico',
|
||||
'termoidraul' => 'termoidraulico',
|
||||
'elettric' => 'elettricista',
|
||||
|
|
@ -425,6 +464,10 @@ private function canonicalizeTag(string $raw): ?string
|
|||
{
|
||||
$clean = trim(mb_strtolower($raw), " \t\n\r\0\x0B-_,.;:");
|
||||
$clean = preg_replace('/\s+/', ' ', $clean) ?? '';
|
||||
if ($clean === 'pc') {
|
||||
return 'pc';
|
||||
}
|
||||
|
||||
if ($clean === '' || mb_strlen($clean) < 3) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -438,6 +481,10 @@ private function canonicalizeTag(string $raw): ?string
|
|||
}
|
||||
|
||||
$map = [
|
||||
'informat' => 'informatica',
|
||||
'assist' => 'assistenza',
|
||||
'computer' => 'pc',
|
||||
'apple' => 'apple',
|
||||
'idr' => 'idraulico',
|
||||
'idraul' => 'idraulico',
|
||||
'elett' => 'elettricista',
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@
|
|||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
use Filament\Tables\Columns\ImageColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Concerns\InteractsWithTable;
|
||||
use Filament\Tables\Contracts\HasTable;
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
|
@ -224,8 +226,17 @@ public function table(Table $table): Table
|
|||
}),
|
||||
])
|
||||
->columns([
|
||||
TextColumn::make('periodo_dal')->label('Dal')->date('d/m/Y')->sortable()->toggleable(),
|
||||
TextColumn::make('periodo_al')->label('Al')->date('d/m/Y')->sortable(),
|
||||
ImageColumn::make('miniatura_foto')
|
||||
->label('Foto')
|
||||
->getStateUsing(fn(StabileServizioLettura $record): ?string => $this->resolveReadingPrimaryPhotoPath($record))
|
||||
->disk('public')
|
||||
->square()
|
||||
->size(42)
|
||||
->toggleable()
|
||||
->extraAttributes(['class' => 'py-2']),
|
||||
|
||||
TextColumn::make('periodo_dal')->label('Dal')->date('d/m/Y')->sortable()->toggleable()->extraAttributes(['class' => 'text-[11px] leading-tight']),
|
||||
TextColumn::make('periodo_al')->label('Al')->date('d/m/Y')->sortable()->extraAttributes(['class' => 'text-[11px] leading-tight']),
|
||||
|
||||
TextColumn::make('stabile.denominazione')
|
||||
->label('Stabile')
|
||||
|
|
@ -240,7 +251,19 @@ public function table(Table $table): Table
|
|||
|
||||
return trim($codice . ($nome !== '' ? (' - ' . $nome) : '')) ?: ('Stabile #' . $stabile->id);
|
||||
})
|
||||
->wrap()
|
||||
->limit(36)
|
||||
->tooltip(function (StabileServizioLettura $record): string {
|
||||
$stabile = $record->stabile;
|
||||
if (! $stabile instanceof Stabile) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$codice = trim((string) ($stabile->codice_stabile ?? ''));
|
||||
$nome = trim((string) ($stabile->denominazione ?? ''));
|
||||
|
||||
return trim($codice . ($nome !== '' ? (' - ' . $nome) : '')) ?: ('Stabile #' . $stabile->id);
|
||||
})
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: $this->archivioScope !== 'all'),
|
||||
|
||||
TextColumn::make('servizio.nome')
|
||||
|
|
@ -254,23 +277,37 @@ public function table(Table $table): Table
|
|||
$tipo = trim((string) ($record->servizio?->tipo ?? ''));
|
||||
return $tipo !== '' ? strtoupper($tipo) : '—';
|
||||
})
|
||||
->wrap()
|
||||
->limit(30)
|
||||
->tooltip(function (StabileServizioLettura $record): string {
|
||||
$nome = trim((string) ($record->servizio?->nome ?? ''));
|
||||
if ($nome !== '') {
|
||||
return $nome;
|
||||
}
|
||||
|
||||
$tipo = trim((string) ($record->servizio?->tipo ?? ''));
|
||||
return $tipo !== '' ? strtoupper($tipo) : '—';
|
||||
})
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('servizio.contatore_matricola')->label('Matricola')->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('servizio.contatore_matricola')->label('Matricola')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']),
|
||||
|
||||
TextColumn::make('unitaImmobiliare.denominazione')
|
||||
->label('Attribuzione')
|
||||
->formatStateUsing(function ($state, StabileServizioLettura $record): string {
|
||||
return $this->resolveReadingAssignmentLabel($record);
|
||||
})
|
||||
->wrap()
|
||||
->limit(34)
|
||||
->tooltip(fn(StabileServizioLettura $record): string => $this->resolveReadingAssignmentLabel($record))
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('rilevatore_nome')
|
||||
->label('Letturista / nominativo')
|
||||
->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—')
|
||||
->wrap()
|
||||
->limit(28)
|
||||
->tooltip(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('consumo_valore')
|
||||
|
|
@ -284,13 +321,15 @@ public function table(Table $table): Table
|
|||
$unit = trim((string) ($record->consumo_unita ?? ''));
|
||||
return trim((string) $val) . ($unit !== '' ? (' ' . $unit) : '');
|
||||
})
|
||||
->sortable(),
|
||||
->sortable()
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight font-medium']),
|
||||
|
||||
TextColumn::make('importo_totale')->label('Importo')->money('EUR')->sortable()->toggleable(),
|
||||
TextColumn::make('importo_totale')->label('Importo')->money('EUR')->sortable()->toggleable()->extraAttributes(['class' => 'text-[11px] leading-tight']),
|
||||
|
||||
TextColumn::make('tipologia_lettura')
|
||||
->label('Tipo lettura')
|
||||
->formatStateUsing(fn($state) => $tipologiaOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—'))
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('canale_acquisizione')
|
||||
|
|
@ -307,12 +346,14 @@ public function table(Table $table): Table
|
|||
default => $v !== '' ? strtoupper($v) : '—',
|
||||
};
|
||||
})
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('workflow_stato')
|
||||
->label('Workflow')
|
||||
->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? (string) $state : 'Acquisita')
|
||||
->badge()
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('raw.consistency_check.summary')
|
||||
|
|
@ -329,7 +370,12 @@ public function table(Table $table): Table
|
|||
default => 'gray',
|
||||
};
|
||||
})
|
||||
->wrap()
|
||||
->limit(28)
|
||||
->tooltip(function (StabileServizioLettura $record): string {
|
||||
$summary = trim((string) data_get($record->raw, 'consistency_check.summary', ''));
|
||||
return $summary !== '' ? $summary : '—';
|
||||
})
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('lettura_foto_original_name')
|
||||
|
|
@ -342,6 +388,18 @@ public function table(Table $table): Table
|
|||
|
||||
return filled($record->lettura_foto_path) ? '1 foto' : '—';
|
||||
})
|
||||
->tooltip(function (StabileServizioLettura $record): string {
|
||||
$photos = $this->getReadingPhotoEntries($record);
|
||||
if ($photos === []) {
|
||||
return 'Nessuna foto';
|
||||
}
|
||||
|
||||
return implode("\n", array_map(
|
||||
fn(array $photo): string => (string) ($photo['name'] ?? basename((string) ($photo['path'] ?? 'foto'))),
|
||||
$photos
|
||||
));
|
||||
})
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('protocollo_numero')
|
||||
|
|
@ -356,55 +414,72 @@ public function table(Table $table): Table
|
|||
|
||||
return trim($categoria . ($numero !== '' ? (' #' . $numero) : ''));
|
||||
})
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('prossima_lettura_scadenza_at')
|
||||
->label('Scadenza prossima lettura')
|
||||
->dateTime('d/m/Y H:i')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('deadline_lettura_at')
|
||||
->label('Deadline campagna')
|
||||
->dateTime('d/m/Y H:i')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('sollecito_inviato_at')
|
||||
->label('Sollecito')
|
||||
->dateTime('d/m/Y H:i')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('rilevatore_tipo')
|
||||
->label('Rilevatore')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('lettura_precedente_valore')
|
||||
->label('Lettura precedente')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']),
|
||||
|
||||
TextColumn::make('archivio_documentale_path')
|
||||
->label('Archivio documentale')
|
||||
->wrap()
|
||||
->limit(36)
|
||||
->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('fatturaElettronica.numero_fattura')
|
||||
->label('Fattura FE')
|
||||
->url(fn(StabileServizioLettura $record): ?string => $record->fattura_elettronica_id
|
||||
? FatturaElettronicaScheda::getUrl(['id' => (int) $record->fattura_elettronica_id], panel : 'admin-filament')
|
||||
: null)
|
||||
->openUrlInNewTab()
|
||||
->formatStateUsing(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(),
|
||||
|
||||
TextColumn::make('voceSpesa.descrizione')->label('Voce spesa')->wrap()->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('voceSpesa.descrizione')
|
||||
->label('Voce spesa')
|
||||
->limit(30)
|
||||
->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('voceSpesa.tipo_gestione')
|
||||
->label('Gestione')
|
||||
->formatStateUsing(fn($state) => $gestioneOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—'))
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('fornitore.ragione_sociale')->label('Fornitore')->wrap()->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('fornitore.ragione_sociale')
|
||||
->label('Fornitore')
|
||||
->limit(30)
|
||||
->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—')
|
||||
->extraAttributes(['class' => 'text-[11px] leading-tight'])
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
TextColumn::make('created_at')->label('Creato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true),
|
||||
TextColumn::make('created_at')->label('Creato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']),
|
||||
])
|
||||
->headerActions([
|
||||
Action::make('toggleScope')
|
||||
|
|
@ -574,20 +649,47 @@ public function table(Table $table): Table
|
|||
Action::make('apriFoto')
|
||||
->label('Foto')
|
||||
->icon('heroicon-o-photo')
|
||||
->url(fn(StabileServizioLettura $record): ?string => $this->resolveReadingPrimaryPhotoUrl($record))
|
||||
->openUrlInNewTab()
|
||||
->visible(fn(StabileServizioLettura $record): bool => $this->resolveReadingPrimaryPhotoUrl($record) !== null),
|
||||
->button()
|
||||
->color('gray')
|
||||
->modalHeading('Foto contatore')
|
||||
->modalWidth('5xl')
|
||||
->modalSubmitAction(false)
|
||||
->modalCancelActionLabel('Chiudi')
|
||||
->modalContent(function (StabileServizioLettura $record): View {
|
||||
return view('filament.pages.condomini.partials.lettura-servizio-foto-modal', [
|
||||
'record' => $record,
|
||||
'photos' => $this->getReadingPhotoEntries($record),
|
||||
'mapUrl' => $this->resolveReadingMapUrl($record),
|
||||
'assigned' => $this->resolveReadingAssignmentLabel($record),
|
||||
]);
|
||||
})
|
||||
->visible(fn(StabileServizioLettura $record): bool => $this->getReadingPhotoEntries($record) !== []),
|
||||
|
||||
Action::make('mappaFoto')
|
||||
->label('Mappa')
|
||||
->icon('heroicon-o-map')
|
||||
->button()
|
||||
->color('gray')
|
||||
->url(fn(StabileServizioLettura $record): ?string => $this->resolveReadingMapUrl($record))
|
||||
->openUrlInNewTab()
|
||||
->visible(fn(StabileServizioLettura $record): bool => $this->resolveReadingMapUrl($record) !== null),
|
||||
|
||||
Action::make('apriFatturaFe')
|
||||
->label('FE')
|
||||
->icon('heroicon-o-receipt-percent')
|
||||
->button()
|
||||
->color('primary')
|
||||
->url(fn(StabileServizioLettura $record): ?string => $record->fattura_elettronica_id
|
||||
? FatturaElettronicaScheda::getUrl(['record' => (int) $record->fattura_elettronica_id], panel: 'admin-filament')
|
||||
: null)
|
||||
->openUrlInNewTab()
|
||||
->visible(fn(StabileServizioLettura $record): bool => (int) ($record->fattura_elettronica_id ?? 0) > 0),
|
||||
|
||||
Action::make('riattribuisci')
|
||||
->label('Riattribuisci')
|
||||
->icon('heroicon-o-user-circle')
|
||||
->button()
|
||||
->color('gray')
|
||||
->form([
|
||||
Select::make('unita_immobiliare_id')
|
||||
->label('Unità / nominativo')
|
||||
|
|
@ -646,6 +748,7 @@ public function table(Table $table): Table
|
|||
Action::make('spostaContatoreComune')
|
||||
->label('Sposta a contatore comune')
|
||||
->icon('heroicon-o-arrow-path-rounded-square')
|
||||
->button()
|
||||
->color('warning')
|
||||
->form([
|
||||
Select::make('stabile_servizio_id')
|
||||
|
|
@ -728,6 +831,8 @@ public function table(Table $table): Table
|
|||
Action::make('edit')
|
||||
->label('Modifica')
|
||||
->icon('heroicon-o-pencil-square')
|
||||
->button()
|
||||
->color('gray')
|
||||
->form([
|
||||
Select::make('stabile_servizio_id')
|
||||
->label('Servizio')
|
||||
|
|
@ -847,6 +952,7 @@ public function table(Table $table): Table
|
|||
Action::make('delete')
|
||||
->label('Elimina')
|
||||
->icon('heroicon-o-trash')
|
||||
->button()
|
||||
->color('danger')
|
||||
->requiresConfirmation()
|
||||
->action(fn(StabileServizioLettura $record) => $record->delete()),
|
||||
|
|
@ -1345,21 +1451,55 @@ private function resolveReadingAssignmentLabel(StabileServizioLettura $record):
|
|||
return $parts !== [] ? implode(' - ', $parts) : 'Contatore generale / bene comune';
|
||||
}
|
||||
|
||||
private function resolveReadingPrimaryPhotoPath(StabileServizioLettura $record): ?string
|
||||
/**
|
||||
* @return array<int, array{name: string, path: string, url: string, latitude: float|null, longitude: float|null}>
|
||||
*/
|
||||
private function getReadingPhotoEntries(StabileServizioLettura $record): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
$append = function (string $path, ?string $name = null, $lat = null, $lng = null) use (&$entries): void {
|
||||
$path = trim($path);
|
||||
if ($path === '' || isset($entries[$path]) || ! Storage::disk('public')->exists($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entries[$path] = [
|
||||
'name' => trim((string) ($name ?? '')) !== '' ? trim((string) $name) : basename($path),
|
||||
'path' => $path,
|
||||
'url' => Storage::disk('public')->url($path),
|
||||
'latitude' => is_numeric($lat) ? (float) $lat : null,
|
||||
'longitude' => is_numeric($lng) ? (float) $lng : null,
|
||||
];
|
||||
};
|
||||
|
||||
if (filled($record->lettura_foto_path)) {
|
||||
return (string) $record->lettura_foto_path;
|
||||
$append(
|
||||
(string) $record->lettura_foto_path,
|
||||
(string) ($record->lettura_foto_original_name ?? ''),
|
||||
data_get($record->lettura_foto_metadata, 'gps_decimal.lat'),
|
||||
data_get($record->lettura_foto_metadata, 'gps_decimal.lng')
|
||||
);
|
||||
}
|
||||
|
||||
$photos = is_array(data_get($record->lettura_foto_metadata, 'photos')) ? data_get($record->lettura_foto_metadata, 'photos') : [];
|
||||
foreach ($photos as $photo) {
|
||||
$path = trim((string) ($photo['path'] ?? ''));
|
||||
if ($path !== '') {
|
||||
return $path;
|
||||
}
|
||||
$append(
|
||||
(string) ($photo['path'] ?? ''),
|
||||
(string) ($photo['original_name'] ?? $photo['stored_basename'] ?? ''),
|
||||
data_get($photo, 'metadata.gps_decimal.lat'),
|
||||
data_get($photo, 'metadata.gps_decimal.lng')
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return array_values($entries);
|
||||
}
|
||||
|
||||
private function resolveReadingPrimaryPhotoPath(StabileServizioLettura $record): ?string
|
||||
{
|
||||
$photos = $this->getReadingPhotoEntries($record);
|
||||
|
||||
return $photos !== [] ? (string) ($photos[0]['path'] ?? '') : null;
|
||||
}
|
||||
|
||||
private function resolveReadingPrimaryPhotoUrl(StabileServizioLettura $record): ?string
|
||||
|
|
|
|||
|
|
@ -2,10 +2,15 @@
|
|||
namespace App\Filament\Pages\Contabilita;
|
||||
|
||||
use App\Filament\Pages\Contabilita\FatturaElettronicaScheda;
|
||||
use App\Models\Documento;
|
||||
use App\Jobs\RunCassettoFiscaleDownload;
|
||||
use App\Models\FatturaElettronica;
|
||||
use App\Models\Stabile;
|
||||
use App\Models\User;
|
||||
use App\Services\Consumi\AcquaPdfTextParser;
|
||||
use App\Services\Consumi\ConsumiAcquaIngestionService;
|
||||
use App\Services\Consumi\ConsumiAcquaTariffeIngestionService;
|
||||
use App\Services\Documenti\PdfTextExtractionService;
|
||||
use App\Services\FattureElettroniche\CassettoFiscaleDownloadService;
|
||||
use App\Services\FattureElettroniche\FatturaElettronicaImporter;
|
||||
use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService;
|
||||
|
|
@ -110,6 +115,169 @@ private function safeUtf8(string $value): string
|
|||
return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value) ?? $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{status: string, created_letture?: int, updated_letture?: int, tickets?: int, message?: string}
|
||||
*/
|
||||
private function autoLinkWaterReadingsForInvoice(int $fatturaId, int $userId): array
|
||||
{
|
||||
$fattura = FatturaElettronica::query()->find($fatturaId);
|
||||
if (! $fattura instanceof FatturaElettronica) {
|
||||
return ['status' => 'skipped', 'message' => 'Fattura non trovata'];
|
||||
}
|
||||
|
||||
$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) !== '';
|
||||
if (! $hasPdf && ! $hasStoredPayload) {
|
||||
return ['status' => 'skipped', 'message' => 'Nessun PDF/payload acqua disponibile'];
|
||||
}
|
||||
|
||||
try {
|
||||
if ($userId > 0) {
|
||||
app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($fattura, $userId);
|
||||
$fattura->refresh();
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
$documento = Documento::query()
|
||||
->where('documentable_type', FatturaElettronica::class)
|
||||
->where('documentable_id', (int) $fattura->id)
|
||||
->first();
|
||||
|
||||
$parsed = [];
|
||||
if ($documento instanceof Documento) {
|
||||
$text = is_string($documento->contenuto_ocr) ? trim((string) $documento->contenuto_ocr) : '';
|
||||
if ($text === '') {
|
||||
try {
|
||||
$result = app(PdfTextExtractionService::class)->extractAndStore($documento, false);
|
||||
if (($result['status'] ?? null) === 'ok') {
|
||||
$documento->refresh();
|
||||
$text = is_string($documento->contenuto_ocr) ? trim((string) $documento->contenuto_ocr) : '';
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
$text = '';
|
||||
}
|
||||
}
|
||||
|
||||
if ($text !== '') {
|
||||
$parsed = app(AcquaPdfTextParser::class)->parse($text);
|
||||
}
|
||||
}
|
||||
|
||||
$storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : '';
|
||||
if ($storedRaw !== '' && $parsed === []) {
|
||||
$decoded = json_decode($storedRaw, true);
|
||||
if (is_array($decoded)) {
|
||||
$parsed = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [];
|
||||
$contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [];
|
||||
$consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : [];
|
||||
$generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [];
|
||||
$finestra = is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : [];
|
||||
$letture = is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : [];
|
||||
$quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [];
|
||||
$tariffe = is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : [];
|
||||
$ivaInfo = is_array($parsed['iva'] ?? null) ? $parsed['iva'] : [];
|
||||
|
||||
$hasAny = false;
|
||||
foreach ([$codici['utenza'] ?? null, $codici['cliente'] ?? null, $codici['contratto'] ?? null, $contatore['matricola'] ?? null] as $value) {
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
$hasAny = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$hasTariffData = count(array_filter([
|
||||
$generale['periodicita_fatturazione'] ?? null,
|
||||
$tariffe['profilo'] ?? null,
|
||||
$ivaInfo['codice'] ?? null,
|
||||
$quadro['quota_fissa'] ?? null,
|
||||
], static fn($value): bool => $value !== null && $value !== '')) > 0;
|
||||
|
||||
if (! $hasAny && count($consumi) < 1 && ! $hasTariffData) {
|
||||
return ['status' => 'skipped', 'message' => 'Nessun dato acqua utile rilevato'];
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'type' => 'acqua',
|
||||
'source' => 'pdf_ocr',
|
||||
'codici' => [
|
||||
'utenza' => $codici['utenza'] ?? null,
|
||||
'cliente' => $codici['cliente'] ?? null,
|
||||
'contratto' => $codici['contratto'] ?? null,
|
||||
],
|
||||
'contatore' => [
|
||||
'matricola' => $contatore['matricola'] ?? null,
|
||||
],
|
||||
'consumi' => array_values($consumi),
|
||||
'generale' => $generale,
|
||||
'finestra_autolettura' => $finestra,
|
||||
'riepilogo_letture' => array_values($letture),
|
||||
'quadro_dettaglio' => $quadro,
|
||||
'tariffe' => $tariffe,
|
||||
'iva' => [
|
||||
'codice' => $ivaInfo['codice'] ?? null,
|
||||
'aliquota_percentuale' => $ivaInfo['aliquota_percentuale'] ?? null,
|
||||
'descrizione' => $ivaInfo['descrizione'] ?? null,
|
||||
],
|
||||
'parsed_at' => now()->toISOString(),
|
||||
];
|
||||
|
||||
$totalMc = null;
|
||||
$reference = null;
|
||||
if ($consumi !== []) {
|
||||
$sum = 0.0;
|
||||
$hasNumeric = false;
|
||||
foreach ($consumi as $consumo) {
|
||||
if (isset($consumo['valore']) && is_numeric($consumo['valore'])) {
|
||||
$sum += (float) $consumo['valore'];
|
||||
$hasNumeric = true;
|
||||
}
|
||||
}
|
||||
if ($hasNumeric) {
|
||||
$totalMc = $sum;
|
||||
}
|
||||
|
||||
$first = $consumi[0] ?? [];
|
||||
$dal = $first['dal'] ?? null;
|
||||
$al = $first['al'] ?? null;
|
||||
if (is_string($dal) && is_string($al) && $dal !== '' && $al !== '') {
|
||||
$reference = $dal . ' - ' . $al;
|
||||
}
|
||||
}
|
||||
|
||||
$fattura->consumo_unita = 'mc';
|
||||
$fattura->consumo_valore = $totalMc;
|
||||
$fattura->consumo_riferimento = $reference;
|
||||
$fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$fattura->save();
|
||||
|
||||
try {
|
||||
app(ConsumiAcquaTariffeIngestionService::class)->ingest($fattura, $parsed, null);
|
||||
} catch (\Throwable) {
|
||||
// best-effort
|
||||
}
|
||||
|
||||
$ingestion = app(ConsumiAcquaIngestionService::class)->ingest($fattura, $parsed, null, $userId, false, null);
|
||||
if (($ingestion['status'] ?? null) === 'ok') {
|
||||
try {
|
||||
app(ConsumiAcquaTariffeIngestionService::class)->ingest(
|
||||
$fattura,
|
||||
$parsed,
|
||||
isset($ingestion['servizio_id']) && is_numeric($ingestion['servizio_id']) ? (int) $ingestion['servizio_id'] : null
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
// best-effort
|
||||
}
|
||||
}
|
||||
|
||||
return $ingestion;
|
||||
}
|
||||
|
||||
public int $cassettoAnno;
|
||||
|
||||
/**
|
||||
|
|
@ -1006,6 +1174,8 @@ protected function getHeaderActions(): array
|
|||
$imported = 0;
|
||||
$duplicates = 0;
|
||||
$errors = 0;
|
||||
$waterCreated = 0;
|
||||
$waterUpdated = 0;
|
||||
|
||||
$duplicateDetails = [];
|
||||
$errorDetails = [];
|
||||
|
|
@ -1125,6 +1295,14 @@ protected function getHeaderActions(): array
|
|||
// ignore
|
||||
}
|
||||
|
||||
if (in_array((string) ($result['status'] ?? ''), ['imported', 'duplicate'], true) && is_numeric($result['id'] ?? null)) {
|
||||
$waterResult = $this->autoLinkWaterReadingsForInvoice((int) $result['id'], (int) $user->id);
|
||||
if (($waterResult['status'] ?? null) === 'ok') {
|
||||
$waterCreated += (int) ($waterResult['created_letture'] ?? 0);
|
||||
$waterUpdated += (int) ($waterResult['updated_letture'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (($result['status'] ?? null) === 'imported') {
|
||||
$imported++;
|
||||
} elseif (($result['status'] ?? null) === 'duplicate') {
|
||||
|
|
@ -1228,6 +1406,14 @@ protected function getHeaderActions(): array
|
|||
// ignore
|
||||
}
|
||||
|
||||
if (in_array((string) ($result['status'] ?? ''), ['imported', 'duplicate'], true) && is_numeric($result['id'] ?? null)) {
|
||||
$waterResult = $this->autoLinkWaterReadingsForInvoice((int) $result['id'], (int) $user->id);
|
||||
if (($waterResult['status'] ?? null) === 'ok') {
|
||||
$waterCreated += (int) ($waterResult['created_letture'] ?? 0);
|
||||
$waterUpdated += (int) ($waterResult['updated_letture'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (($result['status'] ?? null) === 'imported') {
|
||||
$imported++;
|
||||
} elseif (($result['status'] ?? null) === 'duplicate') {
|
||||
|
|
@ -1314,7 +1500,7 @@ protected function getHeaderActions(): array
|
|||
|
||||
Notification::make()
|
||||
->title('Import completato')
|
||||
->body($this->safeUtf8("Importate: {$imported} · Duplicate: {$duplicates} · Errori: {$errors}" . $logNote))
|
||||
->body($this->safeUtf8("Importate: {$imported} · Duplicate: {$duplicates} · Errori: {$errors} · Letture acqua create: {$waterCreated} · aggiornate: {$waterUpdated}" . $logNote))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use App\Models\Stabile;
|
||||
use App\Models\User;
|
||||
use App\Support\ArchivioPaths;
|
||||
use App\Support\GoogleAccountStore;
|
||||
use BackedEnum;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Pages\Page;
|
||||
|
|
@ -130,12 +131,18 @@ public function importTecnoRepairFromArchive(bool $dryRun = false): void
|
|||
return;
|
||||
}
|
||||
|
||||
$mdbPath = trim($this->tecnorepairMdbPath);
|
||||
if ($mdbPath === '' || ! is_file($mdbPath)) {
|
||||
Notification::make()->title('Percorso MDB non valido')->warning()->send();
|
||||
$mdbPath = $this->resolveTecnoRepairMdbPath($this->tecnorepairMdbPath);
|
||||
if ($mdbPath === null) {
|
||||
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;
|
||||
}
|
||||
|
||||
$this->tecnorepairMdbPath = $mdbPath;
|
||||
|
||||
$adminId = (int) ($this->fornitore->amministratore_id ?? 0);
|
||||
if ($adminId <= 0) {
|
||||
Notification::make()->title('Amministratore del fornitore non trovato')->danger()->send();
|
||||
|
|
@ -390,7 +397,7 @@ private function refreshArchiveSummary(): void
|
|||
$this->loadOperationalWorkflowSettings();
|
||||
$this->moduleRows = [
|
||||
['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' => '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.'],
|
||||
|
|
@ -545,16 +552,73 @@ private function resolveGoogleWorkspaceStatus(): ?array
|
|||
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 [
|
||||
'connected' => (bool) ($oauth['connected'] ?? false),
|
||||
'email' => (string) ($oauth['email'] ?? ''),
|
||||
'name' => (string) ($oauth['name'] ?? ''),
|
||||
'connected_at' => (string) ($oauth['connected_at'] ?? ''),
|
||||
'connected' => (bool) ($account['connected'] ?? false),
|
||||
'email' => (string) ($account['email'] ?? ''),
|
||||
'name' => (string) ($account['name'] ?? ''),
|
||||
'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
|
||||
{
|
||||
if (! $this->fornitore instanceof Fornitore) {
|
||||
|
|
|
|||
|
|
@ -753,11 +753,19 @@ private function splitTags(string $value): array
|
|||
private function canonicalizeTag(string $raw): ?string
|
||||
{
|
||||
$clean = trim(mb_strtolower($raw));
|
||||
if ($clean === 'pc') {
|
||||
return 'pc';
|
||||
}
|
||||
|
||||
if ($clean === '' || mb_strlen($clean) < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$map = [
|
||||
'informat' => 'informatica',
|
||||
'assist' => 'assistenza',
|
||||
'computer' => 'pc',
|
||||
'apple' => 'apple',
|
||||
'idr' => 'idraulico',
|
||||
'idraul' => 'idraulico',
|
||||
'elett' => 'elettricista',
|
||||
|
|
@ -782,6 +790,7 @@ private function hydrateBoxData(User $user): void
|
|||
{
|
||||
$this->fattureAssociate = [];
|
||||
$this->raVersateRows = [];
|
||||
$identityCandidates = $this->getSupplierIdentityCandidates();
|
||||
|
||||
$this->box = [
|
||||
'stabile_id' => null,
|
||||
|
|
@ -967,17 +976,8 @@ private function hydrateBoxData(User $user): void
|
|||
if (Schema::hasTable('fatture_elettroniche')) {
|
||||
$feBase = FatturaElettronica::query()
|
||||
->where('stabile_id', $activeStabileId)
|
||||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($ids): void {
|
||||
$q->where('fornitore_id', $this->fornitore->id);
|
||||
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($identityCandidates): void {
|
||||
$this->applySupplierIdentityFilterToFeQuery($q, $identityCandidates);
|
||||
});
|
||||
|
||||
$this->box['fe']['count'] = (int) (clone $feBase)->count();
|
||||
|
|
@ -1008,7 +1008,16 @@ private function hydrateBoxData(User $user): void
|
|||
if (Schema::hasTable('contabilita_fatture_fornitori')) {
|
||||
$contabBase = ContabilitaFatturaFornitore::query()
|
||||
->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.
|
||||
$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
|
||||
{
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ class PostItGestione extends Page
|
|||
|
||||
public string $selectedGroupModalMode = 'open';
|
||||
|
||||
public string $appuntiOwnerFilter = 'mine';
|
||||
|
||||
public ?int $selectedAppuntoId = null;
|
||||
|
||||
/** @var array<int,string> */
|
||||
public array $riaperturaNote = [];
|
||||
|
||||
|
|
@ -88,11 +92,16 @@ public function mount(): void
|
|||
$focus = (int) request()->query('focus_post_it', 0);
|
||||
$this->focusPostItId = $focus > 0 ? $focus : null;
|
||||
|
||||
if (in_array((string) request()->query('tab'), ['storico', 'lavagna', 'archivio'], true)) {
|
||||
if (in_array((string) request()->query('tab'), ['storico', 'appunti', 'lavagna', 'archivio'], true)) {
|
||||
$this->activeTab = (string) request()->query('tab');
|
||||
}
|
||||
}
|
||||
|
||||
public function selectAppunto(int $postItId): void
|
||||
{
|
||||
$this->selectedAppuntoId = $postItId > 0 ? $postItId : null;
|
||||
}
|
||||
|
||||
public function getPostItInserimentoUrl(): string
|
||||
{
|
||||
return PostIt::getUrl(panel: 'admin-filament');
|
||||
|
|
@ -274,6 +283,62 @@ private function shouldShowPostIt(ChiamataPostIt $postIt): bool
|
|||
return ! $this->isLegacyFilteredSmdrPostIt($postIt);
|
||||
}
|
||||
|
||||
private function hasOperationalNoteContent(ChiamataPostIt $postIt): bool
|
||||
{
|
||||
return trim($this->extractOperationalNoteContent($postIt)) !== '';
|
||||
}
|
||||
|
||||
private function extractOperationalNoteContent(ChiamataPostIt $postIt): string
|
||||
{
|
||||
$note = trim((string) ($postIt->nota ?? ''));
|
||||
if ($note === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$origin = strtolower(trim((string) ($postIt->origine ?? '')));
|
||||
if (! in_array($origin, ['smdr', 'smdr_table', 'smdr_live_banner', 'topbar_live_call', 'panasonic_csta_table', 'csta_table'], true)) {
|
||||
return $note;
|
||||
}
|
||||
|
||||
$lines = preg_split('/\r\n|\r|\n/', $note) ?: [];
|
||||
$kept = [];
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim((string) $line);
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^\d{2}\/\d{2}\/\d{2}\s+\d{2}:\d{2}\s+\d+\b/i', $line) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^chiamata ricevuta dal numero\b/i', $line) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^interno\/gruppo chiamato:\b/i', $line) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^nota operativa:\s*\d{2}\/\d{2}\/\d{2}/i', $line) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^dettaglio sorgente:\s*\d{2}\/\d{2}\/\d{2}/i', $line) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^contatto:\s*contatto non in rubrica$/i', $line) === 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$kept[] = $line;
|
||||
}
|
||||
|
||||
return trim(implode("\n", $kept));
|
||||
}
|
||||
|
||||
public function getRecentiApertiVisibiliProperty()
|
||||
{
|
||||
return $this->recentiVisibili
|
||||
|
|
@ -298,6 +363,60 @@ public function getRecentiChiusiRaggruppatiProperty()
|
|||
return $this->buildGroupedCollection($this->recentiChiusiVisibili);
|
||||
}
|
||||
|
||||
public function getAppuntiOperativiProperty()
|
||||
{
|
||||
if (! $this->isPostItTableReady()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
try {
|
||||
$query = ChiamataPostIt::query()
|
||||
->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
||||
->orderByDesc('chiamata_il')
|
||||
->orderByDesc('id');
|
||||
|
||||
$authId = (int) (Auth::id() ?? 0);
|
||||
if ($this->appuntiOwnerFilter === 'mine' && $authId > 0) {
|
||||
$query->where(function ($inner) use ($authId): void {
|
||||
$inner->where('creato_da_user_id', $authId)
|
||||
->orWhere('assegnato_a_user_id', $authId);
|
||||
});
|
||||
}
|
||||
|
||||
$items = $query
|
||||
->limit(200)
|
||||
->get()
|
||||
->filter(fn(ChiamataPostIt $postIt): bool => $this->shouldShowPostIt($postIt) && $this->hasOperationalNoteContent($postIt))
|
||||
->values();
|
||||
|
||||
if ($items->isNotEmpty() && ! $items->contains(fn(ChiamataPostIt $postIt): bool => (int) $postIt->id === (int) $this->selectedAppuntoId)) {
|
||||
$this->selectedAppuntoId = $this->focusPostItId && $items->contains(fn(ChiamataPostIt $postIt): bool => (int) $postIt->id === (int) $this->focusPostItId)
|
||||
? (int) $this->focusPostItId
|
||||
: (int) $items->first()->id;
|
||||
}
|
||||
|
||||
return $items;
|
||||
} catch (QueryException) {
|
||||
return collect();
|
||||
}
|
||||
}
|
||||
|
||||
public function getSelectedAppuntoProperty(): ?ChiamataPostIt
|
||||
{
|
||||
if (! $this->isPostItTableReady() || ! $this->selectedAppuntoId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ChiamataPostIt::query()
|
||||
->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
||||
->find($this->selectedAppuntoId);
|
||||
}
|
||||
|
||||
public function getOperationalNoteSummary(ChiamataPostIt $postIt, int $limit = 180): string
|
||||
{
|
||||
return Str::limit($this->extractOperationalNoteContent($postIt), $limit);
|
||||
}
|
||||
|
||||
public function openGroupModal(string $modalKey, string $mode = 'open'): void
|
||||
{
|
||||
$this->selectedGroupModalKey = $modalKey;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
namespace App\Filament\Pages;
|
||||
|
||||
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
|
||||
use App\Models\DettaglioRipartizioneSpese;
|
||||
use App\Models\GestioneContabile;
|
||||
use App\Models\Incasso;
|
||||
|
|
@ -1690,10 +1691,12 @@ protected function hydrateRelazioni(): void
|
|||
$nome = $contatto ? 'Contatto #' . $contatto->id : 'Contatto non definito';
|
||||
}
|
||||
|
||||
$tipoRaw = strtolower(trim((string) ($relazione->ruolo_standard ?? '')));
|
||||
$tipoLabel = $labelsRuoli[$tipoRaw] ?? ($tipoRaw !== '' ? $tipoRaw : 'Altro');
|
||||
if (! empty($relazione->ruolo_custom)) {
|
||||
$tipoLabel .= ' (' . (string) $relazione->ruolo_custom . ')';
|
||||
$tipoRaw = strtolower(trim((string) ($relazione->ruolo_standard ?? '')));
|
||||
$customRaw = trim((string) ($relazione->ruolo_custom ?? ''));
|
||||
$hasMeaningfulCustomRole = $customRaw !== '' && ! in_array(strtolower($customRaw), ['p', 'i', 'c', 'u'], true);
|
||||
$tipoLabel = $labelsRuoli[$tipoRaw] ?? ($tipoRaw !== '' ? $tipoRaw : 'Altro');
|
||||
if ($hasMeaningfulCustomRole) {
|
||||
$tipoLabel .= ' (' . $customRaw . ')';
|
||||
}
|
||||
|
||||
$quota = data_get($relazione->meta, 'right_percent');
|
||||
|
|
@ -1716,16 +1719,22 @@ protected function hydrateRelazioni(): void
|
|||
return [
|
||||
'id' => (int) $relazione->id,
|
||||
'persona_id' => (int) ($relazione->rubrica_id ?? 0),
|
||||
'rubrica_id' => (int) ($relazione->rubrica_id ?? 0),
|
||||
'nome' => $nome,
|
||||
'codice_fiscale' => $contatto?->codice_fiscale,
|
||||
'tipo_raw' => $tipoRaw,
|
||||
'tipo' => $tipoLabel,
|
||||
'ruolo_custom' => $hasMeaningfulCustomRole ? $customRaw : null,
|
||||
'has_custom_role' => $hasMeaningfulCustomRole,
|
||||
'ruolo_rate' => $ruoloRate,
|
||||
'quota' => $quota,
|
||||
'quota_label' => $quota !== null ? number_format($quota, 2, ',', '.') : null,
|
||||
'data_inizio' => $formatDate($relazione->data_inizio),
|
||||
'data_fine' => $formatDate($relazione->data_fine),
|
||||
'attivo' => true,
|
||||
'telefono' => trim((string) ($contatto?->telefono_cellulare ?: $contatto?->telefono_ufficio ?: $contatto?->telefono_casa ?: '')),
|
||||
'email' => trim((string) ($contatto?->email ?? '')),
|
||||
'rubrica_url' => $contatto ? RubricaUniversaleScheda::getUrl(['record' => (int) $contatto->id], panel : 'admin-filament'): null,
|
||||
'riceve_comunicazioni' => false,
|
||||
'riceve_convocazioni' => false,
|
||||
'vota_assemblea' => false,
|
||||
|
|
@ -1771,19 +1780,22 @@ protected function hydrateRelazioni(): void
|
|||
|
||||
$altri = $relazioniMapped
|
||||
->filter(function ($rel) {
|
||||
return $rel['attivo'] && ! in_array(strtolower($rel['tipo_raw'] ?? ''), [
|
||||
'condomino',
|
||||
'proprietario',
|
||||
'comproprietario',
|
||||
'nudo_proprietario',
|
||||
'usufruttuario',
|
||||
'usufrutto',
|
||||
'inquilino',
|
||||
'locatario',
|
||||
'conduttore',
|
||||
], true);
|
||||
return $rel['attivo'] && (
|
||||
! in_array(strtolower($rel['tipo_raw'] ?? ''), [
|
||||
'condomino',
|
||||
'proprietario',
|
||||
'comproprietario',
|
||||
'nudo_proprietario',
|
||||
'usufruttuario',
|
||||
'usufrutto',
|
||||
'inquilino',
|
||||
'locatario',
|
||||
'conduttore',
|
||||
], true)
|
||||
|| ! empty($rel['has_custom_role'])
|
||||
);
|
||||
})
|
||||
->unique(fn(array $r) => (strtolower((string) ($rel['tipo_raw'] ?? 'altro')) . '|' . (int) ($r['persona_id'] ?? 0)))
|
||||
->unique(fn(array $r) => strtolower((string) ($r['tipo_raw'] ?? 'altro')) . '|' . strtolower((string) ($r['ruolo_custom'] ?? '')) . '|' . (int) ($r['persona_id'] ?? 0))
|
||||
->values();
|
||||
|
||||
if ($proprietari->isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class Fornitore extends Model
|
|||
|
||||
protected $fillable = [
|
||||
'amministratore_id',
|
||||
'cod_forn',
|
||||
'rubrica_id',
|
||||
'codice_univoco',
|
||||
'ragione_sociale',
|
||||
|
|
@ -57,6 +58,7 @@ class Fornitore extends Model
|
|||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'cod_forn' => 'integer',
|
||||
'escludi_righe_fe' => 'boolean',
|
||||
'fe_features' => 'array',
|
||||
'operational_config' => 'array',
|
||||
|
|
|
|||
|
|
@ -1453,7 +1453,9 @@ private function updateExisting(FatturaElettronica $existing, string $xml, ?stri
|
|||
$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);
|
||||
|
||||
return DB::transaction(function () use ($existing, $data, $xml, $originalFilename, $extra, $stabileId, $fornitoreId, $fornitorePiva, $importRighe) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('fornitori')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('fornitori', function (Blueprint $table): void {
|
||||
if (! Schema::hasColumn('fornitori', 'cod_forn')) {
|
||||
$table->unsignedBigInteger('cod_forn')->nullable()->after('old_id');
|
||||
}
|
||||
});
|
||||
|
||||
DB::table('fornitori')
|
||||
->whereNull('cod_forn')
|
||||
->whereNotNull('old_id')
|
||||
->update(['cod_forn' => DB::raw('old_id')]);
|
||||
|
||||
$indexes = collect(DB::select("SHOW INDEX FROM fornitori"))
|
||||
->pluck('Key_name');
|
||||
|
||||
Schema::table('fornitori', function (Blueprint $table) use ($indexes): void {
|
||||
if (! $indexes->contains('fornitori_amministratore_cod_forn_index')) {
|
||||
$table->index(['amministratore_id', 'cod_forn'], 'fornitori_amministratore_cod_forn_index');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('fornitori')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$indexes = collect(DB::select("SHOW INDEX FROM fornitori"))
|
||||
->pluck('Key_name');
|
||||
|
||||
Schema::table('fornitori', function (Blueprint $table) use ($indexes): void {
|
||||
if ($indexes->contains('fornitori_amministratore_cod_forn_index')) {
|
||||
$table->dropIndex('fornitori_amministratore_cod_forn_index');
|
||||
}
|
||||
|
||||
if (Schema::hasColumn('fornitori', 'cod_forn')) {
|
||||
$table->dropColumn('cod_forn');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -10,6 +10,8 @@ ## Stato operativo reale alla chiusura sessione
|
|||
- rimuovere il footer Filament invasivo su mobile
|
||||
- riallineare pagina `tabelle-millesimali` a `nord`, `tipo`, `calcolo`
|
||||
- eliminare i blocchi Blade fragili nelle viste `ticket-acqua` e `ticket-acqua-generale`
|
||||
- compattare la UI di `letture-servizi` con pulsanti, tooltip, miniatura e modal foto
|
||||
- agganciare automaticamente il parsing acqua alle FE ricevute importate quando esiste PDF/payload utilizzabile
|
||||
|
||||
## Verifiche fatte e confermate
|
||||
|
||||
|
|
@ -21,9 +23,28 @@ ## Verifiche fatte e confermate
|
|||
- Il problema operativo dello scarico FE nasceva dal contesto dello stabile attivo in topbar/sessione, non dalla chiamata HTTP finale.
|
||||
- Nella pagina FE è stato aggiunto un riepilogo esplicito del `Codice fiscale soggetto usato per lo scarico`.
|
||||
- Le due Blade acqua sono state semplificate spostando il rendering metadati foto in partial condivisa, per evitare parse error residui in staging dovuti a blocchi Blade annidati.
|
||||
- La pagina `fatture-ricevute` continua a filtrare per `stabile_id` attivo; nel DB locale per `stabile_id=25` esistono FE 2025 ricevute.
|
||||
- Per `stabile_id=25` esistono FE 2025 con `allegato_pdf_path` e in diversi casi `consumo_raw`, quindi il nuovo aggancio acqua post-import ha input reali da elaborare.
|
||||
- `php artisan view:clear && php artisan view:cache` continua a passare dopo i ritocchi UI e il nuovo post-processing FE.
|
||||
- `php artisan route:list` continua a caricare `755` route.
|
||||
|
||||
## File toccati in questa fase
|
||||
|
||||
- `app/Filament/Pages/Condomini/LettureServiziArchivio.php`
|
||||
- colonna miniatura foto
|
||||
- testi piu compatti (`text-[11px]`)
|
||||
- tooltip al posto di testo lungo per servizio/attribuzione/check/voce/fornitore
|
||||
- azioni riga rese pulsanti
|
||||
- modal foto invece dell'apertura diretta del file
|
||||
- pulsante FE dedicato in riga
|
||||
- `resources/views/filament/pages/condomini/partials/lettura-servizio-foto-modal.blade.php`
|
||||
- nuova vista modal per anteprima foto contatore
|
||||
- `app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php`
|
||||
- nuovo post-processing `autoLinkWaterReadingsForInvoice()` dopo import FE
|
||||
- sincronizzazione `consumo_raw`/consumi FE
|
||||
- chiamata a `ConsumiAcquaIngestionService` e `ConsumiAcquaTariffeIngestionService` in best-effort
|
||||
- notifica finale estesa con conteggio letture acqua create/aggiornate
|
||||
|
||||
- `app/Providers/Filament/AdminFilamentPanelProvider.php`
|
||||
- chain Filament riparata
|
||||
- footer hook rimosso dal panel
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
@php
|
||||
$photos = is_array($photos ?? null) ? $photos : [];
|
||||
$primary = $photos[0] ?? null;
|
||||
@endphp
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">{{ $assigned ?? 'Contatore' }}</div>
|
||||
<div class="text-xs text-slate-500">
|
||||
Riga #{{ (int) ($record->id ?? 0) }}
|
||||
@if(! empty($record->periodo_dal) || ! empty($record->periodo_al))
|
||||
· Periodo {{ optional($record->periodo_dal)->format('d/m/Y') ?: '—' }} → {{ optional($record->periodo_al)->format('d/m/Y') ?: '—' }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@if(is_string($mapUrl ?? null) && $mapUrl !== '')
|
||||
<a href="{{ $mapUrl }}" target="_blank" rel="noreferrer" class="inline-flex items-center rounded-md bg-slate-800 px-3 py-2 text-xs font-medium text-white hover:bg-slate-700">
|
||||
Apri mappa
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($primary)
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200 bg-slate-50">
|
||||
<img src="{{ $primary['url'] }}" alt="{{ $primary['name'] }}" class="max-h-[60vh] w-full object-contain bg-white" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if(count($photos) > 1)
|
||||
<div class="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
@foreach($photos as $photo)
|
||||
<a href="{{ $photo['url'] }}" target="_blank" rel="noreferrer" class="overflow-hidden rounded-lg border border-slate-200 bg-white transition hover:border-slate-400">
|
||||
<img src="{{ $photo['url'] }}" alt="{{ $photo['name'] }}" class="h-28 w-full object-cover" />
|
||||
<div class="border-t border-slate-100 px-2 py-2 text-[11px] text-slate-600">{{ $photo['name'] }}</div>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if($photos === [])
|
||||
<div class="rounded-lg border border-dashed border-slate-300 bg-slate-50 px-4 py-6 text-sm text-slate-500">
|
||||
Nessuna foto disponibile per questa lettura.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
|
||||
<div class="rounded-xl border bg-white p-4">
|
||||
<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">
|
||||
<label class="block text-sm md:col-span-2">
|
||||
|
|
@ -248,7 +248,7 @@
|
|||
</form>
|
||||
@endif
|
||||
</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
|
||||
<div class="mt-3 text-sm text-gray-500">Contesto amministratore Google non disponibile per questo fornitore.</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@
|
|||
<button type="button" wire:click="$set('activeTab', 'storico')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'storico' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">
|
||||
Storico Post-it
|
||||
</button>
|
||||
<button type="button" wire:click="$set('activeTab', 'appunti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'appunti' ? 'bg-emerald-700 text-white' : 'bg-emerald-50 text-emerald-900 hover:bg-emerald-100' }}">
|
||||
Appunti chiamata
|
||||
</button>
|
||||
<button type="button" wire:click="$set('activeTab', 'lavagna')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'lavagna' ? 'bg-amber-600 text-white' : 'bg-amber-50 text-amber-900 hover:bg-amber-100' }}">
|
||||
Lavagna chiamate
|
||||
</button>
|
||||
|
|
@ -111,6 +114,111 @@
|
|||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@elseif($activeTab === 'appunti')
|
||||
<div class="rounded-xl border border-emerald-200 bg-white p-4">
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-base font-semibold text-emerald-900">Appunti presi durante la chiamata</h2>
|
||||
<p class="text-sm text-emerald-800">Elenco separato dagli eventi telefonici grezzi, con focus sui Post-it che contengono note operative reali.</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button type="button" wire:click="$set('appuntiOwnerFilter', 'mine')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $appuntiOwnerFilter === 'mine' ? 'bg-emerald-700 text-white' : 'bg-emerald-50 text-emerald-900 hover:bg-emerald-100' }}">
|
||||
I miei appunti
|
||||
</button>
|
||||
<button type="button" wire:click="$set('appuntiOwnerFilter', 'all')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $appuntiOwnerFilter === 'all' ? 'bg-slate-700 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
|
||||
Tutti
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
|
||||
<div class="space-y-3">
|
||||
@forelse($this->appuntiOperativi as $appunto)
|
||||
<button type="button" wire:click="selectAppunto({{ (int) $appunto->id }})" class="block w-full rounded-xl border p-4 text-left transition {{ (int) ($selectedAppuntoId ?? 0) === (int) $appunto->id ? 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200' : 'bg-white hover:bg-gray-50' }}">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">{{ $appunto->oggetto ?: 'Senza oggetto' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-600">
|
||||
{{ $appunto->nome_chiamante ?: 'Chiamante non identificato' }}
|
||||
@if($appunto->telefono)
|
||||
· {{ $appunto->telefono }}
|
||||
@endif
|
||||
@if($appunto->rubrica)
|
||||
· Rubrica #{{ (int) $appunto->rubrica->id }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-[11px] text-slate-500">{{ optional($appunto->chiamata_il)->format('d/m/Y H:i') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 whitespace-pre-wrap text-sm text-slate-700">{{ $this->getOperationalNoteSummary($appunto) }}</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2 text-[11px]">
|
||||
<span class="rounded-md bg-slate-100 px-2 py-1 text-slate-700">{{ $appunto->stato }}</span>
|
||||
@if($appunto->creatoDa)
|
||||
<span class="rounded-md bg-blue-100 px-2 py-1 text-blue-800">Creato da {{ $appunto->creatoDa->name }}</span>
|
||||
@endif
|
||||
@if($appunto->assegnatoAUser)
|
||||
<span class="rounded-md bg-emerald-100 px-2 py-1 text-emerald-800">Assegnato a {{ $appunto->assegnatoAUser->name }}</span>
|
||||
@endif
|
||||
@if($appunto->ticket_id)
|
||||
<span class="rounded-md bg-indigo-100 px-2 py-1 text-indigo-800">Ticket #{{ (int) $appunto->ticket_id }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</button>
|
||||
@empty
|
||||
<div class="rounded-lg border border-dashed border-emerald-300 bg-emerald-50/50 p-4 text-sm text-emerald-900">Nessun appunto operativo trovato per il filtro corrente.</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border bg-slate-50 p-4">
|
||||
@if($this->selectedAppunto)
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-slate-900">Post-it #{{ (int) $this->selectedAppunto->id }} · {{ $this->selectedAppunto->oggetto ?: 'Senza oggetto' }}</div>
|
||||
<div class="mt-1 text-xs text-slate-500">
|
||||
{{ $this->selectedAppunto->nome_chiamante ?: 'Chiamante non identificato' }}
|
||||
@if($this->selectedAppunto->telefono)
|
||||
· {{ $this->selectedAppunto->telefono }}
|
||||
@endif
|
||||
@if($this->selectedAppunto->stabile)
|
||||
· {{ $this->selectedAppunto->stabile->denominazione ?: ('Stabile #' . $this->selectedAppunto->stabile->id) }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border bg-white p-3">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-slate-500">Appunto completo</div>
|
||||
<div class="mt-2 whitespace-pre-wrap text-sm text-slate-800">{{ $this->selectedAppunto->nota ?: '—' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
<div class="rounded-xl border bg-white p-3 text-xs text-slate-600">
|
||||
<div class="font-semibold text-slate-900">Creato da</div>
|
||||
<div class="mt-1">{{ $this->selectedAppunto->creatoDa?->name ?: '—' }}</div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-3 text-xs text-slate-600">
|
||||
<div class="font-semibold text-slate-900">Assegnazione</div>
|
||||
<div class="mt-1">{{ $this->selectedAppunto->assegnatoAUser?->name ?: ($this->selectedAppunto->assegnatoAFornitore?->ragione_sociale ?: 'Non assegnato') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ $this->getPostItPageUrl((int) $this->selectedAppunto->id) }}" 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">Apri scheda Post-it</a>
|
||||
@if($this->selectedAppunto->rubrica)
|
||||
<a href="{{ \App\Filament\Pages\Gescon\RubricaUniversaleScheda::getUrl(['record' => (int) $this->selectedAppunto->rubrica->id], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md border px-3 py-1.5 text-xs text-indigo-700">Apri rubrica</a>
|
||||
@endif
|
||||
@if($this->selectedAppunto->ticket_id)
|
||||
<a href="{{ \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament') }}#ticket-{{ (int) $this->selectedAppunto->ticket_id }}" class="inline-flex items-center rounded-md border px-3 py-1.5 text-xs text-slate-700">Apri ticket</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="rounded-lg border border-dashed border-slate-300 bg-white p-4 text-sm text-slate-500">Seleziona un appunto per vedere la scheda completa.</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@elseif($activeTab === 'lavagna')
|
||||
<div class="rounded-xl border border-amber-200 bg-amber-50/40 p-4">
|
||||
<div class="mb-4">
|
||||
|
|
|
|||
|
|
@ -473,6 +473,24 @@ class="inline-flex items-center rounded-lg border px-4 py-2 text-sm font-semibol
|
|||
<div class="text-sm font-semibold text-gray-900">{{ $relazione['nome'] }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">CF: {{ $relazione['codice_fiscale'] ?? '—' }}</div>
|
||||
<div class="text-xs text-gray-500 mt-1">Quota: {{ $relazione['quota_label'] ?? '—' }}%</div>
|
||||
@if(!empty($relazione['telefono']))
|
||||
<div class="text-xs text-gray-500 mt-1">Telefono: {{ $relazione['telefono'] }}</div>
|
||||
@endif
|
||||
@if(!empty($relazione['email']))
|
||||
<div class="text-xs text-gray-500 mt-1">Email: {{ $relazione['email'] }}</div>
|
||||
@endif
|
||||
@if(!empty($relazione['data_inizio']) || !empty($relazione['data_fine']))
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
Periodo:
|
||||
@if(!empty($relazione['data_inizio'])) Da {{ $relazione['data_inizio'] }} @endif
|
||||
@if(!empty($relazione['data_fine'])) · fino a {{ $relazione['data_fine'] }} @endif
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty($relazione['rubrica_url']))
|
||||
<div class="mt-2">
|
||||
<a href="{{ $relazione['rubrica_url'] }}" class="inline-flex items-center rounded-md border border-indigo-200 bg-indigo-50 px-2 py-1 text-[11px] font-medium text-indigo-700 hover:bg-indigo-100">Apri scheda rubrica</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user