netgescon-day0/app/Http/Controllers/Fornitore/TicketOperativoController.php

395 lines
15 KiB
PHP

<?php
namespace App\Http\Controllers\Fornitore;
use App\Http\Controllers\Controller;
use App\Models\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\TicketAttachment;
use App\Models\TicketIntervento;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
class TicketOperativoController extends Controller
{
public function index(Request $request)
{
[$fornitore] = $this->resolveOperatoreContext();
$stato = (string) $request->query('stato', 'aperti');
$query = TicketIntervento::query()
->with(['ticket.stabile'])
->where('fornitore_id', $fornitore->id)
->orderByDesc('created_at');
if ($stato === 'chiusi') {
$query->whereIn('stato', ['chiuso', 'fatturato']);
} elseif ($stato === 'fatturabili') {
$query->whereIn('stato', ['fatturabile', 'fatturato']);
} else {
$query->whereNotIn('stato', ['chiuso']);
}
$interventi = $query->paginate(15)->withQueryString();
$rows = collect($interventi->items())
->map(fn(TicketIntervento $intervento): array => $this->buildInterventoRow($intervento));
$fatturabili = TicketIntervento::query()
->where('fornitore_id', $fornitore->id)
->whereIn('stato', ['fatturabile', 'fatturato'])
->orderByDesc('updated_at')
->limit(100)
->get();
return view('fornitore.tickets.index', [
'fornitore' => $fornitore,
'interventi' => $interventi,
'rows' => $rows,
'stato' => $stato,
'fatturabili' => $fatturabili,
]);
}
public function show(TicketIntervento $intervento)
{
[$fornitore] = $this->resolveOperatoreContext();
$this->authorizeIntervento($intervento, $fornitore);
$intervento->load([
'ticket.stabile',
'ticket.messages.user',
'ticket.attachments.user',
'ticket.apertoDaUser',
'ticket.assegnatoAUser',
'ticket.assegnatoAFornitore',
'eseguitoDaDipendente',
]);
$storico = TicketIntervento::query()
->with(['ticket'])
->where('fornitore_id', $fornitore->id)
->where('id', '!=', $intervento->id)
->when(
(int) ($intervento->ticket?->soggetto_richiedente_id ?? 0) > 0,
fn($q) => $q->whereHas('ticket', fn($t) => $t->where('soggetto_richiedente_id', (int) $intervento->ticket->soggetto_richiedente_id)),
fn($q) => $q->whereHas('ticket', fn($t) => $t->where('stabile_id', (int) ($intervento->ticket?->stabile_id ?? 0)))
)
->latest('id')
->limit(12)
->get();
$caller = $this->extractCallerData((string) ($intervento->ticket?->descrizione ?? ''));
return view('fornitore.tickets.show', [
'fornitore' => $fornitore,
'intervento' => $intervento,
'storico' => $storico,
'caller' => $caller,
]);
}
public function start(TicketIntervento $intervento)
{
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
$this->authorizeIntervento($intervento, $fornitore);
$intervento->update([
'stato' => 'in_corso',
'preso_in_carico_da_user_id' => Auth::id(),
'eseguito_da_dipendente_id' => $dipendente?->id,
'iniziato_at' => $intervento->iniziato_at ?? now(),
]);
$intervento->ticket->update(['stato' => 'In Lavorazione']);
return redirect()->route('fornitore.tickets.show', $intervento)
->with('success', 'Intervento preso in carico.');
}
public function complete(Request $request, TicketIntervento $intervento)
{
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
$this->authorizeIntervento($intervento, $fornitore);
$validated = $request->validate([
'rapporto_fornitore' => 'required|string|max:5000',
'tempo_minuti' => 'nullable|integer|min:1|max:1440',
'articoli_utilizzati' => 'nullable|array',
'articoli_utilizzati.*' => 'nullable|string|max:120',
'foto_lavoro' => 'nullable|array|max:10',
'foto_lavoro.*' => 'nullable|image|max:10240',
'documenti_lavoro' => 'nullable|array|max:10',
'documenti_lavoro.*' => 'nullable|file|max:10240|mimes:pdf,doc,docx,xls,xlsx,txt,jpg,jpeg,png,webp',
'descrizione_file' => 'nullable|array',
'descrizione_file.*' => 'nullable|string|max:255',
'apparato_marca' => 'nullable|string|max:120',
'apparato_modello' => 'nullable|string|max:120',
'apparato_seriale' => 'nullable|string|max:120',
'lavoro_standard' => 'nullable|boolean',
'richiesta_proforma' => 'nullable|boolean',
'proforma_codice' => 'nullable|string|max:80',
'proforma_note' => 'nullable|string|max:2000',
'messaggio_veloce' => 'nullable|string|max:1500',
'qr_token' => 'nullable|string|max:40',
]);
$rapporto = trim((string) $validated['rapporto_fornitore']);
$apparato = $this->buildApparatoSummary(
(string) ($validated['apparato_marca'] ?? ''),
(string) ($validated['apparato_modello'] ?? ''),
(string) ($validated['apparato_seriale'] ?? '')
);
if ($apparato !== '') {
$rapporto .= "\n\n" . $apparato;
}
$payload = [
'rapporto_fornitore' => $rapporto,
'tempo_minuti' => $validated['tempo_minuti'] ?? null,
'terminato_at' => now(),
'stato' => ! empty($validated['lavoro_standard']) ? 'fatturabile' : 'in_verifica',
'eseguito_da_dipendente_id' => $dipendente?->id ?? $intervento->eseguito_da_dipendente_id,
];
$articoli = collect($validated['articoli_utilizzati'] ?? [])
->map(fn($item) => trim((string) $item))
->filter()
->values()
->all();
if ($articoli !== []) {
$payload['articoli_utilizzati'] = $articoli;
}
$fotoFiles = $request->file('foto_lavoro', []);
if (! is_array($fotoFiles)) {
$fotoFiles = $fotoFiles ? [$fotoFiles] : [];
}
if (count($fotoFiles) > 0) {
$payload['foto_path'] = $fotoFiles[0]->store('ticket-interventi/foto/' . $intervento->id, 'public');
}
$docFiles = $request->file('documenti_lavoro', []);
if (! is_array($docFiles)) {
$docFiles = $docFiles ? [$docFiles] : [];
}
$allFiles = array_merge($fotoFiles, $docFiles);
$descriptions = (array) ($validated['descrizione_file'] ?? []);
if (Schema::hasTable('ticket_attachments')) {
foreach ($allFiles as $index => $file) {
if (! $file) {
continue;
}
$path = $file->store('ticket-interventi/allegati/' . $intervento->id, 'public');
TicketAttachment::query()->create([
'ticket_id' => (int) $intervento->ticket_id,
'ticket_update_id' => null,
'user_id' => (int) Auth::id(),
'file_path' => $path,
'original_file_name' => (string) $file->getClientOriginalName(),
'mime_type' => (string) $file->getClientMimeType(),
'size' => (int) $file->getSize(),
'description' => trim((string) ($descriptions[$index] ?? '')) ?: 'Allegato rapporto fornitore',
]);
}
}
$tokenInserito = trim((string) ($validated['qr_token'] ?? ''));
if ($tokenInserito !== '' && strcasecmp($tokenInserito, (string) $intervento->qr_token) === 0) {
$payload['qr_scansionato_at'] = now();
}
$intervento->update($payload);
$stamp = now()->format('d/m/Y H:i');
$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['proforma_codice'] ?? ''));
$proformaNote = trim((string) ($validated['proforma_note'] ?? ''));
if (! empty($validated['richiesta_proforma']) || $proformaCodice !== '' || $proformaNote !== '') {
$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['messaggio_veloce'] ?? ''));
if ($quickMessage !== '') {
$intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => '[' . $stamp . '] Messaggio veloce fornitore: ' . $quickMessage,
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
}
return redirect()->route('fornitore.tickets.show', $intervento)
->with('success', 'Intervento inviato in verifica amministratore.');
}
public function sollecito(TicketIntervento $intervento)
{
[$fornitore] = $this->resolveOperatoreContext();
$this->authorizeIntervento($intervento, $fornitore);
$stamp = now()->format('d/m/Y H:i');
$intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => '[' . $stamp . '] Sollecito fornitore su ticket/intervento in corso.',
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
return back()->with('success', 'Sollecito inviato allo studio amministrativo.');
}
/**
* @return array{0:Fornitore,1:?FornitoreDipendente}
*/
private function resolveOperatoreContext(): array
{
$user = Auth::user();
if ($user && $user->hasAnyRole(['super-admin', 'admin', 'amministratore'])) {
$fornitoreId = (int) request()->query('fornitore', 0);
if ($fornitoreId <= 0) {
$interventoRoute = request()->route('intervento');
if ($interventoRoute instanceof TicketIntervento) {
$fornitoreId = (int) ($interventoRoute->fornitore_id ?? 0);
}
}
if ($fornitoreId > 0) {
$fornitoreAdmin = Fornitore::query()->find($fornitoreId);
abort_unless($fornitoreAdmin, 404);
return [$fornitoreAdmin, null];
}
}
$dipendente = null;
$fornitore = Fornitore::query()
->where('email', (string) $user->email)
->first();
if (! $fornitore && $user) {
$dipendente = FornitoreDipendente::query()
->where('attivo', true)
->where(function ($q) use ($user): void {
$q->where('user_id', (int) $user->id)
->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]);
})
->orderByDesc('id')
->first();
if ($dipendente) {
$fornitore = Fornitore::query()->find((int) $dipendente->fornitore_id);
$dipendente->ultimo_accesso_at = now();
$dipendente->save();
}
}
abort_unless($fornitore, 403, 'Profilo fornitore non collegato.');
return [$fornitore, $dipendente];
}
private function authorizeIntervento(TicketIntervento $intervento, Fornitore $fornitore): void
{
abort_unless((int) $intervento->fornitore_id === (int) $fornitore->id, 403);
}
/**
* @return array{ingresso:string,contatto:string,telefono:string,problema:string,apparato:string}
*/
private function buildInterventoRow(TicketIntervento $intervento): array
{
$descrizione = (string) ($intervento->ticket->descrizione ?? '');
$caller = $this->extractCallerData($descrizione);
return [
'ingresso' => optional($intervento->created_at)->format('d/m/Y H:i') ?: '-',
'contatto' => $caller['contatto'],
'telefono' => $caller['telefono'],
'problema' => $caller['problema'] !== '' ? $caller['problema'] : ((string) ($intervento->ticket->titolo ?? '-')),
'apparato' => $this->extractApparato((string) ($intervento->rapporto_fornitore ?? '')),
];
}
/**
* @return array{contatto:string,telefono:string,problema:string}
*/
private function extractCallerData(string $descrizione): array
{
$contatto = '-';
$telefono = '';
$problema = '';
$lines = preg_split('/\r\n|\r|\n/', $descrizione) ?: [];
foreach ($lines as $line) {
$trim = trim($line);
if ($problema === '' && $trim !== '') {
$problema = $trim;
}
if (str_starts_with($trim, 'Chiamante selezionato:')) {
$contatto = trim((string) str_replace('Chiamante selezionato:', '', $trim));
}
if (str_starts_with($trim, 'Telefono:')) {
$telefono = trim((string) str_replace('Telefono:', '', $trim));
}
}
return [
'contatto' => $contatto,
'telefono' => $telefono,
'problema' => $problema,
];
}
private function extractApparato(string $rapporto): string
{
foreach ((preg_split('/\r\n|\r|\n/', $rapporto) ?: []) as $line) {
$trim = trim($line);
if (str_starts_with(mb_strtolower($trim), 'apparato:')) {
return trim((string) substr($trim, strlen('apparato:')));
}
}
return '-';
}
private function buildApparatoSummary(string $marca, string $modello, string $seriale): string
{
$marca = trim($marca);
$modello = trim($modello);
$seriale = trim($seriale);
if ($marca === '' && $modello === '' && $seriale === '') {
return '';
}
return 'Apparato: '
. ($marca !== '' ? ('marca=' . $marca . ' ') : '')
. ($modello !== '' ? ('modello=' . $modello . ' ') : '')
. ($seriale !== '' ? ('seriale=' . $seriale) : '');
}
}