netgescon-day0/app/Http/Controllers/Condomino/TicketController.php

321 lines
13 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Condomino;
use App\Http\Controllers\Condomino\Concerns\ResolvesCondominoAccess;
use App\Http\Controllers\Controller;
use App\Models\CategoriaTicket;
use App\Models\Documento;
use App\Models\Ticket;
use App\Models\TicketAttachment;
use App\Models\TicketMessage;
use App\Models\User;
use App\Services\Documenti\ImageTextExtractionService;
use App\Services\Documenti\PdfTextExtractionService;
use App\Services\Support\TicketAttachmentUploadService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
class TicketController extends Controller
{
use ResolvesCondominoAccess;
/** @var array<int, string>|null */
private ?array $documentiColumnsCache = null;
public function index()
{
$user = Auth::user();
$soggettoId = $this->resolveSoggettoId($user);
$tickets = Ticket::where('soggetto_richiedente_id', $soggettoId)
->with(['stabile', 'categoriaTicket', 'unitaImmobiliare'])
->orderBy('created_at', 'desc')
->paginate(10);
return view('condomino.tickets.index', compact('tickets'));
}
public function create()
{
$user = Auth::user();
$unitaImmobiliari = $this->resolveUserUnita($user);
$categorieTicket = CategoriaTicket::all();
return view('condomino.tickets.create', compact('unitaImmobiliari', 'categorieTicket'));
}
public function store(Request $request)
{
$user = Auth::user();
$soggettoId = $this->resolveSoggettoId($user);
if (! $soggettoId) {
return back()->withErrors([
'ticket' => 'Non e stato possibile identificare il nominativo utente autenticato.',
])->withInput();
}
$request->validate([
'unita_immobiliare_id' => 'required|integer',
'categoria_ticket_id' => 'nullable|exists:categorie_ticket,id',
'titolo' => 'required|string|max:255',
'descrizione' => 'required|string',
'luogo_intervento' => 'nullable|string|max:255',
'priorita' => 'required|in:Bassa,Media,Alta,Urgente',
'allegati.*' => 'nullable|file|max:20480', // 20MB per file
'attachment_descriptions' => 'nullable|array',
'attachment_descriptions.*' => 'nullable|string|max:255',
]);
$unitaImmobiliare = $this->resolveUserUnita($user)
->firstWhere('id', (int) $request->unita_immobiliare_id);
if (! $unitaImmobiliare) {
return back()->withErrors([
'unita_immobiliare_id' => 'Unita immobiliare non accessibile per l\'utente autenticato.',
])->withInput();
}
$ticket = Ticket::create([
'stabile_id' => $unitaImmobiliare->stabile_id,
'unita_immobiliare_id' => (int) $unitaImmobiliare->id,
'soggetto_richiedente_id' => $soggettoId,
'categoria_ticket_id' => $request->categoria_ticket_id,
'aperto_da_user_id' => $user->id,
'titolo' => $request->titolo,
'descrizione' => $request->descrizione,
'luogo_intervento' => $request->luogo_intervento,
'data_apertura' => now(),
'stato' => 'Aperto',
'priorita' => $request->priorita,
]);
// Gestione allegati
if ($request->hasFile('allegati')) {
$supportsAttachmentMetadata = Schema::hasColumn('ticket_attachments', 'metadata');
$attachmentDescriptions = (array) $request->input('attachment_descriptions', []);
foreach ($request->file('allegati') as $index => $file) {
if (! $file) {
continue;
}
$displayName = $this->buildStoredAttachmentDisplayName($ticket, (int) $index, (string) $file->getClientOriginalName());
$stored = app(TicketAttachmentUploadService::class)->store($file, 'ticket-attachments/' . $ticket->id, [
'stored_basename' => pathinfo($displayName, PATHINFO_FILENAME),
'display_name' => $displayName,
]);
$attachmentPayload = [
'ticket_update_id' => null,
'user_id' => $user->id,
'file_path' => $stored['path'],
'original_file_name' => $stored['original_name'],
'mime_type' => $stored['mime'],
'size' => $stored['size'],
'description' => $this->resolveAttachmentDescription(
(string) ($attachmentDescriptions[$index] ?? ''),
$stored,
),
];
if ($supportsAttachmentMetadata) {
$attachmentPayload['metadata'] = is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [];
}
$attachment = $ticket->attachments()->create($attachmentPayload);
if ($attachment instanceof TicketAttachment) {
$this->archiveTicketAttachmentDocument($ticket, $attachment);
}
}
}
TicketMessage::query()->create([
'ticket_id' => (int) $ticket->id,
'user_id' => $user?->id,
'messaggio' => 'Ticket creato da area condomino/inquilino.',
]);
return redirect()->route('condomino.tickets.index')
->with('success', 'Ticket creato con successo.');
}
public function show(Ticket $ticket)
{
$user = Auth::user();
$soggettoId = $this->resolveSoggettoId($user);
// Verifica che il ticket appartenga all'utente
if (! $soggettoId || (int) $ticket->soggetto_richiedente_id !== (int) $soggettoId) {
abort(403);
}
$ticket->load(['stabile', 'categoriaTicket', 'unitaImmobiliare', 'attachments']);
return view('condomino.tickets.show', compact('ticket'));
}
private function buildStoredAttachmentDisplayName(Ticket $ticket, int $index, string $originalName): string
{
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
return sprintf(
'ticket-%06d-all-%02d%s',
(int) $ticket->id,
$index + 1,
$extension !== '' ? '.' . $extension : ''
);
}
/**
* @param array<string, mixed> $stored
*/
private function resolveAttachmentDescription(string $specificDescription, array $stored): string
{
$specificDescription = trim($specificDescription);
if ($specificDescription !== '') {
return $specificDescription;
}
$metadata = is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [];
$parts = ['Upload da area condomino'];
$lat = $metadata['gps_decimal']['lat'] ?? null;
$lng = $metadata['gps_decimal']['lng'] ?? null;
if (is_numeric($lat) && is_numeric($lng)) {
$parts[] = 'GPS: ' . number_format((float) $lat, 5, '.', '') . ',' . number_format((float) $lng, 5, '.', '');
}
$mapsUrl = trim((string) ($metadata['google_maps_url'] ?? ''));
if ($mapsUrl !== '') {
$parts[] = 'Maps: ' . $mapsUrl;
}
$exifDate = trim((string) ($metadata['exif_datetime'] ?? ''));
if ($exifDate !== '') {
$parts[] = 'EXIF: ' . $exifDate;
}
$device = trim((string) ($metadata['device'] ?? ''));
if ($device !== '') {
$parts[] = 'Device: ' . $device;
}
$displayName = trim((string) ($stored['original_name'] ?? ''));
if ($displayName !== '') {
$parts[] = 'File: ' . $displayName;
}
$sourceName = trim((string) ($stored['source_original_name'] ?? ''));
if ($sourceName !== '' && $sourceName !== $displayName) {
$parts[] = 'Orig: ' . $sourceName;
}
return mb_substr(implode(' | ', array_filter($parts)), 0, 255);
}
private function archiveTicketAttachmentDocument(Ticket $ticket, TicketAttachment $attachment): void
{
if (! Schema::hasTable('documenti')) {
return;
}
$publicPath = (string) ($attachment->file_path ?? '');
if ($publicPath === '' || ! Storage::disk('public')->exists($publicPath)) {
return;
}
$localPath = Storage::disk('public')->path($publicPath);
$contents = (string) Storage::disk('public')->get($publicPath);
$extension = strtolower((string) pathinfo($localPath, PATHINFO_EXTENSION));
$archiveReference = 'ticket-' . (int) $ticket->id . '-attachment-' . (int) $attachment->id;
$attachmentMeta = is_array($attachment->metadata ?? null) ? $attachment->metadata : [];
$payload = [
'documentable_type' => Ticket::class,
'documentable_id' => (int) $ticket->id,
'stabile_id' => (int) $ticket->stabile_id,
'utente_id' => Auth::id(),
'nome' => (string) ($attachment->original_file_name ?: ('Allegato ticket #' . (int) $attachment->id)),
'nome_file' => (string) ($attachment->original_file_name ?: basename($localPath)),
'path_file' => $localPath,
'percorso_file' => $localPath,
'tipo_documento' => 'ticket_allegato',
'tipologia' => $attachment->isImage() ? 'foto' : ($attachment->isPdf() ? 'comunicazione' : 'altro'),
'mime_type' => $attachment->resolvedMimeType(),
'descrizione' => (string) ($attachment->description ?: 'Allegato ticket #' . (int) $ticket->id),
'note' => 'Ticket #' . (int) $ticket->id . ' · ' . (string) ($ticket->titolo ?: 'senza titolo'),
'dimensione_file' => (int) ($attachment->size ?? strlen($contents)),
'estensione' => $extension !== '' ? $extension : null,
'hash_file' => hash('sha256', $contents),
'xml_data' => [
'ticket_id' => (int) $ticket->id,
'ticket_attachment_id' => (int) $attachment->id,
'source_public_path' => $publicPath,
'archive_reference' => $archiveReference,
'archive_scope' => 'ticket',
'visibility_scope' => 'interno',
'attachment_metadata' => $attachmentMeta,
],
'metadati_ocr' => $attachment->isImage()
? ['status' => 'pending_image_ocr', 'source' => 'ticket_attachment']
: ['source' => 'ticket_attachment'],
'data_upload' => now(),
'approvato' => true,
'archiviato' => true,
'urgente' => false,
'is_demo' => false,
'archive_reference' => $archiveReference,
'visibility_scope' => 'interno',
'visibility_groups' => ['supporto', 'amministrazione', 'stabile:' . (int) $ticket->stabile_id],
'visibility_roles' => ['super-admin', 'admin', 'amministratore', 'collaboratore'],
];
$documento = Documento::query()->updateOrCreate(
[
'documentable_type' => Ticket::class,
'documentable_id' => (int) $ticket->id,
'path_file' => $localPath,
],
$this->filterDocumentoPayload($payload)
);
if ($attachment->isPdf()) {
app(PdfTextExtractionService::class)->extractAndStore($documento, false);
} elseif ($attachment->isImage()) {
app(ImageTextExtractionService::class)->extractAndStore($documento, false);
}
}
/**
* @return array<int, string>
*/
private function documentiColumns(): array
{
if ($this->documentiColumnsCache === null) {
$this->documentiColumnsCache = Schema::hasTable('documenti')
? Schema::getColumnListing('documenti')
: [];
}
return $this->documentiColumnsCache;
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
private function filterDocumentoPayload(array $payload): array
{
$allowed = array_flip($this->documentiColumns());
return array_intersect_key($payload, $allowed);
}
}