repository_id) { return 'local'; } if ($documento && $documento->storage_disk) { return $documento->storage_disk; } return AppSetting::getDocumentiStorageDisk(); } public function index(Request $request) { $this->authorizeRead('documenti'); $folderId = $request->get('folder_id'); $repoId = $request->get('repository_id'); $query = Documento::with(['user', 'target', 'cartella', 'repository']) ->orderByDesc('created_at'); if ($repoId) { $query->where('repository_id', $repoId); } elseif ($folderId) { $query->where('cartella_id', $folderId); } else { $query->whereNull('repository_id'); } $documenti = $query->paginate(20); $cartelle = DocumentoCartella::with('children') ->whereNull('parent_id') ->orderBy('nome') ->get(); $currentFolder = $folderId ? DocumentoCartella::find($folderId) : null; $breadcrumb = $currentFolder ? $currentFolder->ancestors() : []; $sottoCartelle = $folderId ? DocumentoCartella::where('parent_id', $currentFolder->id)->orderBy('nome')->get() : DocumentoCartella::whereNull('parent_id')->orderBy('nome')->get(); $currentRepo = $repoId ? StorageRepository::find($repoId) : null; $repositories = StorageRepository::attivi()->get(); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get(); $eventi = Evento::orderBy('nome_evento')->get(); $mailingLists = \App\Models\MailingList::orderBy('nome')->get(); $tipologie = TipologiaDocumento::attive(); $cartelleMoveOptions = DocumentoCartella::whereNull('parent_id') ->with('children') ->orderBy('nome') ->get() ->map(fn($f) => [ 'id' => $f->id, 'nome' => $f->nome, 'children' => $f->children->map(fn($c) => ['id' => $c->id, 'nome' => $c->nome]), ]); return view('documenti.index', compact( 'documenti', 'cartelle', 'currentFolder', 'breadcrumb', 'sottoCartelle', 'currentRepo', 'repositories', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie', 'cartelleMoveOptions' )); } public function create() { $this->authorizeWrite('documenti'); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get(); $eventi = Evento::orderBy('nome_evento')->get(); $tipologie = TipologiaDocumento::attive(); return view('documenti.create', compact('individui', 'gruppi', 'eventi', 'tipologie')); } public function edit($documento) { $this->authorizeWrite('documenti'); $documento = Documento::with('target')->findOrFail($documento); $individui = Individuo::orderBy('cognome')->orderBy('nome')->get(); $gruppi = Gruppo::orderBy('nome')->get(); $eventi = Evento::orderBy('nome_evento')->get(); $mailingLists = \App\Models\MailingList::orderBy('nome')->get(); $tipologie = TipologiaDocumento::attive(); return view('documenti.edit', compact('documento', 'individui', 'gruppi', 'eventi', 'mailingLists', 'tipologie')); } public function options(Request $request) { $tipo = $request->get('tipo'); $options = ''; switch ($tipo) { case 'individuo': $items = Individuo::orderBy('cognome')->orderBy('nome')->get(); foreach ($items as $item) { $options .= ''; } break; case 'gruppo': $items = Gruppo::orderBy('nome')->get(); foreach ($items as $item) { $options .= ''; } break; case 'evento': $items = Evento::orderBy('nome_evento')->get(); foreach ($items as $item) { $options .= ''; } break; case 'mailing': $items = \App\Models\MailingList::orderBy('nome')->get(); foreach ($items as $item) { $options .= ''; } break; } return response($options); } public function update(Request $request, $documento) { $this->authorizeWrite('documenti'); $documento = Documento::findOrFail($documento); $tipologieValidi = TipologiaDocumento::opzioni(); $tipologieRule = 'in:' . implode(',', $tipologieValidi); $data = $request->validate([ 'nome_file' => 'required|string|max:255', 'tipologia' => 'required|' . $tipologieRule, 'visibilita' => 'nullable|string', 'visibilita_target_id' => 'nullable|integer', 'visibilita_target_type' => 'nullable|string', 'note' => 'nullable|string', ]); $data['visibilita'] = $request->contesto_tipo ?: 'pubblico'; if ($request->contesto_tipo) { $typeMap = [ 'individuo' => Individuo::class, 'gruppo' => Gruppo::class, 'evento' => Evento::class, 'mailing' => \App\Models\MailingList::class, ]; $data['visibilita_target_type'] = $typeMap[$request->contesto_tipo] ?? null; } else { $data['visibilita'] = 'pubblico'; $data['visibilita_target_id'] = null; $data['visibilita_target_type'] = null; } $documento->update($data); return redirect('/documenti')->with('success', 'Documento aggiornato.'); } public function store(Request $request) { $this->authorizeWrite('documenti'); $data = $request->validate([ 'nome_file' => 'required|string|max:255', 'tipologia' => 'required|in:avatar,galleria,documento,statuto,altro', 'file' => 'required|file|max:10240', 'visibilita' => 'required|in:pubblico,individuo,gruppo', 'visibilita_target_id' => 'nullable|integer', 'visibilita_target_type' => 'nullable|string', 'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id', 'repository_id' => 'nullable|integer|exists:storage_repositories,id', ]); $repositoryId = $data['repository_id'] ?? null; if ($repositoryId) { $repo = StorageRepository::findOrFail($repositoryId); $filesystem = $this->repoService->buildFilesystem($repo); if (!$filesystem) { return back()->with('error', 'Impossibile connettersi al repository remoto.'); } $extension = $request->file('file')->getClientOriginalExtension(); $filename = $data['nome_file'] . '.' . ($extension ?: $request->file('file')->guessExtension()); $path = 'documents/' . date('Y/m/d') . '/' . $filename; $stream = fopen($request->file('file')->getRealPath(), 'rb'); $filesystem->writeStream($path, $stream); fclose($stream); $filePath = $path; } else { $disk = $this->resolveStorageDisk(); $storagePath = AppSetting::getDocumentiStoragePath(); $filePath = $request->file('file')->store($storagePath, $disk); } $visibilitaTargetType = null; if ($data['visibilita'] === 'individuo' && $data['visibilita_target_id']) { $visibilitaTargetType = Individuo::class; } elseif ($data['visibilita'] === 'gruppo' && $data['visibilita_target_id']) { $visibilitaTargetType = Gruppo::class; } Documento::create([ 'nome_file' => $data['nome_file'], 'file_path' => $filePath, 'storage_disk' => $disk ?? null, 'tipo' => 'upload', 'tipologia' => $data['tipologia'], 'visibilita' => $data['visibilita'], 'visibilita_target_id' => $data['visibilita_target_id'] ?? null, 'visibilita_target_type' => $visibilitaTargetType, 'mime_type' => $request->file('file')->getMimeType(), 'dimensione' => $request->file('file')->getSize(), 'user_id' => auth()->id(), 'cartella_id' => $data['cartella_id'] ?? null, 'repository_id' => $repositoryId, ]); $redirect = $request->_redirect ?? url()->previous(); return redirect($redirect)->with('success', 'Documento caricato.'); } public function download($documento) { $this->authorizeRead('documenti'); $documento = Documento::findOrFail($documento); $extension = pathinfo($documento->file_path, PATHINFO_EXTENSION); $filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file; if ($documento->repository_id) { $repo = StorageRepository::find($documento->repository_id); if (!$repo) { abort(404); } $filesystem = $this->repoService->buildFilesystem($repo); if (!$filesystem || !$filesystem->fileExists($documento->file_path)) { abort(404); } $stream = $filesystem->readStream($documento->file_path); if (!$stream) { abort(404); } return response()->stream(function () use ($stream) { fpassthru($stream); fclose($stream); }, 200, [ 'Content-Type' => $documento->mime_type ?: 'application/octet-stream', 'Content-Disposition' => 'attachment; filename="' . $filename . '"', 'Content-Length' => $documento->dimensione, ]); } $disk = $this->resolveStorageDisk($documento); if (!Storage::disk($disk)->exists($documento->file_path)) { abort(404); } return Storage::disk($disk)->download( $documento->file_path, $filename, ['Content-Type' => $documento->mime_type] ); } public function preview($documento) { $this->authorizeRead('documenti'); $documento = Documento::findOrFail($documento); $previewableMimeTypes = [ 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'application/pdf', 'text/plain', 'text/html', 'text/csv', ]; if (!in_array($documento->mime_type, $previewableMimeTypes)) { return response()->view('documenti.preview-fallback', [ 'documento' => $documento, ])->header('X-Preview-Fallback', 'true'); } if ($documento->repository_id) { $repo = StorageRepository::find($documento->repository_id); if (!$repo) { abort(404); } $filesystem = $this->repoService->buildFilesystem($repo); if (!$filesystem || !$filesystem->fileExists($documento->file_path)) { abort(404); } $stream = $filesystem->readStream($documento->file_path); if (!$stream) { abort(404); } return response()->stream(function () use ($stream) { fpassthru($stream); fclose($stream); }, 200, [ 'Content-Type' => $documento->mime_type, 'Content-Disposition' => 'inline; filename="' . basename($documento->file_path) . '"', ]); } $disk = $this->resolveStorageDisk($documento); if (!Storage::disk($disk)->exists($documento->file_path)) { abort(404); } $fullPath = Storage::disk($disk)->path($documento->file_path); return response()->file($fullPath, ['Content-Type' => $documento->mime_type]); } public function destroy($documento) { $this->authorizeDelete('documenti'); $documento = Documento::findOrFail($documento); if ($documento->repository_id) { $repo = StorageRepository::find($documento->repository_id); if ($repo) { $filesystem = $this->repoService->buildFilesystem($repo); if ($filesystem && $filesystem->fileExists($documento->file_path)) { $filesystem->delete($documento->file_path); } } } else { $disk = $this->resolveStorageDisk($documento); if (Storage::disk($disk)->exists($documento->file_path)) { Storage::disk($disk)->delete($documento->file_path); } } $documento->delete(); $redirect = request('_redirect', url()->previous()); return redirect($redirect)->with('success', 'Documento eliminato.'); } public function checkLinks($documento) { $documento = Documento::findOrFail($documento); $links = []; if (in_array($documento->visibilita, ['individuo', 'gruppo', 'evento']) && $documento->visibilita_target_id) { if ($documento->visibilita === 'individuo') { $target = Individuo::find($documento->visibilita_target_id); if ($target) { $links[] = [ 'type' => 'individuo', 'name' => $target->cognome . ' ' . $target->nome, 'url' => '/individui/' . $target->id, ]; } } elseif ($documento->visibilita === 'gruppo') { $target = Gruppo::find($documento->visibilita_target_id); if ($target) { $links[] = [ 'type' => 'gruppo', 'name' => $target->nome, 'url' => '/gruppi/' . $target->id, ]; } } elseif ($documento->visibilita === 'evento') { $target = Evento::find($documento->visibilita_target_id); if ($target) { $links[] = [ 'type' => 'evento', 'name' => $target->nome_evento, 'url' => '/eventi/' . $target->id, ]; } } } if ($documento->tipologia === 'avatar') { $linkedToAvatar = Individuo::whereHas('avatar', function($q) use ($documento) { $q->where('id', $documento->id); })->count(); if ($linkedToAvatar > 0) { $links[] = [ 'type' => 'avatar', 'name' => 'Avatar di ' . $linkedToAvatar . ' individuo(i)', 'url' => null, ]; } } if ($documento->eventi()->count() > 0) { foreach ($documento->eventi as $evento) { $links[] = [ 'type' => 'evento', 'name' => 'Evento: ' . $evento->nome_evento, 'url' => '/eventi/' . $evento->id, ]; } } return response()->json([ 'has_links' => count($links) > 0, 'links' => $links, ]); } public function checkMassLinks(Request $request) { $idsInput = $request->input('ids', ''); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); if (empty($ids)) { return response()->json(['linked_count' => 0, 'links' => []]); } $documenti = Documento::whereIn('id', $ids)->get(); $allLinks = []; $linkedCount = 0; foreach ($documenti as $documento) { $isLinked = false; $linkName = ''; $linkUrl = null; if (in_array($documento->visibilita, ['individuo', 'gruppo', 'evento']) && $documento->visibilita_target_id) { $isLinked = true; $linkName = $documento->nome_file . ' → '; if ($documento->visibilita === 'individuo') { $target = Individuo::find($documento->visibilita_target_id); $linkName .= $target ? $target->cognome . ' ' . $target->nome : 'Individuo #' . $documento->visibilita_target_id; $linkUrl = $target ? '/individui/' . $target->id : null; } elseif ($documento->visibilita === 'gruppo') { $target = Gruppo::find($documento->visibilita_target_id); $linkName .= $target ? $target->nome : 'Gruppo #' . $documento->visibilita_target_id; $linkUrl = $target ? '/gruppi/' . $target->id : null; } elseif ($documento->visibilita === 'evento') { $target = Evento::find($documento->visibilita_target_id); $linkName .= $target ? $target->nome_evento : 'Evento #' . $documento->visibilita_target_id; $linkUrl = $target ? '/eventi/' . $target->id : null; } } if ($documento->tipologia === 'avatar') { $linkedToAvatar = Individuo::whereHas('avatar', function($q) use ($documento) { $q->where('id', $documento->id); })->count(); if ($linkedToAvatar > 0) { $isLinked = true; $allLinks[] = [ 'name' => $documento->nome_file . ' → Avatar di ' . $linkedToAvatar . ' individuo(i)', 'url' => null, ]; $linkedCount++; } } if ($documento->eventi()->count() > 0) { $eventi = $documento->eventi; foreach ($eventi as $evento) { $allLinks[] = [ 'name' => $documento->nome_file . ' → Evento: ' . $evento->nome_evento, 'url' => '/eventi/' . $evento->id, ]; $linkedCount++; } } if ($isLinked && $documento->tipologia !== 'avatar' && $linkName) { $allLinks[] = [ 'name' => $linkName, 'url' => $linkUrl, ]; $linkedCount++; } } return response()->json([ 'linked_count' => $linkedCount, 'links' => $allLinks, ]); } public function massDownload(Request $request) { $this->authorizeRead('documenti'); $idsInput = $request->input('ids', ''); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); if (empty($ids)) { return back()->with('error', 'Nessun documento selezionato.'); } $documenti = Documento::whereIn('id', $ids)->get(); if ($documenti->isEmpty()) { return back()->with('error', 'Nessun documento trovato.'); } $zip = new \ZipArchive(); $zipPath = tempnam(sys_get_temp_dir(), 'docs_') . '.zip'; if ($zip->open($zipPath, \ZipArchive::CREATE) !== true) { return back()->with('error', 'Impossibile creare l\'archivio ZIP.'); } $addedCount = 0; foreach ($documenti as $documento) { $extension = pathinfo($documento->file_path, PATHINFO_EXTENSION); $filename = $extension ? $documento->nome_file . '.' . $extension : $documento->nome_file; try { if ($documento->repository_id) { $repo = StorageRepository::find($documento->repository_id); if ($repo) { $filesystem = $this->repoService->buildFilesystem($repo); if ($filesystem && $filesystem->fileExists($documento->file_path)) { $stream = $filesystem->readStream($documento->file_path); if ($stream) { $zip->addFromString($filename, stream_get_contents($stream)); fclose($stream); $addedCount++; } } } } else { $disk = $this->resolveStorageDisk($documento); if (Storage::disk($disk)->exists($documento->file_path)) { $zip->addFile(Storage::disk($disk)->path($documento->file_path), $filename); $addedCount++; } } } catch (\Exception $e) { \Illuminate\Support\Facades\Log::warning('ZIP download skipped file: ' . $documento->id . ' - ' . $e->getMessage()); } } $zip->close(); if ($addedCount === 0) { unlink($zipPath); return back()->with('error', 'Nessun file disponibile per il download.'); } return response()->download($zipPath, 'documenti.zip')->deleteFileAfterSend(true); } public function massDestroy(Request $request) { $this->authorizeDelete('documenti'); $idsInput = $request->input('ids', ''); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); if (empty($ids)) { return back()->with('error', 'Nessun documento selezionato.'); } $documenti = Documento::whereIn('id', $ids)->get(); foreach ($documenti as $documento) { if ($documento->repository_id) { $repo = StorageRepository::find($documento->repository_id); if ($repo) { $filesystem = $this->repoService->buildFilesystem($repo); if ($filesystem && $filesystem->fileExists($documento->file_path)) { $filesystem->delete($documento->file_path); } } } else { $disk = $this->resolveStorageDisk($documento); if (Storage::disk($disk)->exists($documento->file_path)) { Storage::disk($disk)->delete($documento->file_path); } } $documento->delete(); } return back()->with('success', count($ids) . ' documenti eliminati.'); } public function massUpdate(Request $request) { $this->authorizeWrite('documenti'); $idsInput = $request->input('ids', ''); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); if (empty($ids)) { return back()->with('error', 'Nessun documento selezionato.'); } $data = $request->validate([ 'tipologia' => 'nullable|string', 'visibilita' => 'nullable|string', 'visibilita_target_id' => 'nullable|integer', 'visibilita_target_type' => 'nullable|string', ]); $tipologia = $data['tipologia'] ?? null; $visibilita = $data['visibilita'] ?? null; if (empty($tipologia) && empty($visibilita)) { return back()->with('error', 'Specificare almeno un campo da aggiornare.'); } $updateData = []; if (!empty($tipologia)) { $updateData['tipologia'] = $tipologia; } if (!empty($visibilita)) { $updateData['visibilita'] = $visibilita; $typeMap = [ 'individuo' => Individuo::class, 'gruppo' => Gruppo::class, 'evento' => Evento::class, 'mailing' => \App\Models\MailingList::class, ]; if (isset($typeMap[$visibilita])) { $updateData['visibilita_target_type'] = $typeMap[$visibilita]; } } if (!empty($data['visibilita_target_id']) && !empty($data['visibilita_target_type'])) { $typeMap = [ 'individuo' => Individuo::class, 'gruppo' => Gruppo::class, 'evento' => Evento::class, 'mailing' => \App\Models\MailingList::class, ]; $targetType = $data['visibilita_target_type']; if (!isset($typeMap[$targetType])) { return back()->with('error', 'Tipo destinazione non valido.'); } $updateData['visibilita'] = $targetType; $updateData['visibilita_target_id'] = (int) $data['visibilita_target_id']; $updateData['visibilita_target_type'] = $typeMap[$targetType]; } Documento::whereIn('id', $ids)->update($updateData); return back()->with('success', count($ids) . ' documenti aggiornati.'); } public function massAssociate(Request $request) { $this->authorizeWrite('documenti'); $idsInput = $request->input('ids', ''); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); if (empty($ids)) { return back()->with('error', 'Nessun documento selezionato.'); } $data = $request->validate([ 'target_type' => 'required|in:individuo,gruppo,evento,mailing', 'target_id' => 'required|integer', ]); $targetType = match($data['target_type']) { 'individuo' => Individuo::class, 'gruppo' => Gruppo::class, 'evento' => Evento::class, 'mailing' => \App\Models\MailingList::class, }; Documento::whereIn('id', $ids)->update([ 'visibilita' => $data['target_type'], 'visibilita_target_id' => $data['target_id'], 'visibilita_target_type' => $targetType, ]); return back()->with('success', count($ids) . ' documenti associati.'); } public function massMove(Request $request): RedirectResponse { $this->authorizeWrite('documenti'); $idsInput = $request->input('ids', ''); $ids = is_array($idsInput) ? $idsInput : (is_string($idsInput) ? explode(',', $idsInput) : []); if (empty($ids)) { return back()->with('error', 'Nessun documento selezionato.'); } $data = $request->validate([ 'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id', ]); Documento::whereIn('id', $ids)->update([ 'cartella_id' => $data['cartella_id'] ?? null, ]); return back()->with('success', count($ids) . ' documenti spostati.'); } public function moveDocumento(Request $request, int $id): JsonResponse { $this->authorizeWrite('documenti'); $documento = Documento::findOrFail($id); $data = $request->validate([ 'cartella_id' => 'nullable|integer|exists:documenti_cartelle,id', ]); $documento->update(['cartella_id' => $data['cartella_id'] ?? null]); return response()->json(['success' => true]); } }