1121 lines
46 KiB
PHP
1121 lines
46 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Fornitore;
|
|
|
|
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
|
|
use App\Models\Fornitore;
|
|
use App\Models\FornitoreDipendente;
|
|
use App\Models\Product;
|
|
use App\Models\ProductIdentifier;
|
|
use App\Models\ProductOffer;
|
|
use App\Models\Ticket;
|
|
use App\Models\TicketAttachment;
|
|
use App\Models\TicketIntervento;
|
|
use App\Models\TicketInterventoSessione;
|
|
use App\Models\User;
|
|
use BackedEnum;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Livewire\WithFileUploads;
|
|
use UnitEnum;
|
|
|
|
class TicketInterventoScheda extends Page
|
|
{
|
|
use ResolvesOperatoreContext;
|
|
use WithFileUploads;
|
|
|
|
protected static ?string $title = 'Scheda intervento';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Fornitore';
|
|
|
|
protected static ?string $slug = 'fornitore/tickets/{record}';
|
|
|
|
protected static bool $shouldRegisterNavigation = false;
|
|
|
|
protected string $view = 'filament.pages.fornitore.ticket-intervento-scheda';
|
|
|
|
public TicketIntervento $intervento;
|
|
|
|
public Fornitore $fornitore;
|
|
|
|
public ?FornitoreDipendente $dipendente = null;
|
|
|
|
/** @var array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string} */
|
|
public array $caller = [
|
|
'contatto' => '-',
|
|
'telefono' => '',
|
|
'email' => '',
|
|
'problema' => '',
|
|
'sorgente' => '',
|
|
'riferimento' => '',
|
|
];
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $storicoRows = [];
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $dipendentiOptions = [];
|
|
|
|
public ?int $dipendenteId = null;
|
|
|
|
public string $rapportoFornitore = '';
|
|
|
|
public ?int $tempoMinuti = null;
|
|
|
|
/** @var array<int, string> */
|
|
public array $articoliUtilizzati = ['', '', ''];
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $materialiUtilizzati = [];
|
|
|
|
public string $materialSearch = '';
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $materialSuggestions = [];
|
|
|
|
public string $apparatoMarca = '';
|
|
|
|
public string $apparatoModello = '';
|
|
|
|
public string $apparatoSeriale = '';
|
|
|
|
/** @var array<int, mixed> */
|
|
public array $fotoLavoro = [];
|
|
|
|
/** @var array<int, mixed> */
|
|
public array $documentiLavoro = [];
|
|
|
|
/** @var array<int, string> */
|
|
public array $descrizioneFile = ['', '', '', '', '', ''];
|
|
|
|
public string $messaggioVeloce = '';
|
|
|
|
public bool $lavoroStandard = false;
|
|
|
|
public bool $richiestaProforma = false;
|
|
|
|
public string $proformaCodice = '';
|
|
|
|
public string $proformaNote = '';
|
|
|
|
public string $qrToken = '';
|
|
|
|
public ?array $attachmentPreview = null;
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $sessionRows = [];
|
|
|
|
/** @var array<string, mixed>|null */
|
|
public ?array $lastGeoSnapshot = null;
|
|
|
|
/** @var array<string, mixed>|null */
|
|
public ?array $googleWorkspaceStatus = null;
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
|
|
}
|
|
|
|
public function mount(int | string $record): void
|
|
{
|
|
$recordId = (int) $record;
|
|
abort_unless($recordId > 0, 404);
|
|
|
|
$user = Auth::user();
|
|
$isAdmin = $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore']);
|
|
|
|
if ($isAdmin) {
|
|
$intervento = $this->resolveInterventoRecord($recordId);
|
|
abort_unless($intervento instanceof TicketIntervento, 404);
|
|
|
|
[$fornitore, $dipendente] = $this->resolveOperatoreContext((int) $intervento->fornitore_id);
|
|
} else {
|
|
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
|
|
abort_unless($fornitore instanceof Fornitore, 403, 'Profilo fornitore non collegato.');
|
|
|
|
$intervento = $this->resolveInterventoRecord($recordId, $fornitore, $dipendente);
|
|
abort_unless($intervento instanceof TicketIntervento, 404, 'Ticket/intervento non disponibile per questo fornitore.');
|
|
}
|
|
|
|
$this->intervento = $intervento;
|
|
|
|
$this->fornitore = $fornitore;
|
|
$this->dipendente = $dipendente;
|
|
|
|
$this->authorizeIntervento($this->intervento, $this->fornitore);
|
|
$this->authorizeDipendenteIntervento($this->intervento, $this->dipendente);
|
|
|
|
$this->reloadIntervento();
|
|
}
|
|
|
|
public function assignDipendente(): void
|
|
{
|
|
if ($this->dipendente instanceof FornitoreDipendente) {
|
|
Notification::make()->title('Assegnazione consentita solo al coordinatore fornitore')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$validated = $this->validate([
|
|
'dipendenteId' => ['nullable', 'integer'],
|
|
]);
|
|
|
|
$dipendente = null;
|
|
$selectedId = (int) ($validated['dipendenteId'] ?? 0);
|
|
|
|
if ($selectedId > 0) {
|
|
$dipendente = FornitoreDipendente::query()
|
|
->where('fornitore_id', (int) $this->fornitore->id)
|
|
->where('attivo', true)
|
|
->find($selectedId);
|
|
|
|
if (! $dipendente instanceof FornitoreDipendente) {
|
|
Notification::make()->title('Collaboratore non valido per questo fornitore')->danger()->send();
|
|
return;
|
|
}
|
|
}
|
|
|
|
$this->intervento->eseguito_da_dipendente_id = $dipendente?->id;
|
|
$this->intervento->save();
|
|
|
|
$stamp = now()->format('d/m/Y H:i');
|
|
$message = $dipendente
|
|
? '[' . $stamp . '] Intervento assegnato al collaboratore fornitore: ' . $dipendente->ruolo_operativo_label . '.'
|
|
: '[' . $stamp . '] Assegnazione collaboratore rimossa: intervento riportato al coordinamento del fornitore.';
|
|
|
|
$this->intervento->ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => $message,
|
|
'canale' => 'interno',
|
|
'direzione' => 'inbound',
|
|
'inviato_il' => now(),
|
|
]);
|
|
|
|
$this->reloadIntervento();
|
|
|
|
Notification::make()->title('Assegnazione aggiornata')->success()->send();
|
|
}
|
|
|
|
public function startIntervento(): void
|
|
{
|
|
$this->startInterventoWithGeo([]);
|
|
}
|
|
|
|
public function startInterventoWithGeo(array $geo = []): void
|
|
{
|
|
$activeSession = $this->getActiveSession();
|
|
if ($activeSession instanceof TicketInterventoSessione) {
|
|
Notification::make()->title('Esiste gia una sessione aperta')->body('Ferma prima la sessione attiva o registra un checkpoint.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$startedAt = now();
|
|
$geoData = $this->normalizeGeoPayload($geo);
|
|
|
|
TicketInterventoSessione::query()->create([
|
|
'ticket_intervento_id' => (int) $this->intervento->id,
|
|
'ticket_id' => (int) $this->intervento->ticket_id,
|
|
'fornitore_id' => (int) $this->fornitore->id,
|
|
'fornitore_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id,
|
|
'created_by_user_id' => Auth::id(),
|
|
'session_type' => 'work',
|
|
'note' => 'Avvio intervento',
|
|
'started_at' => $startedAt,
|
|
'start_latitude' => $geoData['lat'],
|
|
'start_longitude' => $geoData['lng'],
|
|
'start_accuracy_meters' => $geoData['accuracy'],
|
|
'start_source' => $geoData['source'],
|
|
'geo_payload' => $geoData['payload'],
|
|
]);
|
|
|
|
$this->intervento->update([
|
|
'stato' => 'in_corso',
|
|
'preso_in_carico_da_user_id' => Auth::id(),
|
|
'eseguito_da_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id,
|
|
'iniziato_at' => $this->intervento->iniziato_at ?? $startedAt,
|
|
'terminato_at' => null,
|
|
]);
|
|
|
|
$this->intervento->ticket->update(['stato' => 'In Lavorazione']);
|
|
|
|
$this->reloadIntervento();
|
|
|
|
Notification::make()->title('Intervento preso in carico')->body($this->formatGeoNotification($geoData, 'Posizione avvio'))->success()->send();
|
|
}
|
|
|
|
public function stopIntervento(): void
|
|
{
|
|
$this->stopInterventoWithGeo([]);
|
|
}
|
|
|
|
public function stopInterventoWithGeo(array $geo = []): void
|
|
{
|
|
$activeSession = $this->getActiveSession();
|
|
if (! $activeSession instanceof TicketInterventoSessione) {
|
|
Notification::make()->title('Avvia prima il timer di intervento')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$endedAt = now();
|
|
$minutes = max(1, $activeSession->started_at?->diffInMinutes($endedAt) ?? 1);
|
|
$geoData = $this->normalizeGeoPayload($geo);
|
|
|
|
$activeSession->update([
|
|
'ended_at' => $endedAt,
|
|
'duration_minutes' => $minutes,
|
|
'closed_by_user_id' => Auth::id(),
|
|
'end_latitude' => $geoData['lat'],
|
|
'end_longitude' => $geoData['lng'],
|
|
'end_accuracy_meters' => $geoData['accuracy'],
|
|
'end_source' => $geoData['source'],
|
|
'geo_payload' => array_merge((array) ($activeSession->geo_payload ?? []), $geoData['payload']),
|
|
]);
|
|
|
|
$totalMinutes = $this->calculateTrackedMinutes();
|
|
|
|
$this->intervento->update([
|
|
'terminato_at' => $endedAt,
|
|
'tempo_minuti' => $totalMinutes,
|
|
'stato' => $this->intervento->stato === 'assegnato' ? 'in_corso' : $this->intervento->stato,
|
|
]);
|
|
|
|
$this->tempoMinuti = $totalMinutes;
|
|
$this->reloadIntervento();
|
|
|
|
Notification::make()->title('Timer intervento fermato')->body('Sessione: ' . $minutes . ' minuti. Totale rilevato: ' . $totalMinutes . ' minuti. ' . $this->formatGeoNotification($geoData, 'Posizione stop'))->success()->send();
|
|
}
|
|
|
|
public function addPresenceCheckpoint(array $geo = []): void
|
|
{
|
|
$geoData = $this->normalizeGeoPayload($geo);
|
|
$stamp = now();
|
|
|
|
TicketInterventoSessione::query()->create([
|
|
'ticket_intervento_id' => (int) $this->intervento->id,
|
|
'ticket_id' => (int) $this->intervento->ticket_id,
|
|
'fornitore_id' => (int) $this->fornitore->id,
|
|
'fornitore_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id,
|
|
'created_by_user_id' => Auth::id(),
|
|
'closed_by_user_id' => Auth::id(),
|
|
'session_type' => 'checkpoint',
|
|
'note' => 'Checkpoint presenza',
|
|
'started_at' => $stamp,
|
|
'ended_at' => $stamp,
|
|
'duration_minutes' => 0,
|
|
'start_latitude' => $geoData['lat'],
|
|
'start_longitude' => $geoData['lng'],
|
|
'start_accuracy_meters' => $geoData['accuracy'],
|
|
'start_source' => $geoData['source'],
|
|
'geo_payload' => $geoData['payload'],
|
|
]);
|
|
|
|
$this->reloadIntervento();
|
|
|
|
Notification::make()->title('Checkpoint presenza registrato')->body($this->formatGeoNotification($geoData, 'Posizione checkpoint'))->success()->send();
|
|
}
|
|
|
|
public function updatedFotoLavoro(): void
|
|
{
|
|
$this->syncRapportoDescriptionSlots();
|
|
}
|
|
|
|
public function updatedDocumentiLavoro(): void
|
|
{
|
|
$this->syncRapportoDescriptionSlots();
|
|
}
|
|
|
|
public function updatedMaterialSearch(): void
|
|
{
|
|
$this->refreshMaterialSuggestions();
|
|
}
|
|
|
|
public function addSuggestedMaterial(int $productId): void
|
|
{
|
|
$product = Product::query()
|
|
->with(['identifiers', 'offers' => fn($query) => $query->where(function ($builder): void {
|
|
$builder->whereNull('fornitore_id')
|
|
->orWhere('fornitore_id', (int) $this->fornitore->id);
|
|
})->orderBy('is_internal', 'desc')->orderBy('price_amount')])
|
|
->find($productId);
|
|
|
|
if (! $product instanceof Product) {
|
|
return;
|
|
}
|
|
|
|
$identifier = $product->identifiers
|
|
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id)
|
|
?? $product->identifiers->first();
|
|
|
|
$offer = $product->offers->first();
|
|
|
|
$this->materialiUtilizzati[] = [
|
|
'product_id' => (int) $product->id,
|
|
'descrizione' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))),
|
|
'barcode' => (string) ($identifier->code_value ?? ''),
|
|
'qty' => 1,
|
|
'unit_price' => $offer?->price_amount !== null ? (float) $offer->price_amount : null,
|
|
'currency' => (string) ($offer->currency ?? 'EUR'),
|
|
'source_type' => 'catalog',
|
|
'source_label' => (string) ($offer?->source_name ?: 'Catalogo fornitore'),
|
|
'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)),
|
|
];
|
|
|
|
$this->materialSearch = '';
|
|
$this->materialSuggestions = [];
|
|
}
|
|
|
|
public function addManualMaterial(): void
|
|
{
|
|
$term = trim($this->materialSearch);
|
|
if ($term === '') {
|
|
Notification::make()->title('Inserisci un prodotto o barcode')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$this->materialiUtilizzati[] = [
|
|
'product_id' => null,
|
|
'descrizione' => $term,
|
|
'barcode' => '',
|
|
'qty' => 1,
|
|
'unit_price' => null,
|
|
'currency' => 'EUR',
|
|
'source_type' => 'manual',
|
|
'source_label' => 'Inserimento manuale',
|
|
'external_url' => $this->buildAmazonSearchUrl($term),
|
|
];
|
|
|
|
$this->materialSearch = '';
|
|
$this->materialSuggestions = [];
|
|
}
|
|
|
|
public function removeMateriale(int $index): void
|
|
{
|
|
unset($this->materialiUtilizzati[$index]);
|
|
$this->materialiUtilizzati = array_values($this->materialiUtilizzati);
|
|
}
|
|
|
|
public function saveRapporto(): void
|
|
{
|
|
$validated = $this->validate([
|
|
'rapportoFornitore' => ['required', 'string', 'max:5000'],
|
|
'tempoMinuti' => ['nullable', 'integer', 'min:1', 'max:1440'],
|
|
'articoliUtilizzati' => ['nullable', 'array'],
|
|
'articoliUtilizzati.*' => ['nullable', 'string', 'max:120'],
|
|
'materialiUtilizzati' => ['nullable', 'array'],
|
|
'materialiUtilizzati.*.product_id' => ['nullable', 'integer'],
|
|
'materialiUtilizzati.*.descrizione' => ['required', 'string', 'max:255'],
|
|
'materialiUtilizzati.*.barcode' => ['nullable', 'string', 'max:120'],
|
|
'materialiUtilizzati.*.qty' => ['nullable', 'numeric', 'min:0.01', 'max:9999'],
|
|
'materialiUtilizzati.*.unit_price' => ['nullable', 'numeric', 'min:0', 'max:999999.99'],
|
|
'materialiUtilizzati.*.currency' => ['nullable', 'string', 'max:8'],
|
|
'materialiUtilizzati.*.source_type' => ['nullable', 'string', 'max:32'],
|
|
'materialiUtilizzati.*.source_label' => ['nullable', 'string', 'max:120'],
|
|
'materialiUtilizzati.*.external_url' => ['nullable', 'string', 'max:1000'],
|
|
'fotoLavoro' => ['nullable', 'array', 'max:10'],
|
|
'fotoLavoro.*' => ['nullable', 'image', 'max:10240'],
|
|
'documentiLavoro' => ['nullable', 'array', 'max:10'],
|
|
'documentiLavoro.*' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,xls,xlsx,txt,jpg,jpeg,png,webp'],
|
|
'descrizioneFile' => ['nullable', 'array'],
|
|
'descrizioneFile.*' => ['nullable', 'string', 'max:255'],
|
|
'apparatoMarca' => ['nullable', 'string', 'max:120'],
|
|
'apparatoModello' => ['nullable', 'string', 'max:120'],
|
|
'apparatoSeriale' => ['nullable', 'string', 'max:120'],
|
|
'messaggioVeloce' => ['nullable', 'string', 'max:1500'],
|
|
'lavoroStandard' => ['nullable', 'boolean'],
|
|
'richiestaProforma' => ['nullable', 'boolean'],
|
|
'proformaCodice' => ['nullable', 'string', 'max:80'],
|
|
'proformaNote' => ['nullable', 'string', 'max:2000'],
|
|
'qrToken' => ['nullable', 'string', 'max:40'],
|
|
]);
|
|
|
|
$rapporto = trim((string) $validated['rapportoFornitore']);
|
|
$apparato = $this->buildApparatoSummary(
|
|
(string) ($validated['apparatoMarca'] ?? ''),
|
|
(string) ($validated['apparatoModello'] ?? ''),
|
|
(string) ($validated['apparatoSeriale'] ?? '')
|
|
);
|
|
|
|
if ($apparato !== '') {
|
|
$rapporto .= "\n\n" . $apparato;
|
|
}
|
|
|
|
$computedTempoMinuti = $validated['tempoMinuti'] ?? null;
|
|
$computedTerminatedAt = $this->intervento->terminato_at ?? now();
|
|
|
|
if (! $computedTempoMinuti && $this->intervento->iniziato_at) {
|
|
$computedTempoMinuti = max(1, $this->intervento->iniziato_at->diffInMinutes($computedTerminatedAt));
|
|
}
|
|
|
|
$payload = [
|
|
'rapporto_fornitore' => $rapporto,
|
|
'tempo_minuti' => $computedTempoMinuti,
|
|
'terminato_at' => $computedTerminatedAt,
|
|
'stato' => ! empty($validated['lavoroStandard']) ? 'fatturabile' : 'in_verifica',
|
|
'eseguito_da_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id,
|
|
];
|
|
|
|
$materiali = collect($validated['materialiUtilizzati'] ?? [])
|
|
->map(function ($item): array {
|
|
return [
|
|
'product_id' => isset($item['product_id']) ? (int) $item['product_id'] : null,
|
|
'descrizione' => trim((string) ($item['descrizione'] ?? '')),
|
|
'barcode' => trim((string) ($item['barcode'] ?? '')),
|
|
'qty' => isset($item['qty']) && is_numeric($item['qty']) ? (float) $item['qty'] : 1.0,
|
|
'unit_price' => isset($item['unit_price']) && is_numeric($item['unit_price']) ? round((float) $item['unit_price'], 2) : null,
|
|
'currency' => trim((string) ($item['currency'] ?? 'EUR')) ?: 'EUR',
|
|
'source_type' => trim((string) ($item['source_type'] ?? 'manual')) ?: 'manual',
|
|
'source_label' => trim((string) ($item['source_label'] ?? '')),
|
|
'external_url' => trim((string) ($item['external_url'] ?? '')),
|
|
];
|
|
})
|
|
->filter(fn(array $item): bool => $item['descrizione'] !== '')
|
|
->values()
|
|
->all();
|
|
|
|
$articoli = collect($validated['articoliUtilizzati'] ?? [])
|
|
->map(fn($item) => trim((string) $item))
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
if ($materiali !== []) {
|
|
$payload['materiali_utilizzati'] = $materiali;
|
|
|
|
$materialiLabels = collect($materiali)
|
|
->map(function (array $item): string {
|
|
$label = $item['descrizione'];
|
|
$qty = (float) ($item['qty'] ?? 1);
|
|
|
|
return $label . ($qty > 0 ? (' x' . rtrim(rtrim(number_format($qty, 2, '.', ''), '0'), '.')) : '');
|
|
})
|
|
->values()
|
|
->all();
|
|
|
|
$articoli = array_values(array_unique(array_merge($articoli, $materialiLabels)));
|
|
}
|
|
|
|
if ($articoli !== []) {
|
|
$payload['articoli_utilizzati'] = $articoli;
|
|
}
|
|
|
|
$allFiles = array_merge($this->fotoLavoro, $this->documentiLavoro);
|
|
$firstPhotoPath = null;
|
|
|
|
if ($allFiles !== [] && Schema::hasTable('ticket_attachments')) {
|
|
foreach ($allFiles as $index => $file) {
|
|
if (! is_object($file) || ! method_exists($file, 'store')) {
|
|
continue;
|
|
}
|
|
|
|
$originalName = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
|
|
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
|
|
$isImage = str_starts_with($mime, 'image/');
|
|
|
|
$stored = app(\App\Services\Support\TicketAttachmentUploadService::class)->store(
|
|
$file,
|
|
'ticket-interventi/allegati/' . $this->intervento->id,
|
|
[
|
|
'stored_basename' => pathinfo($this->buildStoredUploadDisplayName($index, $isImage, $originalName), PATHINFO_FILENAME),
|
|
'display_name' => $originalName,
|
|
]
|
|
);
|
|
|
|
if ($isImage && $firstPhotoPath === null) {
|
|
$firstPhotoPath = (string) $stored['path'];
|
|
}
|
|
|
|
TicketAttachment::query()->create([
|
|
'ticket_id' => (int) $this->intervento->ticket_id,
|
|
'ticket_update_id' => null,
|
|
'user_id' => (int) Auth::id(),
|
|
'file_path' => (string) $stored['path'],
|
|
'original_file_name' => (string) $stored['original_name'],
|
|
'mime_type' => (string) $stored['mime'],
|
|
'size' => (int) $stored['size'],
|
|
'description' => trim((string) ($this->descrizioneFile[$index] ?? '')) ?: ($isImage ? 'Foto lavoro fornitore' : 'Allegato rapporto fornitore'),
|
|
]);
|
|
}
|
|
}
|
|
|
|
if ($firstPhotoPath !== null) {
|
|
$payload['foto_path'] = $firstPhotoPath;
|
|
}
|
|
|
|
$tokenInserito = trim((string) ($validated['qrToken'] ?? ''));
|
|
if ($tokenInserito !== '' && strcasecmp($tokenInserito, (string) $this->intervento->qr_token) === 0) {
|
|
$payload['qr_scansionato_at'] = now();
|
|
}
|
|
|
|
$this->intervento->update($payload);
|
|
|
|
$stamp = now()->format('d/m/Y H:i');
|
|
$this->intervento->ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => '[' . $stamp . '] Rapporto intervento caricato dal fornitore.',
|
|
'canale' => 'interno',
|
|
'direzione' => 'inbound',
|
|
'inviato_il' => now(),
|
|
]);
|
|
|
|
$proformaCodice = trim((string) ($validated['proformaCodice'] ?? ''));
|
|
$proformaNote = trim((string) ($validated['proformaNote'] ?? ''));
|
|
if (! empty($validated['richiestaProforma']) || $proformaCodice !== '' || $proformaNote !== '') {
|
|
$this->intervento->ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => '[' . $stamp . '] Proforma fornitore ' . ($proformaCodice !== '' ? ('codice ' . $proformaCodice) : '(senza codice)') . ($proformaNote !== '' ? (' - ' . $proformaNote) : ''),
|
|
'canale' => 'interno',
|
|
'direzione' => 'inbound',
|
|
'inviato_il' => now(),
|
|
]);
|
|
}
|
|
|
|
$quickMessage = trim((string) ($validated['messaggioVeloce'] ?? ''));
|
|
if ($quickMessage !== '') {
|
|
$this->intervento->ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => '[' . $stamp . '] Messaggio veloce fornitore: ' . $quickMessage,
|
|
'canale' => 'interno',
|
|
'direzione' => 'inbound',
|
|
'inviato_il' => now(),
|
|
]);
|
|
}
|
|
|
|
$this->fotoLavoro = [];
|
|
$this->documentiLavoro = [];
|
|
$this->descrizioneFile = [];
|
|
$this->materialiUtilizzati = [];
|
|
$this->materialSearch = '';
|
|
$this->materialSuggestions = [];
|
|
$this->messaggioVeloce = '';
|
|
$this->proformaCodice = '';
|
|
$this->proformaNote = '';
|
|
$this->dispatch('fornitore-local-previews-clear');
|
|
|
|
$this->reloadIntervento();
|
|
|
|
Notification::make()->title('Rapporto inviato in verifica amministratore')->success()->send();
|
|
}
|
|
|
|
public function inviaSollecito(): void
|
|
{
|
|
$stamp = now()->format('d/m/Y H:i');
|
|
$this->intervento->ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => '[' . $stamp . '] Sollecito fornitore su ticket/intervento in corso.',
|
|
'canale' => 'interno',
|
|
'direzione' => 'inbound',
|
|
'inviato_il' => now(),
|
|
]);
|
|
|
|
$this->reloadIntervento();
|
|
|
|
Notification::make()->title('Sollecito inviato allo studio amministrativo')->success()->send();
|
|
}
|
|
|
|
public function getTicketsUrl(): string
|
|
{
|
|
return TicketOperativi::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
|
|
}
|
|
|
|
public function getAttachmentPublicUrl(TicketAttachment $attachment): string
|
|
{
|
|
return route('filament.tickets.attachments.view', ['attachment' => (int) $attachment->id]);
|
|
}
|
|
|
|
public function openAttachmentPreview(int $attachmentId): void
|
|
{
|
|
$attachment = $this->intervento->ticket->attachments->firstWhere('id', $attachmentId);
|
|
if (! $attachment instanceof TicketAttachment) {
|
|
return;
|
|
}
|
|
|
|
$this->attachmentPreview = [
|
|
'id' => (int) $attachment->id,
|
|
'name' => (string) ($attachment->original_file_name ?? 'allegato'),
|
|
'description' => (string) ($attachment->description ?? ''),
|
|
'url' => $this->getAttachmentPublicUrl($attachment),
|
|
'is_image' => $attachment->isImage(),
|
|
'is_pdf' => $attachment->isPdf(),
|
|
'mime' => $attachment->resolvedMimeType(),
|
|
];
|
|
}
|
|
|
|
public function closeAttachmentPreview(): void
|
|
{
|
|
$this->attachmentPreview = null;
|
|
}
|
|
|
|
public function getCollaboratoriUrl(): string
|
|
{
|
|
return Collaboratori::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
|
|
}
|
|
|
|
public function getRubricaUrl(): string
|
|
{
|
|
return RubricaClienti::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
|
|
}
|
|
|
|
public function getProdottiUrl(): string
|
|
{
|
|
return ProdottiCatalogo::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
|
|
}
|
|
|
|
public function getGoogleConnectUrl(): string
|
|
{
|
|
return route('oauth.google.redirect');
|
|
}
|
|
|
|
public function getGoogleDisconnectUrl(): string
|
|
{
|
|
return route('oauth.google.disconnect');
|
|
}
|
|
|
|
public function canAssignDipendente(): bool
|
|
{
|
|
return ! ($this->dipendente instanceof FornitoreDipendente) && count($this->dipendentiOptions) > 0;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function getRapportinoUploadsProperty(): array
|
|
{
|
|
$rows = [];
|
|
|
|
foreach (array_merge($this->fotoLavoro, $this->documentiLavoro) as $index => $file) {
|
|
if (! is_object($file)) {
|
|
continue;
|
|
}
|
|
|
|
$name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
|
|
$size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0);
|
|
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
|
|
$isImage = str_starts_with($mime, 'image/');
|
|
$isPdf = $mime === 'application/pdf' || str_ends_with(strtolower($name), '.pdf');
|
|
$previewUrl = null;
|
|
|
|
if ($isImage && method_exists($file, 'temporaryUrl')) {
|
|
try {
|
|
$previewUrl = (string) $file->temporaryUrl();
|
|
} catch (\Throwable) {
|
|
$previewUrl = null;
|
|
}
|
|
}
|
|
|
|
$rows[] = [
|
|
'index' => $index,
|
|
'name' => $name,
|
|
'size' => $size,
|
|
'mime' => $mime,
|
|
'is_image' => $isImage,
|
|
'is_pdf' => $isPdf,
|
|
'preview_url' => $previewUrl,
|
|
'description' => (string) ($this->descrizioneFile[$index] ?? ''),
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
protected function resolveInterventoRecord(int $recordId, ?Fornitore $fornitore = null, ?FornitoreDipendente $dipendente = null): ?TicketIntervento
|
|
{
|
|
$query = TicketIntervento::query();
|
|
|
|
if ($fornitore instanceof Fornitore) {
|
|
$query->where('fornitore_id', (int) $fornitore->id);
|
|
}
|
|
|
|
if ($dipendente instanceof FornitoreDipendente) {
|
|
$query->where(function (Builder $builder) use ($dipendente): void {
|
|
$builder->whereNull('eseguito_da_dipendente_id')
|
|
->orWhere('eseguito_da_dipendente_id', (int) $dipendente->id);
|
|
});
|
|
}
|
|
|
|
return $query
|
|
->where(function (Builder $builder) use ($recordId): void {
|
|
$builder->whereKey($recordId)
|
|
->orWhere('ticket_id', $recordId);
|
|
})
|
|
->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [$recordId])
|
|
->latest('id')
|
|
->first();
|
|
}
|
|
|
|
protected function syncRapportoDescriptionSlots(): void
|
|
{
|
|
$next = [];
|
|
|
|
foreach (array_merge($this->fotoLavoro, $this->documentiLavoro) as $index => $_file) {
|
|
$next[$index] = (string) ($this->descrizioneFile[$index] ?? '');
|
|
}
|
|
|
|
$this->descrizioneFile = $next;
|
|
}
|
|
|
|
protected function buildStoredUploadDisplayName(int $index, bool $isImage, string $originalName): string
|
|
{
|
|
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
|
|
$baseName = 'ticket-' . str_pad((string) $this->intervento->ticket_id, 6, '0', STR_PAD_LEFT)
|
|
. '-int-' . str_pad((string) $this->intervento->id, 6, '0', STR_PAD_LEFT)
|
|
. '-' . ($isImage ? 'foto-' : 'doc-') . str_pad((string) ($index + 1), 2, '0', STR_PAD_LEFT);
|
|
|
|
return $baseName . ($extension !== '' ? '.' . $extension : '');
|
|
}
|
|
|
|
protected function reloadIntervento(): void
|
|
{
|
|
$this->intervento->load([
|
|
'ticket.stabile',
|
|
'ticket.unitaImmobiliare',
|
|
'ticket.soggettoRichiedente',
|
|
'ticket.messages.user',
|
|
'ticket.attachments.user',
|
|
'ticket.apertoDaUser',
|
|
'ticket.assegnatoAUser',
|
|
'ticket.assegnatoAFornitore',
|
|
'eseguitoDaDipendente',
|
|
'sessioni',
|
|
]);
|
|
|
|
$this->intervento->refresh();
|
|
$this->intervento->load([
|
|
'ticket.stabile',
|
|
'ticket.unitaImmobiliare',
|
|
'ticket.soggettoRichiedente',
|
|
'ticket.messages.user',
|
|
'ticket.attachments.user',
|
|
'ticket.apertoDaUser',
|
|
'ticket.assegnatoAUser',
|
|
'ticket.assegnatoAFornitore',
|
|
'eseguitoDaDipendente',
|
|
'sessioni',
|
|
]);
|
|
|
|
$this->caller = $this->resolveCallerData($this->intervento->ticket);
|
|
$this->storicoRows = TicketIntervento::query()
|
|
->with(['ticket'])
|
|
->where('fornitore_id', (int) $this->fornitore->id)
|
|
->where('id', '!=', (int) $this->intervento->id)
|
|
->when(
|
|
(int) ($this->intervento->ticket?->soggetto_richiedente_id ?? 0) > 0,
|
|
fn($query) => $query->whereHas('ticket', fn($ticketQuery) => $ticketQuery->where('soggetto_richiedente_id', (int) $this->intervento->ticket->soggetto_richiedente_id)),
|
|
fn($query) => $query->whereHas('ticket', fn($ticketQuery) => $ticketQuery->where('stabile_id', (int) ($this->intervento->ticket?->stabile_id ?? 0)))
|
|
)
|
|
->latest('id')
|
|
->limit(12)
|
|
->get()
|
|
->map(fn(TicketIntervento $old) => [
|
|
'ticket_id' => (int) $old->ticket_id,
|
|
'titolo' => (string) ($old->ticket->titolo ?? '-'),
|
|
'stato' => (string) $old->stato,
|
|
'updated_at' => optional($old->updated_at)->format('d/m/Y H:i') ?: '-',
|
|
'note' => (string) ($old->note_amministratore ?? ''),
|
|
])
|
|
->all();
|
|
|
|
$this->dipendentiOptions = $this->fornitore->dipendenti()
|
|
->where('attivo', true)
|
|
->with('fornitoreEsterno')
|
|
->orderBy('cognome')
|
|
->orderBy('nome')
|
|
->get()
|
|
->map(fn(FornitoreDipendente $dipendente) => [
|
|
'id' => (int) $dipendente->id,
|
|
'label' => $dipendente->ruolo_operativo_label . ($dipendente->email ? ' · ' . $dipendente->email : ''),
|
|
])
|
|
->all();
|
|
|
|
$this->dipendenteId = (int) ($this->intervento->eseguito_da_dipendente_id ?? 0) ?: null;
|
|
$this->rapportoFornitore = (string) ($this->intervento->rapporto_fornitore ?? '');
|
|
$this->tempoMinuti = $this->calculateTrackedMinutes();
|
|
$this->articoliUtilizzati = array_pad((array) ($this->intervento->articoli_utilizzati ?? []), 3, '');
|
|
$this->materialiUtilizzati = collect((array) ($this->intervento->materiali_utilizzati ?? []))
|
|
->map(fn(array $item): array => [
|
|
'product_id' => isset($item['product_id']) ? (int) $item['product_id'] : null,
|
|
'descrizione' => (string) ($item['descrizione'] ?? ''),
|
|
'barcode' => (string) ($item['barcode'] ?? ''),
|
|
'qty' => isset($item['qty']) ? (float) $item['qty'] : 1,
|
|
'unit_price' => isset($item['unit_price']) ? (float) $item['unit_price'] : null,
|
|
'currency' => (string) ($item['currency'] ?? 'EUR'),
|
|
'source_type' => (string) ($item['source_type'] ?? 'manual'),
|
|
'source_label' => (string) ($item['source_label'] ?? ''),
|
|
'external_url' => (string) ($item['external_url'] ?? ''),
|
|
])
|
|
->values()
|
|
->all();
|
|
$this->sessionRows = $this->intervento->sessioni
|
|
->sortByDesc(fn(TicketInterventoSessione $sessione) => $sessione->started_at?->getTimestamp() ?? 0)
|
|
->values()
|
|
->map(function (TicketInterventoSessione $sessione): array {
|
|
return [
|
|
'id' => (int) $sessione->id,
|
|
'type' => (string) $sessione->session_type,
|
|
'note' => (string) ($sessione->note ?? ''),
|
|
'started_at' => optional($sessione->started_at)->format('d/m/Y H:i:s') ?: '-',
|
|
'ended_at' => optional($sessione->ended_at)->format('d/m/Y H:i:s') ?: 'Aperta',
|
|
'duration_minutes' => $sessione->duration_minutes,
|
|
'start_geo' => $this->formatGeoPoint($sessione->start_latitude, $sessione->start_longitude, $sessione->start_accuracy_meters),
|
|
'end_geo' => $this->formatGeoPoint($sessione->end_latitude, $sessione->end_longitude, $sessione->end_accuracy_meters),
|
|
];
|
|
})
|
|
->all();
|
|
$latestSession = $this->intervento->sessioni
|
|
->sortByDesc(fn(TicketInterventoSessione $sessione) => $sessione->updated_at?->getTimestamp() ?? 0)
|
|
->first();
|
|
$this->lastGeoSnapshot = $latestSession instanceof TicketInterventoSessione
|
|
? [
|
|
'label' => (string) ($latestSession->session_type === 'checkpoint' ? 'Ultimo checkpoint' : 'Ultimo evento sessione'),
|
|
'start_geo' => $this->formatGeoPoint($latestSession->start_latitude, $latestSession->start_longitude, $latestSession->start_accuracy_meters),
|
|
'end_geo' => $this->formatGeoPoint($latestSession->end_latitude, $latestSession->end_longitude, $latestSession->end_accuracy_meters),
|
|
'updated_at' => optional($latestSession->updated_at)->format('d/m/Y H:i:s') ?: '-',
|
|
]
|
|
: null;
|
|
$this->googleWorkspaceStatus = $this->resolveGoogleWorkspaceStatus();
|
|
$this->qrToken = '';
|
|
}
|
|
|
|
protected function getActiveSession(): ?TicketInterventoSessione
|
|
{
|
|
return TicketInterventoSessione::query()
|
|
->where('ticket_intervento_id', (int) $this->intervento->id)
|
|
->where('session_type', 'work')
|
|
->whereNull('ended_at')
|
|
->latest('started_at')
|
|
->first();
|
|
}
|
|
|
|
protected function calculateTrackedMinutes(): ?int
|
|
{
|
|
$total = (int) TicketInterventoSessione::query()
|
|
->where('ticket_intervento_id', (int) $this->intervento->id)
|
|
->sum('duration_minutes');
|
|
|
|
$activeSession = $this->getActiveSession();
|
|
if ($activeSession instanceof TicketInterventoSessione && $activeSession->started_at) {
|
|
$total += max(1, $activeSession->started_at->diffInMinutes(now()));
|
|
}
|
|
|
|
return $total > 0 ? $total : ($this->intervento->tempo_minuti ? (int) $this->intervento->tempo_minuti : null);
|
|
}
|
|
|
|
/**
|
|
* @return array{lat:?float,lng:?float,accuracy:?int,source:string,payload:array<string,mixed>}
|
|
*/
|
|
protected function normalizeGeoPayload(array $geo): array
|
|
{
|
|
$lat = isset($geo['lat']) && is_numeric($geo['lat']) ? round((float) $geo['lat'], 7) : null;
|
|
$lng = isset($geo['lng']) && is_numeric($geo['lng']) ? round((float) $geo['lng'], 7) : null;
|
|
$accuracy = isset($geo['accuracy']) && is_numeric($geo['accuracy']) ? max(0, (int) round((float) $geo['accuracy'])) : null;
|
|
$source = trim((string) ($geo['source'] ?? 'browser')) ?: 'browser';
|
|
$error = trim((string) ($geo['error'] ?? ''));
|
|
|
|
return [
|
|
'lat' => $lat,
|
|
'lng' => $lng,
|
|
'accuracy' => $accuracy,
|
|
'source' => $source,
|
|
'payload' => array_filter([
|
|
'lat' => $lat,
|
|
'lng' => $lng,
|
|
'accuracy' => $accuracy,
|
|
'source' => $source,
|
|
'error' => $error !== '' ? $error : null,
|
|
'captured_at' => now()->toDateTimeString(),
|
|
], fn($value) => $value !== null && $value !== ''),
|
|
];
|
|
}
|
|
|
|
protected function formatGeoNotification(array $geoData, string $prefix): string
|
|
{
|
|
if ($geoData['lat'] === null || $geoData['lng'] === null) {
|
|
return $prefix . ': geolocalizzazione non disponibile dal browser.';
|
|
}
|
|
|
|
$message = $prefix . ': ' . number_format((float) $geoData['lat'], 5, '.', '') . ', ' . number_format((float) $geoData['lng'], 5, '.', '');
|
|
if (is_int($geoData['accuracy'])) {
|
|
$message .= ' ±' . $geoData['accuracy'] . ' m';
|
|
}
|
|
|
|
return $message;
|
|
}
|
|
|
|
protected function formatGeoPoint(mixed $lat, mixed $lng, mixed $accuracy): string
|
|
{
|
|
if (! is_numeric($lat) || ! is_numeric($lng)) {
|
|
return '-';
|
|
}
|
|
|
|
$label = number_format((float) $lat, 5, '.', '') . ', ' . number_format((float) $lng, 5, '.', '');
|
|
if (is_numeric($accuracy)) {
|
|
$label .= ' ±' . (int) $accuracy . ' m';
|
|
}
|
|
|
|
return $label;
|
|
}
|
|
|
|
protected function refreshMaterialSuggestions(): void
|
|
{
|
|
$term = trim($this->materialSearch);
|
|
if ($term === '') {
|
|
$this->materialSuggestions = [];
|
|
return;
|
|
}
|
|
|
|
$normalized = preg_replace('/[^A-Za-z0-9]+/', '', strtoupper($term)) ?? '';
|
|
|
|
$products = Product::query()
|
|
->with(['identifiers', 'offers' => fn($query) => $query->orderBy('is_internal', 'desc')->orderBy('price_amount')])
|
|
->where('is_active', true)
|
|
->where(function ($query) use ($term, $normalized): void {
|
|
$query->where('name', 'like', '%' . $term . '%')
|
|
->orWhere('brand', 'like', '%' . $term . '%')
|
|
->orWhere('model', 'like', '%' . $term . '%')
|
|
->orWhere('internal_code', 'like', '%' . $term . '%');
|
|
|
|
if ($normalized !== '') {
|
|
$query->orWhereHas('identifiers', function ($identifierQuery) use ($normalized): void {
|
|
$identifierQuery->where('normalized_code', $normalized)
|
|
->orWhere('code_value', 'like', '%' . $normalized . '%');
|
|
});
|
|
}
|
|
})
|
|
->where(function ($query): void {
|
|
$query->whereNull('default_fornitore_id')
|
|
->orWhere('default_fornitore_id', (int) $this->fornitore->id)
|
|
->orWhereHas('offers', fn($offerQuery) => $offerQuery->where('fornitore_id', (int) $this->fornitore->id));
|
|
})
|
|
->limit(8)
|
|
->get();
|
|
|
|
$this->materialSuggestions = $products->map(function (Product $product): array {
|
|
$identifier = $product->identifiers
|
|
->first(fn(ProductIdentifier $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id)
|
|
?? $product->identifiers->first();
|
|
$offer = $product->offers
|
|
->first(fn(ProductOffer $item): bool => (int) ($item->fornitore_id ?? 0) === (int) $this->fornitore->id)
|
|
?? $product->offers->first();
|
|
|
|
return [
|
|
'id' => (int) $product->id,
|
|
'label' => trim(implode(' ', array_filter([(string) $product->name, (string) ($product->brand ?? ''), (string) ($product->model ?? '')]))),
|
|
'barcode' => (string) ($identifier->code_value ?? ''),
|
|
'price' => $offer?->price_amount !== null ? number_format((float) $offer->price_amount, 2, ',', '.') . ' ' . (string) ($offer->currency ?? 'EUR') : null,
|
|
'source' => (string) ($offer?->source_name ?: 'Catalogo'),
|
|
'external_url' => (string) ($offer?->referral_url ?: $offer?->external_url ?: $this->buildAmazonSearchUrl((string) $product->name)),
|
|
];
|
|
})->all();
|
|
}
|
|
|
|
protected function buildAmazonSearchUrl(string $term): string
|
|
{
|
|
$query = ['k' => trim($term)];
|
|
$tag = trim((string) config('services.amazon.referral_tag', ''));
|
|
if ($tag !== '') {
|
|
$query['tag'] = $tag;
|
|
}
|
|
|
|
return 'https://www.amazon.it/s?' . http_build_query($query);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
protected function resolveGoogleWorkspaceStatus(): ?array
|
|
{
|
|
$amministratore = $this->fornitore->amministratore;
|
|
if (! $amministratore) {
|
|
return null;
|
|
}
|
|
|
|
$oauth = (array) (($amministratore->impostazioni['google']['oauth'] ?? null) ?: []);
|
|
|
|
return [
|
|
'connected' => (bool) ($oauth['connected'] ?? false),
|
|
'email' => (string) ($oauth['email'] ?? ''),
|
|
'name' => (string) ($oauth['name'] ?? ''),
|
|
'connected_at' => (string) ($oauth['connected_at'] ?? ''),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string}
|
|
*/
|
|
protected function resolveCallerData(?Ticket $ticket): array
|
|
{
|
|
$parsed = $this->extractCallerData((string) ($ticket?->descrizione ?? ''));
|
|
|
|
$caller = [
|
|
'contatto' => $parsed['contatto'],
|
|
'telefono' => $parsed['telefono'],
|
|
'email' => '',
|
|
'problema' => $parsed['problema'] !== '' ? $parsed['problema'] : (string) ($ticket?->titolo ?? ''),
|
|
'sorgente' => 'Descrizione ticket',
|
|
'riferimento' => $this->buildCallerReference($ticket),
|
|
];
|
|
|
|
$soggetto = $ticket?->soggettoRichiedente;
|
|
if ($soggetto) {
|
|
$label = trim((string) ($soggetto->ragione_sociale ?: trim(($soggetto->nome ?? '') . ' ' . ($soggetto->cognome ?? ''))));
|
|
if ($label !== '') {
|
|
$caller['contatto'] = $label;
|
|
}
|
|
|
|
$caller['telefono'] = trim((string) ($soggetto->telefono ?? '')) ?: $caller['telefono'];
|
|
$caller['email'] = trim((string) ($soggetto->email ?? ''));
|
|
$caller['sorgente'] = 'Richiedente collegato al ticket';
|
|
|
|
return $caller;
|
|
}
|
|
|
|
$openedBy = $ticket?->apertoDaUser;
|
|
if ($openedBy instanceof User) {
|
|
$caller['contatto'] = trim((string) ($openedBy->name ?? '')) ?: $caller['contatto'];
|
|
$caller['email'] = trim((string) ($openedBy->email ?? ''));
|
|
$caller['sorgente'] = 'Utente che ha aperto il ticket';
|
|
}
|
|
|
|
return $caller;
|
|
}
|
|
|
|
protected function buildCallerReference(?Ticket $ticket): string
|
|
{
|
|
if (! $ticket instanceof Ticket) {
|
|
return '';
|
|
}
|
|
|
|
$parts = [];
|
|
|
|
$stabile = $ticket->stabile;
|
|
if ($stabile) {
|
|
$denominazione = trim((string) ($stabile->denominazione ?? ''));
|
|
$indirizzo = trim(implode(' ', array_filter([
|
|
$stabile->indirizzo ?? null,
|
|
$stabile->cap ?? null,
|
|
$stabile->citta ?? null,
|
|
])));
|
|
|
|
if ($denominazione !== '') {
|
|
$parts[] = $denominazione;
|
|
}
|
|
|
|
if ($indirizzo !== '') {
|
|
$parts[] = $indirizzo;
|
|
}
|
|
}
|
|
|
|
$luogoIntervento = trim((string) ($ticket->luogo_intervento ?? ''));
|
|
if ($luogoIntervento !== '') {
|
|
$parts[] = 'Luogo: ' . $luogoIntervento;
|
|
}
|
|
|
|
return implode(' · ', array_unique($parts));
|
|
}
|
|
}
|