first commit
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
2025-04-23 00:14:33 +06:00
commit 356f56eebd
197 changed files with 21536 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Events;
use App\Models\Document;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class DocumentVersionUpdated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $document;
/**
* Create a new event instance.
*/
public function __construct(Document $document)
{
$this->document = $document;
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers;
use App\Models\ActivityLog;
use Illuminate\Http\Request;
class ActivityLogController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$logs = ActivityLog::latest()->paginate(10);
return view('activity_log.index', compact('logs'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(ActivityLog $activityLog)
{
return view('activity_log.show', compact('activityLog'));
}
/**
* Show the form for editing the specified resource.
*/
public function edit(ActivityLog $activityLog)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, ActivityLog $activityLog)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(ActivityLog $activityLog)
{
//
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers;
use App\Models\Document;
use App\Notifications\DocumentStatusChanged;
use Illuminate\Http\Request;
class ApprovalController extends Controller
{
//
public function updateStatus(Request $request, Document $document)
{
$validated = $request->validate([
'status' => 'required|in:approved,rejected',
'comment' => 'required_if:status,rejected'
]);
$document->approvals()->create([
'user_id' => auth()->id(),
'status' => $validated['status'],
'comment' => $validated['comment'] ?? null
]);
$document->update(['status' => $validated['status']]);
event(new DocumentStatusChanged($document, $validated['status']));
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Http\Controllers;
use App\Models\ApprovalWorkflow;
use App\Models\Document;
use App\Notifications\ApprovalRequestNotification;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Notification;
class ApprovalWorkflowController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(ApprovalWorkflow $approvalWorkflow)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(ApprovalWorkflow $approvalWorkflow)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, ApprovalWorkflow $approvalWorkflow)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(ApprovalWorkflow $approvalWorkflow)
{
//
}
public function initiateApproval(Document $document)
{
$workflow = $document->project->approvalWorkflow;
$currentStep = $workflow->getCurrentStep($document);
$document->approvals()->create([
'user_id' => auth()->id(),
'status' => 'pending',
'step_index' => 0,
'required_role' => $currentStep['role']
]);
Notification::sendUsersWithRole($currentStep['role'])->notify(
new ApprovalRequestNotification($document, $auth()->user())
);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Foundation\Auth\EmailVerificationRequest;
use Illuminate\Http\RedirectResponse;
class VerifyEmailController extends Controller
{
/**
* Mark the authenticated user's email address as verified.
*/
public function __invoke(EmailVerificationRequest $request): RedirectResponse
{
if ($request->user()->hasVerifiedEmail()) {
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
if ($request->user()->markEmailAsVerified()) {
/** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */
$user = $request->user();
event(new Verified($user));
}
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers;
use App\Models\Document;
use App\Models\User;
use Illuminate\Http\Request;
class CommentController extends Controller
{
public function store(Request $request, Document $document)
{
$comment = $document->comments()->create([
'user_id' => auth()->id(),
'content' => $request->content,
'parent_id' => $request->parent_id
]);
$this->processMentions($comment);
return back();
}
private function processMentions(Comment $comment)
{
preg_match_all('/@([\w\-]+)/', $comment->content, $matches);
foreach ($matches[1] as $username) {
$user = User::where('username', $username)->first();
if ($user) {
$user->notify(new MentionNotification($comment));
}
}
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Project;
use App\Models\Document;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class DashboardController extends Controller
{
public function index()
{
// Estadísticas principales
$stats = [
'projects_count' => Project::count(),
'documents_count' => Document::count(),
'users_count' => User::count(),
'storage_used' => $this->calculateStorageUsed(),
'storage_limit' => 0,
'storage_percentage' => 0,
];
// Documentos recientes (últimos 7 días)
$recentDocuments = Document::with(['project', 'currentVersion'])
->where('created_at', '>=', now()->subDays(7))
->orderBy('created_at', 'desc')
->limit(5)
->get();
// Actividad reciente
$recentActivities = DB::table('activity_log')
->orderBy('created_at', 'desc')
->limit(10)
->get();
return view('dashboard', compact('stats', 'recentDocuments', 'recentActivities'));
}
private function calculateStorageUsed()
{
return Document::with('versions')
->get()
->sum(function($document) {
return $document->versions->sum('size');
});
}
public function storageUsage()
{
$total = $this->calculateStorageUsed();
$limit = config('app.storage_limit', 1073741824); // 1GB por defecto
return response()->json([
'used' => $total,
'limit' => $limit,
'percentage' => ($total / $limit) * 100
]);
}
private function calculateStorage($projects)
{
// Adaptación de tu lógica existente + nueva propuesta
return $projects->sum('storage_used') . ' GB';
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessDocumentOCR;
use App\Models\Document;
use App\Models\Project;
use Illuminate\Http\Request;
class DocumentController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'files.*' => 'required|file|mimes:pdf,docx,xlsx,jpg,png|max:5120',
'project_id' => 'required|exists:projects,id',
'folder_id' => 'nullable|exists:folders,id'
]);
foreach ($request->file('files') as $file) {
$document = Document::create([
'name' => $file->getClientOriginalName(),
'project_id' => $request->project_id,
'folder_id' => $request->folder_id,
'created_by' => auth()->id()
]);
$document->addMedia($file)->toMediaCollection('documents');
ProcessDocumentOCR::dispatch($document->currentVersion);
}
return redirect()->back()->with('success', 'Files uploaded successfully');
}
/**
* Display the specified resource.
*/
public function show(Document $document)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Document $document)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Document $document)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Document $document)
{
//
}
public function upload(Request $request, Project $project)
{
$request->validate([
'files.*' => 'required|mimes:pdf,docx,xlsx,jpeg,png|max:2048'
]);
foreach ($request->file('files') as $file) {
$document = $project->documents()->create([
'name' => $file->getClientOriginalName(),
'status' => 'pending'
]);
$this->createVersion($document, $file);
}
}
private function createVersion(Document $document, $file)
{
$version = $document->versions()->create([
'file_path' => $file->store("projects/{$document->project_id}/documents"),
'hash' => hash_file('sha256', $file),
'user_id' => auth()->id()
]);
$document->update(['current_version_id' => $version->id]);
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers;
use App\Models\Folder;
use Illuminate\Http\Request;
class FolderController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*/
public function show(Folder $folder)
{
//
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Folder $folder)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Folder $folder)
{
//
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Folder $folder)
{
//
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Permission;
class PermissionController extends Controller
{
use AuthorizesRequests; // <-- Añade este trait
public function index()
{
$this->authorize('viewAny', Permission::class);
$permissions = Permission::all()->groupBy('group');
return view('permissions.index', compact('permissions'));
}
public function store(Request $request)
{
$this->authorize('create', Permission::class);
$request->validate([
'name' => 'required|string|max:255|unique:permissions,name',
'group' => 'required|string|max:255'
]);
Permission::create($request->only('name', 'group'));
return redirect()->route('permissions.index')
->with('success', 'Permiso creado exitosamente');
}
public function update(Request $request, Permission $permission)
{
$this->authorize('update', $permission);
$request->validate([
'name' => 'required|string|max:255|unique:permissions,name,'.$permission->id,
'group' => 'required|string|max:255'
]);
$permission->update($request->only('name', 'group'));
return redirect()->route('permissions.index')
->with('success', 'Permiso actualizado correctamente');
}
public function updateRoles(User $user, Request $request)
{
$this->authorize('managePermissions', $user);
// Usar UserPolicy para autorizar
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers;
use App\Models\DocumentVersion;
use Illuminate\Http\Request;
class PreviewController extends Controller
{
public function show(DocumentVersion $version)
{
$filePath = storage_path("app/{$version->file_path}");
return match($version->mime_type) {
'application/pdf' => response()->file($filePath),
'image/*' => response()->file($filePath),
default => response()->file(
$this->convertToPdf($filePath)
)
};
}
private function convertToPdf($filePath)
{
// Usar OnlyOffice o LibreOffice para conversión
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Validation\Rules;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Auth;
class ProfileController extends Controller
{
public function edit()
{
$user = Auth::user();
return view('profile.edit', compact('user'));
}
public function update(Request $request)
{
$user = Auth::user();
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users,email,'.$user->id],
'current_password' => ['nullable', 'required_with:password', 'current_password'],
'password' => ['nullable', 'confirmed', Rules\Password::defaults()],
]);
$user->update([
'name' => $request->name,
'email' => $request->email,
]);
if ($request->filled('password')) {
$user->update([
'password' => Hash::make($request->password)
]);
}
return redirect()->route('profile.edit')
->with('status', 'Perfil actualizado correctamente');
}
public function show(Request $request)
{
return view('profile.show', [
'user' => $request->user()
]);
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request;
class ProjectController extends Controller
{
use AuthorizesRequests; // ← Añadir este trait
/**
* Display a listing of the resource.
*/
public function index()
{
$projects = Project::withCount('documents')
->whereHas('users', function($query) {
$query->where('user_id', auth()->id());
})
->filter(['search' => request('search')])
->paginate(9);
return view('projects.index', compact('projects'));
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
$this->authorize('create projects');
return view('projects.create', [
'categories' => Category::orderBy('name')->get(),
'users' => User::where('id', '!=', auth()->id())->get(),
]);
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'description' => 'required|string',
'status' => 'required|in:active,inactive',
'team' => 'sometimes|array',
'team.*' => 'exists:users,id',
'project_image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048',
'address' => 'nullable|string|max:255',
'province' => 'nullable|string|max:100',
'country' => 'nullable|string|size:2',
'postal_code' => 'nullable|string|max:10',
'latitude' => 'required|numeric|between:-90,90',
'longitude' => 'required|numeric|between:-180,180',
'icon' => 'nullable|in:'.implode(',', config('project.icons')),
'start_date' => 'nullable|date',
'deadline' => 'nullable|date|after:start_date',
'categories' => 'array|exists:categories,id',
//'categories' => 'required|array',
'categories.*' => 'exists:categories,id',
'documents.*' => 'file|max:5120|mimes:pdf,docx,xlsx,jpg,png'
]);
try {
// Combinar datos del usuario autenticado
$validated = array_merge($validated, [
'creator_id' => auth()->id()
]);
// Manejar la imagen
if ($request->hasFile('project_image')) {
$path = $request->file('project_image')->store('project-images', 'public');
$validated['project_image_path'] = $path; // Usar el nombre correcto de columna
}
// Crear el proyecto con todos los datos validados
$project = Project::create($validated);
// Adjuntar categorías
if($request->has('categories')) {
$project->categories()->sync($request->categories);
}
// Manejar documentos adjuntos
if($request->hasFile('documents')) {
foreach ($request->file('documents') as $file) {
$project->documents()->create([
'file_path' => $file->store('project-documents', 'public'),
'original_name' => $file->getClientOriginalName()
]);
}
}
return redirect()->route('projects.show', $project)
->with('success', 'Proyecto creado exitosamente');
} catch (\Exception $e) {
return back()->withInput()
->with('error', 'Error al crear el proyecto: ' . $e->getMessage());
}
}
/**
* Display the specified resource.
*/
public function show(Project $project)
{
$this->authorize('view', $project); // Si usas políticas
$project->load(['categories', 'documents']);
return view('projects.show', [
'project' => $project->load(['rootFolders', 'documents', 'categories']),
'documents' => $project->documents()->paginate(10),
]);
}
/**
* Show the form for editing the specified resource.
*/
public function edit(Project $project)
{
//
}
/**
* Update the specified resource in storage.
*/
public function update(Request $request, Project $project)
{
$this->authorize('update', $project);
// Lógica de actualización
$project->update($request->all());
return redirect()->route('projects.show', $project)->with('success', 'Project updated successfully.');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Project $project)
{
//
}
public function __invoke(Project $project)
{
return view('projects.show', [
'project' => $project
]);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use App\Http\Requests\StoreRoleRequest;
class RoleController extends Controller
{
public function index()
{
$this->authorize('viewAny', Role::class);
$roles = Role::withCount('users')->paginate(10);
return view('roles.index', compact('roles'));
}
public function create()
{
$this->authorize('create', Role::class);
$permissions = Permission::all()->groupBy('group');
return view('roles.create', compact('permissions'));
}
public function store(StoreRoleRequest $request)
{
$role = Role::create($request->only('name'));
$role->syncPermissions($request->permissions);
return redirect()->route('roles.index')
->with('success', 'Rol creado exitosamente');
}
public function edit(Role $role)
{
$this->authorize('update', $role);
$permissions = Permission::all()->groupBy('group');
$rolePermissions = $role->permissions->pluck('id')->toArray();
return view('roles.edit', compact('role', 'permissions', 'rolePermissions'));
}
public function update(StoreRoleRequest $request, Role $role)
{
$role->update($request->only('name'));
$role->syncPermissions($request->permissions);
return redirect()->route('roles.index')
->with('success', 'Rol actualizado correctamente');
}
public function destroy(Role $role)
{
$this->authorize('delete', $role);
if($role->is_protected) {
return redirect()->back()
->with('error', 'No se puede eliminar un rol protegido');
}
$role->delete();
return redirect()->route('roles.index')
->with('success', 'Rol eliminado correctamente');
}
public function syncPermissions(Request $request, Role $role)
{
$this->authorize('update', $role);
$request->validate([
'permissions' => 'required|array',
'permissions.*' => 'exists:permissions,id'
]);
$role->syncPermissions($request->permissions);
return response()->json(['message' => 'Permisos actualizados correctamente']);
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use App\Http\Requests\UpdateUserRequest;
use Spatie\Permission\Models\Role;
class UserController extends Controller
{
public function index()
{
$this->authorize('viewAny', User::class);
$users = User::with('roles')->paginate(10);
return view('users.index', compact('users'));
}
public function create()
{
$this->authorize('create', User::class);
$roles = Role::all();
return view('users.create', compact('roles'));
}
public function store(Request $request)
{
$this->authorize('create', User::class);
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
'roles' => 'array'
]);
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password'])
]);
$user->syncRoles($data['roles'] ?? []);
return redirect()->route('users.index')
->with('success', 'Usuario creado exitosamente');
}
public function edit(User $user)
{
$this->authorize('update', $user);
$roles = Role::all();
$userRoles = $user->roles->pluck('id')->toArray();
return view('users.edit', compact('user', 'roles', 'userRoles'));
}
public function update(UpdateUserRequest $request, User $user)
{
$user->update($request->validated());
$user->syncRoles($request->roles);
return redirect()->route('users.index')
->with('success', 'Usuario actualizado correctamente');
}
public function updatePassword(Request $request, User $user)
{
$this->authorize('update', $user);
$request->validate([
'password' => 'required|min:8|confirmed'
]);
$user->update([
'password' => Hash::make($request->password)
]);
return redirect()->back()
->with('success', 'Contraseña actualizada correctamente');
}
public function destroy(User $user)
{
$this->authorize('delete', $user);
if($user->is_protected) {
return redirect()->back()
->with('error', 'No se puede eliminar un usuario protegido');
}
$user->delete();
return redirect()->route('users.index')
->with('success', 'Usuario eliminado correctamente');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use thiagoalessio\TesseractOCR\TesseractOCR;
class ProcessDocumentOCR implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(public DocumentVersion $version)
{
//
}
/**
* Execute the job.
*/
public function handle()
{
$ocr = new TesseractOCR();
$text = $ocr->file(storage_path("app/{$this->version->file_path}"))->run();
$this->version->update(['ocr_text' => $text]);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessDocumentUpload implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Document $document, public UploadedFile $file)
{
}
public function handle()
{
// Lógica para procesar el archivo
$this->document->createVersion($this->file);
// Generar miniaturas si es imagen
if (Str::startsWith($this->file->getMimeType(), 'image/')) {
$this->document->generateThumbnails();
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Listeners;
use App\Events\DocumentVersionUpdated;
use App\Models\User;
use App\Notifications\DocumentUpdatedNotification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class SendDocumentVersionNotification
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(DocumentVersionUpdated $event): void
{
// Notificar a usuarios relevantes
$users = User::whereHas('roles', function($query) {
$query->whereIn('name', ['admin', 'approver']);
})
->orWhereHas('projects', function($query) use ($event) {
$query->where('id', $event->document->project_id);
})
->get();
foreach ($users as $user) {
$user->notify(new DocumentUpdatedNotification($event->document));
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Livewire\Actions;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
class Logout
{
/**
* Log the current user out of the application.
*/
public function __invoke()
{
Auth::guard('web')->logout();
Session::invalidate();
Session::regenerateToken();
return redirect('/');
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class ApprovalWorkflow extends Component
{
public function render()
{
return view('livewire.approval-workflow');
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class CommentSystem extends Component
{
public function render()
{
return view('livewire.comment-system');
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class DocumentBrowser extends Component
{
public function render()
{
return view('livewire.document-browser');
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Livewire\Folder;
use Livewire\Component;
use App\Models\Folder;
class CreateModal extends Component
{
public $project;
public $parentFolder;
public $folderName = '';
public $showModal = false;
protected $listeners = [
'openCreateFolderModal' => 'openForRoot',
'openCreateSubfolderModal' => 'openForParent'
];
public function openForRoot($projectId)
{
$this->project = Project::find($projectId);
$this->parentFolder = null;
$this->showModal = true;
}
public function openForParent($parentFolderId)
{
$this->parentFolder = Folder::find($parentFolderId);
$this->project = $this->parentFolder->project;
$this->showModal = true;
}
public function createFolder()
{
$this->validate([
'folderName' => 'required|max:255|unique:folders,name'
]);
Folder::create([
'name' => $this->folderName,
'project_id' => $this->project->id,
'parent_id' => $this->parentFolder?->id
]);
$this->reset(['folderName', 'showModal']);
$this->emit('folderCreated');
}
public function render()
{
return view('livewire.folder.create-modal');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Project;
use App\Models\Folder;
use App\Models\Document;
class ProjectShow extends Component
{
public Project $project;
public $selectedFolderId = null;
public $expandedFolders = [];
public function mount(Project $project)
{
$this->project = $project->load('rootFolders');
}
public function selectFolder($folderId)
{
$this->selectedFolderId = $folderId;
}
public function toggleFolder($folderId)
{
if (in_array($folderId, $this->expandedFolders)) {
$this->expandedFolders = array_diff($this->expandedFolders, [$folderId]);
} else {
$this->expandedFolders[] = $folderId;
}
}
public function getDocumentsProperty()
{
return Document::where('folder_id', $this->selectedFolderId)
->where('project_id', $this->project->id)
->with('versions')
->get();
}
public function render()
{
return view('livewire.project-show', [
'rootFolders' => $this->project->rootFolders
]);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ActivityLog extends Model
{
protected $table = 'activity_log';
protected $fillable = [
'log_name',
'description',
'subject_id',
'subject_type',
'causer_id',
'causer_type',
'properties'
];
public function subject()
{
return $this->morphTo();
}
public function causer()
{
return $this->morphTo();
}
}

24
app/Models/Approval.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Approval extends Model
{
protected $casts = [
'metadata' => 'array',
'step' => ApprovalStep::class
];
public function transitionTo($status, $comment = null)
{
$this->update([
'status' => $status,
'comment' => $comment,
'completed_at' => now()
]);
$this->document->notifyApprovers();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ApprovalWorkflow extends Model
{
protected $casts = ['steps' => 'array'];
public function getCurrentStep(Document $document)
{
$lastApproval = $document->approvals()->latest()->first();
return $lastApproval ? $this->steps[$lastApproval->step_index + 1] : $this->steps[0];
}
}

17
app/Models/Category.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Category extends Model
{
protected $fillable = ['name', 'slug'];
public function projects(): BelongsToMany
{
return $this->belongsToMany(Project::class);
}
}

75
app/Models/Document.php Normal file
View File

@@ -0,0 +1,75 @@
<?php
namespace App\Models;
use App\Events\DocumentVersionUpdated;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
class Document extends Model
{
use LogsActivity;
protected static $logAttributes = ['name', 'status'];
protected static $logOnlyDirty = true;
protected $fillable = [
'name',
'status',
'project_id',
'folder_id',
'current_version_id'
];
public function versions() {
return $this->hasMany(DocumentVersion::class);
}
public function approvals() {
return $this->hasMany(Approval::class);
}
public function comments() {
return $this->hasMany(Comment::class)->whereNull('parent_id');
}
public function createVersion($file)
{
return $this->versions()->create([
'file_path' => $file->store("documents/{$this->id}/versions"),
'hash' => hash_file('sha256', $file),
'user_id' => auth()->id(),
'version_number' => $this->versions()->count() + 1
]);
}
public function getCurrentVersionAttribute()
{
return $this->versions()->latest()->first();
}
public function uploadVersion($file)
{
$this->versions()->create([
'file_path' => $file->store("projects/{$this->id}/versions"),
'hash' => hash_file('sha256', $file),
'version' => $this->versions()->count() + 1,
'user_id' => auth()->id()
]);
event(new DocumentVersionUpdated($this));
return $version;
}
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logExcept(['current_version_id'])
->logUnguarded();
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DocumentVersion extends Model
{
//
}

43
app/Models/Folder.php Normal file
View File

@@ -0,0 +1,43 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Folder extends Model
{
protected $fillable = [
'name',
'parent_id',
'project_id',
'icon',
'color',
'description',
];
public function descendants()
{
return $this->belongsToMany(Folder::class, 'folder_closure', 'ancestor_id', 'descendant_id')
->withPivot('depth');
}
public function documents()
{
return $this->hasMany(Document::class);
}
public function parent()
{
return $this->belongsTo(Folder::class, 'parent_id');
}
public function children()
{
return $this->hasMany(Folder::class, 'parent_id')->with('children');
}
public function project()
{
return $this->belongsTo(Project::class);
}
}

80
app/Models/Project.php Normal file
View File

@@ -0,0 +1,80 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
class Project extends Model
{
protected $fillable = [
'name',
'description',
'creator_id',
'status',
'project_image_path',
'address',
'province',
'country',
'postal_code',
'latitude',
'longitude',
'icon',
'start_date',
'deadline',
// Agrega cualquier otro campo nuevo aquí
];
public function folders() {
return $this->hasMany(Folder::class);
}
public function documents() {
return $this->hasMany(Document::class);
}
public function rootFolders()
{
return $this->folders()->whereNull('parent_id')->with('children');
}
public function creator()
{
return $this->belongsTo(User::class, 'creator_id');
}
public function managers()
{
return $this->belongsToMany(User::class, 'project_managers');
}
public function users()
{
return $this->belongsToMany(User::class, 'project_users');
}
public function scopeFilter(Builder $query, array $filters)
{
$query->when($filters['search'] ?? false, function($query, $search) {
$query->where(function($query) use ($search) {
$query->where('name', 'like', "%{$search}%")
->orWhere('description', 'like', "%{$search}%");
});
});
// Agrega más filtros según necesites
/*
$query->when($filters['status'] ?? false, function($query, $status) {
$query->where('status', $status);
});
*/
}
public function categories()
{
return $this->belongsToMany(Category::class);
}
}

63
app/Models/User.php Normal file
View File

@@ -0,0 +1,63 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
use HasRoles;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_protected' => 'boolean',
];
}
/**
* Get the user's initials
*/
public function initials(): string
{
return Str::of($this->name)
->explode(' ')
->map(fn (string $name) => Str::of($name)->substr(0, 1))
->implode('');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Notifications;
use App\Models\Document;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ApprovalRequestNotification extends Notification
{
use Queueable;
protected $document;
protected $requester;
/**
* Create a new notification instance.
*/
public function __construct(Document $document, User $requester)
{
$this->document = $document;
$this->requester = $requester;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Solicitud de aprobación de documento')
->greeting('Hola ' . $notifiable->name . '!')
->line($this->requester->name . ' ha solicitado tu aprobación para el documento:')
->line('**Documento:** ' . $this->document->name)
->action('Revisar Documento', route('documents.show', $this->document))
->line('Fecha límite: ' . $this->document->due_date->format('d/m/Y'))
->line('Gracias por usar nuestro sistema!');
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'type' => 'approval-request',
'document_id' => $this->document->id,
'requester_id' => $this->requester->id,
'message' => 'Nueva solicitud de aprobación para: ' . $this->document->name,
'link' => route('documents.show', $this->document)
];
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class DocumentStatusChanged extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct(public Document $document, public string $action)
{
//
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['database', 'mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject("Document status changed: {$this->document->name}")
->line("The document '{$this->document->name}' has been {$this->action}.")
->action('View Document', route('documents.show', $this->document));
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Notifications;
use App\Models\Document;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class DocumentUpdatedNotification extends Notification implements ShouldQueue
{
use Queueable;
protected $document;
/**
* Create a new notification instance.
*/
public function __construct(Document $document)
{
$this->document = $document;
}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail', 'database'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
return (new MailMessage)
->subject('Nueva versión de documento: ' . $this->document->name)
->line('Se ha subido una nueva versión del documento.')
->action('Ver Documento', route('documents.show', $this->document))
->line('Gracias por usar nuestro sistema!');
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
'document_id' => $this->document->id,
'message' => 'Nueva versión del documento: ' . $this->document->name,
'url' => route('documents.show', $this->document)
];
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Policies;
use App\Models\User;
class DashboardPolicy
{
/**
* Create a new policy instance.
*/
public function __construct()
{
//
}
public function view(User $user)
{
return $user->hasPermissionTo('view dashboard');
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Policies;
use App\Models\Document;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class DocumentPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return false;
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Document $document)
{
return $user->hasPermissionTo('view documents')
&& $user->hasProjectAccess($document->project_id);
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return false;
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Document $document): bool
{
return false;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Document $document): bool
{
return false;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Document $document): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Document $document): bool
{
return false;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Policies;
use App\Models\Permission;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Spatie\Permission\Models\Permission;
class PermissionPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('view permissions');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Permission $permission): bool
{
return $user->hasPermissionTo('view permissions');
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('create permissions');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Permission $permission): bool
{
if($permission->is_system) return false;
return $user->hasPermissionTo('edit permissions');
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Permission $permission): bool
{
if($permission->is_system || $permission->roles()->exists()) {
return false;
}
return $user->hasPermissionTo('delete permissions');
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Permission $permission): bool
{
return $user->hasPermissionTo('manage permissions');
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Permission $permission): bool
{
return $user->hasPermissionTo('manage permissions');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Policies;
use App\Models\User;
class ProfilePolicy
{
/**
* Create a new policy instance.
*/
public function __construct()
{
//
}
public function update(User $user)
{
return $user->is(auth()->user());
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Policies;
use App\Models\Project;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class ProjectPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('view projects');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Project $project): bool
{
return $user->hasPermissionTo('view projects') &&
$this->hasProjectAccess($user, $project);
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('create projects');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Project $project): bool
{
return $user->hasPermissionTo('edit projects') &&
$this->hasProjectAccess($user, $project);
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Project $project): bool
{
return $user->hasPermissionTo('delete projects') &&
$this->hasProjectAccess($user, $project);
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Project $project): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Project $project): bool
{
return false;
}
protected function hasProjectAccess(User $user, Project $project)
{
// Verificar si el usuario es creador, gestor o tiene acceso directo
return $project->creator_id === $user->id ||
$project->managers->contains($user->id) ||
$project->users->contains($user->id);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Policies;
use App\Models\Role;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class RolePolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('manage roles');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, Role $role): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('manage roles');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, Role $role): bool
{
return $user->hasPermissionTo('manage roles') && !$role->is_protected;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, Role $role): bool
{
return $user->hasPermissionTo('manage roles') && !$role->is_protected;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, Role $role): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, Role $role): bool
{
return false;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\Response;
class UserPolicy
{
/**
* Determine whether the user can view any models.
*/
public function viewAny(User $user): bool
{
return $user->hasPermissionTo('manage users');
}
/**
* Determine whether the user can view the model.
*/
public function view(User $user, User $model): bool
{
return false;
}
/**
* Determine whether the user can create models.
*/
public function create(User $user): bool
{
return $user->hasPermissionTo('manage users');
}
/**
* Determine whether the user can update the model.
*/
public function update(User $user, User $model): bool
{
return $user->hasPermissionTo('manage users') && !$model->is_protected;
}
/**
* Determine whether the user can delete the model.
*/
public function delete(User $user, User $model): bool
{
return $user->hasPermissionTo('manage users')
&& !$model->is_protected
&& $user->id !== $model->id;
}
/**
* Determine whether the user can restore the model.
*/
public function restore(User $user, User $model): bool
{
return false;
}
/**
* Determine whether the user can permanently delete the model.
*/
public function forceDelete(User $user, User $model): bool
{
return false;
}
public function managePermissions(User $user)
{
// recomendada: return $authUser->isAdmin() && !$targetUser->isSuperAdmin();
return $user->hasRole('admin'); // Solo los admins pueden gestionar permisos
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Providers;
use App\Models\Document;
use App\Models\Project;
use App\Models\User;
use App\Policies\DocumentPolicy;
use App\Policies\PermissionPolicy;
use App\Policies\ProfilePolicy;
use App\Policies\ProjectPolicy;
use App\Policies\RolePolicy;
use App\Policies\UserPolicy;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class AppServiceProvider extends ServiceProvider
{
protected $policies = [
User::class => UserPolicy::class,
User::class => ProfilePolicy::class,
Role::class => RolePolicy::class,
Permission::class => PermissionPolicy::class,
Document::class => DocumentPolicy::class,
Project::class => ProjectPolicy::class,
];
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
Blade::componentNamespace('App\\View\\Components', 'icons');
Blade::component('multiselect', \App\View\Components\Multiselect::class);
Livewire::component('project-show', \App\Http\Livewire\ProjectShow::class);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
//use Illuminate\Support\ServiceProvider;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
// Agrega tus eventos aquí
\App\Events\DocumentVersionUpdated::class => [
\App\Listeners\SendDocumentVersionNotification::class,
],
];
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
parent::boot();
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Livewire\Volt\Volt;
class VoltServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Volt::mount([
config('livewire.view_path', resource_path('views/livewire')),
resource_path('views/pages'),
]);
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class ActivityIcon extends Component
{
public $type;
/**
* Create a new component instance.
*/
public function __construct($type)
{
$this->type = $type;
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.activity-icon');
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class FolderItem extends Component
{
public $folder;
public $level;
/**
* Create a new component instance.
*/
public function __construct($folder, $level = 0)
{
$this->folder = $folder;
$this->level = $level;
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.folder-item');
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\View\Components;
use Closure;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;
class Multiselect extends Component
{
public $options;
public $selected;
public $name;
public $id;
/**
* Create a new component instance.
*/
public function __construct($options = [], $selected = [], $name = 'multiselect', $id = null)
{
$this->options = $options;
$this->selected = $selected;
$this->name = $name;
$this->id = $id ?? 'multiselect-' . uniqid();
}
/**
* Get the view / contents that represent the component.
*/
public function render(): View|Closure|string
{
return view('components.multiselect');
}
}