search) { $query->where('title', 'like', "%{$search}%"); } if ($categoryId = $request->category_id) { $query->where('category_id', $categoryId); } if ($projectId = $request->project_id) { $query->where('project_id', $projectId); } if ($status = $request->status) { $query->where('status', $status); } if ($type = $request->documentable_type) { $query->where('documentable_type', $type); } $documents = $query->latest()->paginate(20)->withQueryString(); $categories = DocumentCategory::all(); $projects = Project::select('id', 'name')->get(); return Inertia::render('DocumentManagement::Documents/Index', [ 'documents' => $documents, 'categories' => $categories, 'projects' => $projects, 'filters' => $request->only(['search', 'category_id', 'project_id', 'status', 'documentable_type']), ]); } public function upload(Request $request) { $validated = $request->validate([ 'title' => 'required|string|max:255', 'category_id' => 'required|exists:document_categories,id', 'project_id' => 'nullable|exists:projects,id', 'description' => 'nullable|string', 'documentable_type' => 'nullable|string', 'documentable_id' => 'nullable|integer', 'file' => 'required|file|max:51200|mimes:pdf', ]); $file = $request->file('file'); $category = DocumentCategory::find($validated['category_id']); $path = $file->store("documents/" . str()->slug($category->name), 'local'); $document = Document::create([ 'title' => $validated['title'], 'category_id' => $validated['category_id'], 'project_id' => $validated['project_id'] ?? null, 'description' => $validated['description'] ?? null, 'documentable_type' => $validated['documentable_type'] ?? null, 'documentable_id' => $validated['documentable_id'] ?? null, 'status' => 'Approved', 'uploaded_by' => $request->user()->id, 'current_file_path' => $path, 'current_file_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'file_size' => $file->getSize(), 'version_count' => 1, ]); // Create initial version $document->versions()->create([ 'version_number' => 1, 'file_path' => $path, 'file_name' => $file->getClientOriginalName(), 'mime_type' => $file->getMimeType(), 'file_size' => $file->getSize(), 'uploaded_by' => $request->user()->id, 'change_notes' => 'Initial upload', ]); return back()->with('success', 'Document uploaded successfully.'); } public function uploadVersion(Request $request, Document $document) { $validated = $request->validate([ 'file' => 'required|file|max:51200|mimes:pdf', 'change_notes' => 'nullable|string|max:500', ]); $file = $request->file('file'); $category = $document->category; $path = $file->store("documents/" . str()->slug($category->name), 'local'); $document->addVersion( $path, $file->getClientOriginalName(), $file->getMimeType(), $file->getSize(), $request->user()->id, $validated['change_notes'] ?? null, ); // Keep status as Approved when a new version is uploaded $document->update(['status' => 'Approved']); return back()->with('success', 'New document version uploaded successfully.'); } public function download(Document $document) { $disk = $document->current_file_path ? $this->resolveDisk($document->current_file_path) : null; if (!$disk) { return back()->with('error', 'File not found.'); } return $disk->download($document->current_file_path, $document->current_file_name); } public function preview(Document $document) { $disk = $document->current_file_path ? $this->resolveDisk($document->current_file_path) : null; if (!$disk) { abort(404, 'File not found.'); } $mimeType = $document->mime_type ?: $disk->mimeType($document->current_file_path); $extension = strtolower(pathinfo($document->current_file_name ?: $document->current_file_path, PATHINFO_EXTENSION)); $isDocx = $extension === 'docx' || $mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; if ($isDocx) { return $this->renderDocxPreview($disk->path($document->current_file_path), $document->current_file_name ?: 'document.docx'); } if (!in_array($mimeType, ['application/pdf'], true) && !str_starts_with((string) $mimeType, 'image/')) { abort(404, 'This file type does not support inline preview.'); } return response()->file($disk->path($document->current_file_path), [ 'Content-Type' => $mimeType, 'Content-Disposition' => 'inline; filename="' . addcslashes($document->current_file_name ?: 'document', '"\\') . '"', ]); } private function renderDocxPreview(string $path, string $fileName) { if (!class_exists(\ZipArchive::class)) { abort(501, 'DOCX preview requires the PHP ZIP extension.'); } $archive = new \ZipArchive(); if ($archive->open($path) !== true) { abort(422, 'The DOCX file could not be opened.'); } $documentXml = $archive->getFromName('word/document.xml'); $archive->close(); if (!$documentXml) { abort(422, 'The DOCX document content could not be read.'); } $xml = new \DOMDocument(); $xml->loadXML($documentXml, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); $xpath = new \DOMXPath($xml); $content = []; $extractText = static function (\DOMNode $node) use ($xpath): string { $text = ''; foreach ($xpath->query('.//*[local-name()="t"]', $node) as $textNode) { $text .= $textNode->textContent; } return trim($text); }; $body = $xpath->query('//*[local-name()="body"]')->item(0); foreach ($body?->childNodes ?? [] as $block) { if ($block->localName === 'p') { $text = $extractText($block); if ($text !== '') { $content[] = '

' . e($text) . '

'; } continue; } if ($block->localName === 'tbl') { $rowsHtml = []; foreach ($xpath->query('./*[local-name()="tr"]', $block) as $row) { $cellsHtml = []; foreach ($xpath->query('./*[local-name()="tc"]', $row) as $cell) { $cellsHtml[] = '' . e($extractText($cell)) . ''; } if ($cellsHtml !== []) { $rowsHtml[] = '' . implode('', $cellsHtml) . ''; } } if ($rowsHtml !== []) { $content[] = '' . implode('', $rowsHtml) . '
'; } } } $html = '' . e($fileName) . '' . '' . '' . implode('', $content) . ''; return response($html, 200, [ 'Content-Type' => 'text/html; charset=UTF-8', 'Content-Disposition' => 'inline; filename="' . addcslashes(pathinfo($fileName, PATHINFO_FILENAME) . '.html', '"\\') . '"', ]); } private function resolveDisk(string $path) { foreach (['local', 'public'] as $diskName) { $disk = Storage::disk($diskName); if ($disk->exists($path)) { return $disk; } } return null; } public function downloadVersion(Document $document, int $versionId) { $version = $document->versions()->findOrFail($versionId); $disk = $this->resolveDisk($version->file_path); if (!$disk) { return back()->with('error', 'Version file not found.'); } return $disk->download($version->file_path, $version->file_name); } public function versions(Document $document) { $document->load(['versions.uploader:id,name', 'uploader:id,name', 'approvals.approver', 'category']); return Inertia::render('DocumentManagement::Documents/Versions', [ 'document' => $document, ]); } public function destroy(Document $document) { $user = auth()->user(); // Platform / Executive roles can delete any document $isExecutive = is_null($user?->contractor_id) || in_array($user?->user_type, ['admin', 'super_admin']) || $user?->hasAnyRole(['Super Admin', 'Admin', 'Project Manager', 'Executive']); if (!$isExecutive) { // Contractors can delete documents uploaded by anyone in their contractor company $uploaderContractorId = $document->uploader?->contractor_id; $isOwnCompany = ((int)$document->uploaded_by === (int)$user->id) || ($user->contractor_id && $uploaderContractorId !== null && (int)$uploaderContractorId === (int)$user->contractor_id); abort_unless($isOwnCompany, 403, 'Contractors can only delete documents uploaded by their company.'); } // Delete all version files foreach ($document->versions as $version) { $disk = $this->resolveDisk($version->file_path); $disk?->delete($version->file_path); } if ($document->current_file_path) { $disk = $this->resolveDisk($document->current_file_path); $disk?->delete($document->current_file_path); } $document->delete(); return back()->with('success', 'Document deleted.'); } }