|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 $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 */ private function documentiColumns(): array { if ($this->documentiColumnsCache === null) { $this->documentiColumnsCache = Schema::hasTable('documenti') ? Schema::getColumnListing('documenti') : []; } return $this->documentiColumnsCache; } /** * @param array $payload * @return array */ private function filterDocumentoPayload(array $payload): array { $allowed = array_flip($this->documentiColumns()); return array_intersect_key($payload, $allowed); } }