321 lines
13 KiB
PHP
Executable File
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);
|
|
}
|
|
|
|
}
|