diff --git a/README.md b/README.md index beac658..e6746f9 100644 --- a/README.md +++ b/README.md @@ -57,4 +57,4 @@ php artisan migrate --seed php artisan storage:link # Start server -php artisan serve \ No newline at end of file +composer run dev \ No newline at end of file diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 8677cd5..cbeb956 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,7 +2,11 @@ namespace App\Http\Controllers; +use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Foundation\Validation\ValidatesRequests; +use Illuminate\Routing\Controller as BaseController; + abstract class Controller { - // + use AuthorizesRequests, ValidatesRequests; // <-- Traits esenciales } diff --git a/app/Http/Controllers/FolderController.php b/app/Http/Controllers/FolderController.php index 20cc408..e8c4670 100644 --- a/app/Http/Controllers/FolderController.php +++ b/app/Http/Controllers/FolderController.php @@ -4,6 +4,10 @@ namespace App\Http\Controllers; use App\Models\Folder; use Illuminate\Http\Request; +use Illuminate\Http\Response; +use Illuminate\Support\Facades\Validator; +use App\Rules\UniqueFolderName; +use Illuminate\Support\Facades\Gate; class FolderController extends Controller { @@ -50,9 +54,51 @@ class FolderController extends Controller /** * Update the specified resource in storage. */ - public function update(Request $request, Folder $folder) + public function update(Folder $folder, Request $request) { - // + try { + // Verificar permisos + if (!Gate::allows('update', $folder)) { + return response()->json([ + 'success' => false, + 'message' => 'No tienes permisos para modificar esta carpeta' + ], Response::HTTP_FORBIDDEN); + } + + // Validación + $validator = Validator::make($request->all(), [ + 'name' => [ + 'required', + 'max:255', + new UniqueFolderName( + $folder->project_id, + $folder->parent_id + ) + ] + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + // Actualizar nombre + $folder->update(['name' => $request->name]); + + return response()->json([ + 'success' => true, + 'message' => 'Carpeta actualizada', + 'folder' => $folder + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error al actualizar carpeta: ' . $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } } /** @@ -60,6 +106,91 @@ class FolderController extends Controller */ public function destroy(Folder $folder) { - // + try { + // Verificar permisos + if (!Gate::allows('delete', $folder)) { + return response()->json([ + 'success' => false, + 'message' => 'No tienes permisos para eliminar esta carpeta' + ], Response::HTTP_FORBIDDEN); + } + + // Validar que esté vacía + if ($folder->documents()->exists() || $folder->children()->exists()) { + return response()->json([ + 'success' => false, + 'message' => 'No puedes eliminar carpetas con contenido' + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + // Eliminar + $folder->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Carpeta eliminada' + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error al eliminar carpeta: ' . $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } + } + + /** + * Move the specified folder to a new location. + */ + public function move(Folder $folder, Request $request) + { + try { + // Verificar permisos + if (!Gate::allows('move', $folder)) { + return response()->json([ + 'success' => false, + 'message' => 'No tienes permisos para esta acción' + ], Response::HTTP_FORBIDDEN); + } + + // Validación + $validator = Validator::make($request->all(), [ + 'parent_id' => 'nullable|exists:folders,id', + 'project_id' => 'required|exists:projects,id' + ]); + + if ($validator->fails()) { + return response()->json([ + 'success' => false, + 'errors' => $validator->errors() + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + // Prevenir movimiento a sí mismo o descendientes + if ($request->parent_id && $folder->isDescendantOf($request->parent_id)) { + return response()->json([ + 'success' => false, + 'message' => 'No puedes mover una carpeta a su propia jerarquía' + ], Response::HTTP_UNPROCESSABLE_ENTITY); + } + + // Actualizar ubicación + $folder->update([ + 'parent_id' => $request->parent_id, + 'project_id' => $request->project_id + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Carpeta movida exitosamente', + 'folder' => $folder->fresh() + ]); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error al mover la carpeta: ' . $e->getMessage() + ], Response::HTTP_INTERNAL_SERVER_ERROR); + } } } diff --git a/app/Http/Controllers/GroupController.php b/app/Http/Controllers/GroupController.php new file mode 100644 index 0000000..46712a6 --- /dev/null +++ b/app/Http/Controllers/GroupController.php @@ -0,0 +1,65 @@ +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'); + $this->authorize('create roles'); + $permissions = Permission::all(['id', 'name']); return view('roles.create', compact('permissions')); } - public function store(StoreRoleRequest $request) + public function store(Request $request) { - $role = Role::create($request->only('name')); + /*$role = Role::create($request->only('name')); $role->syncPermissions($request->permissions); return redirect()->route('roles.index') - ->with('success', 'Rol creado exitosamente'); + ->with('success', 'Rol creado exitosamente');*/ + + $this->authorize('create', Role::class); + + $request->validate([ + 'name' => 'required|unique:roles', + 'description' => 'required' + ]); + + Role::create($request->all()); + + return redirect()->route('roles.index'); } public function edit(Role $role) @@ -41,7 +54,7 @@ class RoleController extends Controller return view('roles.edit', compact('role', 'permissions', 'rolePermissions')); } - public function update(StoreRoleRequest $request, Role $role) + public function update(Request $request, Role $role) { $role->update($request->only('name')); $role->syncPermissions($request->permissions); diff --git a/app/Http/Middleware/CheckResourcePermissions.php b/app/Http/Middleware/CheckResourcePermissions.php new file mode 100644 index 0000000..e727a56 --- /dev/null +++ b/app/Http/Middleware/CheckResourcePermissions.php @@ -0,0 +1,27 @@ +route()->parameter('project') + ?? $request->route()->parameter('folder'); + + if (!$request->user()->hasPermissionToResource($resource, $permission)) { + abort(403); + } + + return $next($request); + } +} diff --git a/app/Livewire/FileUpload.php b/app/Livewire/FileUpload.php new file mode 100644 index 0000000..492659c --- /dev/null +++ b/app/Livewire/FileUpload.php @@ -0,0 +1,82 @@ + 'updateFolder']; + + public function updateFolder($folderId) + { + $this->folder = Folder::find($folderId); + } + + public function updatedFiles() + { + $this->validate([ + 'files.*' => "max:{$this->maxSize}1024|mimes:pdf,docx,xlsx,jpg,png,svg", + 'folderId' => 'required|exists:folders,id' + ]); + + $this->previews = []; + foreach ($this->files as $file) { + $this->previews[] = [ + 'name' => $file->getClientOriginalName(), + 'type' => $file->getMimeType(), + 'size' => $file->getSize(), + 'preview' => str_starts_with($file->getMimeType(), 'image/') + ? $file->temporaryUrl() + : null + ]; + + Document::create([ + 'name' => $file->getClientOriginalName(), + 'file_path' => $file->store("projects/{$this->folderId}"), + 'folder_id' => $this->folderId + ]); + } + } + + public function save() + { + $this->validate([ + 'files.*' => "required|max:{$this->maxSize}1024|mimes:pdf,docx,xlsx,jpg,png,svg" + ]); + + foreach ($this->files as $file) { + $path = $file->store("projects/{$this->project->id}/documents", 'public'); + + $this->project->documents()->create([ + 'name' => $file->getClientOriginalName(), + 'file_path' => $path, + 'folder_id' => $this->folder?->id, + 'size' => $file->getSize(), + 'type' => $file->getMimeType() + ]); + } + + $this->reset(['files', 'previews']); + $this->emit('documentsUpdated'); + } + + public function render() + { + return view('livewire.file-upload'); + } +} diff --git a/app/Livewire/GroupSearch.php b/app/Livewire/GroupSearch.php new file mode 100644 index 0000000..77716b1 --- /dev/null +++ b/app/Livewire/GroupSearch.php @@ -0,0 +1,13 @@ + 'required|exists:projects,id', + 'selectedFolder' => 'nullable|exists:folders,id', + 'selectedPermissions' => 'required|array|min:1', + 'selectedUser' => 'required_if:assignTo,user', + 'selectedGroup' => 'required_if:assignTo,group', + ]; + } + + public function getSelectedResourceIdProperty() + { + return $this->selectedFolder ?: $this->selectedProject; + } + + public function getCanSaveProperty() + { + return $this->selectedProject && + ($this->assignTo === 'group' ? $this->selectedGroup : $this->selectedUser) && + count($this->selectedPermissions) > 0; + } + + public function getProjectsProperty() + { + return Project::accessibleBy(auth()->user())->get(); + } + + public function getFoldersProperty() + { + if (!$this->selectedProject) return collect(); + + return Folder::where('project_id', $this->selectedProject) + ->withDepth() + ->get() + ->toTree(); + } + + public function getPermissionsProperty() + { + $type = $this->selectedFolder ? 'folder' : 'project'; + return config("permissions.types.$type", []); + } + + public function userSelected($userId) + { + $this->selectedUser = $userId; + } + + public function groupSelected($groupId) + { + $this->selectedGroup = $groupId; + } + + public function savePermissions() + { + $this->validate(); + + $model = $this->assignTo === 'user' + ? User::find($this->selectedUser) + : Group::find($this->selectedGroup); + + $resource = $this->selectedFolder + ? Folder::find($this->selectedFolder) + : Project::find($this->selectedProject); + + // Asignar permisos usando Spatie + $model->givePermissionTo( + $resource->permissions()->whereIn('name', $this->selectedPermissions)->get() + ); + + $this->reset(['selectedPermissions']); + $this->dispatch('permissionsUpdated'); + session()->flash('message', 'Permisos actualizados correctamente.'); + } + + public function render() + { + return view('livewire.permission-manager'); + } +} diff --git a/app/Livewire/PermissionsList.php b/app/Livewire/PermissionsList.php new file mode 100644 index 0000000..8f0f04a --- /dev/null +++ b/app/Livewire/PermissionsList.php @@ -0,0 +1,59 @@ +resourceId = $resourceId; + $this->determineResourceType(); + } + + protected function determineResourceType() + { + if (Project::find($this->resourceId)) { + $this->resourceType = 'project'; + } else { + $this->resourceType = 'folder'; + } + } + + public function getPermissions() + { + return Permission::where('name', 'like', "{$this->resourceType}-{$this->resourceId}-%") + ->get() + ->groupBy(function ($permission) { + return explode('-', $permission->name)[2]; // Obtener tipo de permiso + }); + } + + public function revokePermission($permissionId, $modelType, $modelId) + { + $permission = Permission::findOrFail($permissionId); + + $model = $modelType === 'user' + ? User::find($modelId) + : Group::find($modelId); + + $model->revokePermissionTo($permission); + + $this->dispatch('permissionsUpdated'); + } + + public function render() + { + return view('livewire.permissions-list', [ + 'permissions' => $this->getPermissions(), + 'users' => User::withPermissionsForResource($this->resourceId, $this->resourceType)->get(), + 'groups' => Group::withPermissionsForResource($this->resourceId, $this->resourceType)->get() + ]); + } +} diff --git a/app/Livewire/ProjectShow.php b/app/Livewire/ProjectShow.php index 7bde869..90f8a66 100644 --- a/app/Livewire/ProjectShow.php +++ b/app/Livewire/ProjectShow.php @@ -3,28 +3,40 @@ namespace App\Livewire; use Livewire\Component; +use Livewire\Attributes\Title; +use Livewire\WithFileUploads; use App\Models\Project; use App\Models\Folder; use App\Models\Document; +use Illuminate\Validation\Rule; class ProjectShow extends Component { + use WithFileUploads; + public Project $project; - public $selectedFolderId = null; + public ?Folder $currentFolder = null; public $expandedFolders = []; + public $files = []; + public $folderName = ''; + + public $selectedFolderId = null; + public function mount(Project $project) { $this->project = $project->load('rootFolders'); + $this->currentFolder = $this->project->rootFolders->first() ?? null; } public function selectFolder($folderId) { $this->selectedFolderId = $folderId; + $this->currentFolder = Folder::with('children')->find($folderId); } - public function toggleFolder($folderId) + public function toggleFolder($folderId): void { if (in_array($folderId, $this->expandedFolders)) { $this->expandedFolders = array_diff($this->expandedFolders, [$folderId]); @@ -33,18 +45,82 @@ class ProjectShow extends Component } } + public function createFolder(): void + { + $this->validate([ + 'folderName' => [ + 'required', + 'max:255', + Rule::unique('folders', 'name')->where(function ($query) { + return $query->where('project_id', $this->project->id) + ->where('parent_id', $this->currentFolder?->id); + }) + ] + ]); + + Folder::create([ + 'name' => $this->folderName, + 'project_id' => $this->project->id, + 'parent_id' => $this->currentFolder?->id + ]); + + $this->reset('folderName'); + $this->project->load('rootFolders'); // Recargar carpetas raíz + if ($this->currentFolder) { + $this->currentFolder->load('children'); // Recargar hijos si está en una subcarpeta + } + $this->project->refresh(); + } + + public function uploadFiles(): void + { + $this->validate([ + 'files.*' => 'file|max:10240|mimes:pdf,docx,xlsx,jpg,png' + ]); + + foreach ($this->files as $file) { + Document::create([ + 'name' => $file->getClientOriginalName(), + 'file_path' => $file->store("projects/{$this->project->id}/documents"), + 'project_id' => $this->project->id, + 'folder_id' => $this->currentFolder?->id + ]); + } + + $this->reset('files'); + if ($this->currentFolder) { + $this->currentFolder->refresh(); // Recargar documentos + } + $this->reset('files'); + } + public function getDocumentsProperty() { - return Document::where('folder_id', $this->selectedFolderId) - ->where('project_id', $this->project->id) - ->with('versions') - ->get(); + return $this->currentFolder + ? $this->currentFolder->documents()->with('versions')->get() + : Document::whereNull('folder_id')->where('project_id', $this->project->id)->with('versions')->get(); + } + + public function getBreadcrumbsProperty() + { + if (!$this->currentFolder) return collect(); + + $breadcrumbs = collect(); + $folder = $this->currentFolder; + + while ($folder) { + $breadcrumbs->prepend($folder); + $folder = $folder->parent; + } + + return $breadcrumbs; } public function render() { - return view('livewire.project-show', [ - 'rootFolders' => $this->project->rootFolders - ]); + return view('livewire.project-show') + ->layout('layouts.livewire-app', [ + 'title' => $this->project->name + ]); } } diff --git a/app/Livewire/Toolbar.php b/app/Livewire/Toolbar.php new file mode 100644 index 0000000..3a385d6 --- /dev/null +++ b/app/Livewire/Toolbar.php @@ -0,0 +1,25 @@ +project = $project; + $this->currentFolder = $currentFolder; + } + + public function render() + { + return view('livewire.toolbar'); + } +} diff --git a/app/Livewire/UserSearch.php b/app/Livewire/UserSearch.php new file mode 100644 index 0000000..2cdf34d --- /dev/null +++ b/app/Livewire/UserSearch.php @@ -0,0 +1,28 @@ +selectedUser = $userId; + $this->emitUp('userSelected', $userId); + } + + public function render() + { + $users = User::query() + ->when($this->search, fn($q) => $q->where('name', 'like', "%{$this->search}%")) + ->limit(5) + ->get(); + + return view('livewire.user-search', compact('users')); + } +} diff --git a/app/Models/Group.php b/app/Models/Group.php new file mode 100644 index 0000000..09658db --- /dev/null +++ b/app/Models/Group.php @@ -0,0 +1,176 @@ +belongsToMany(User::class) + ->withTimestamps() + ->using(GroupUser::class); + } + + /** + * Relationship: Permissions assigned to this group + */ + public function permissions(): BelongsToMany + { + return $this->belongsToMany( + config('permission.models.permission'), + config('permission.table_names.group_has_permissions'), + 'group_id', + 'permission_id' + ); + } + + /** + * Scope: Groups with specific permission + */ + public function scopeWithPermission(Builder $query, $permission): Builder + { + return $query->whereHas('permissions', function ($q) use ($permission) { + $q->where('name', $permission); + }); + } + + /** + * Scope: Groups with permissions on specific resource + */ + public function scopeWithResourcePermissions(Builder $query, $resourceId, $resourceType): Builder + { + return $query->whereHas('permissions', function ($q) use ($resourceId, $resourceType) { + $q->where('name', 'like', "{$resourceType}-{$resourceId}-%"); + }); + } + + /** + * Assign permission to group + */ + public function assignPermission($permission): self + { + if (is_string($permission)) { + $permission = Permission::findByName($permission); + } + + $this->permissions()->syncWithoutDetaching($permission); + + return $this; + } + + /** + * Revoke permission from group + */ + public function revokePermission($permission): self + { + if (is_string($permission)) { + $permission = Permission::findByName($permission); + } + + $this->permissions()->detach($permission); + + return $this; + } + + /** + * Sync all permissions + */ + public function syncPermissions($permissions): self + { + $permissionIds = collect($permissions)->map(function ($perm) { + return is_string($perm) ? Permission::findByName($perm)->id : $perm->id; + }); + + $this->permissions()->sync($permissionIds); + + return $this; + } + + /** + * Check if group has permission + */ + public function hasPermission($permission): bool + { + if (is_string($permission)) { + return $this->permissions->contains('name', $permission); + } + + return $this->permissions->contains('id', $permission->id); + } + + /** + * Get all users with permissions through this group + */ + public function getUsersWithPermissionsAttribute() + { + return User::whereHas('groups', function ($query) { + $query->where('groups.id', $this->id); + })->get(); + } + + /** + * Get permission names attribute + */ + public function getPermissionNamesAttribute() + { + return $this->permissions->pluck('name'); + } + + /** + * Validation rules + */ + public static function validationRules($groupId = null): array + { + return [ + 'name' => 'required|string|max:255|unique:groups,name,'.$groupId, + 'description' => 'nullable|string|max:500', + 'permissions' => 'array', + 'permissions.*' => 'exists:permissions,id', + 'users' => 'array', + 'users.*' => 'exists:users,id' + ]; + } + + /** + * The "booting" method of the model + */ + protected static function boot() + { + parent::boot(); + + static::deleting(function ($group) { + if ($group->isForceDeleting()) { + $group->users()->detach(); + $group->permissions()->detach(); + } + }); + } +} diff --git a/app/Models/Project.php b/app/Models/Project.php index 40d72c4..f0c80b0 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -29,6 +29,11 @@ class Project extends Model return $this->hasMany(Folder::class); } + public function currentFolder() + { + return $this->belongsTo(Folder::class); + } + public function documents() { return $this->hasMany(Document::class); } diff --git a/app/Models/User.php b/app/Models/User.php index e8a556d..4d18b93 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,6 +4,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Str; @@ -12,8 +13,7 @@ use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; - use HasRoles; + use HasFactory, Notifiable, HasRoles, SoftDeletes; /** * The attributes that are mass assignable. @@ -60,4 +60,34 @@ class User extends Authenticatable ->map(fn (string $name) => Str::of($name)->substr(0, 1)) ->implode(''); } + + public function groups() + { + return $this->belongsToMany(Group::class) + ->withTimestamps() + ->using(GroupUser::class); + } + + public function hasPermissionThroughGroup($permission) + { + return $this->groups->flatMap(function ($group) { + return $group->permissions; + })->contains('name', $permission); + } + + public function getAllPermissionsAttribute() + { + return $this->getAllPermissions() + ->merge($this->groups->flatMap->permissions) + ->unique('id'); + } + + public function hasAnyPermission($permissions): bool + { + return $this->hasPermissionTo($permissions) || + $this->groups->contains(function ($group) use ($permissions) { + return $group->hasAnyPermission($permissions); + }); + } + } diff --git a/app/Policies/DocumentPolicy.php b/app/Policies/DocumentPolicy.php index 44bb6d1..2c04a8b 100644 --- a/app/Policies/DocumentPolicy.php +++ b/app/Policies/DocumentPolicy.php @@ -4,10 +4,13 @@ namespace App\Policies; use App\Models\Document; use App\Models\User; +use Illuminate\Auth\Access\HandlesAuthorization; use Illuminate\Auth\Access\Response; class DocumentPolicy { + use HandlesAuthorization; + /** * Determine whether the user can view any models. */ @@ -22,7 +25,8 @@ class DocumentPolicy public function view(User $user, Document $document) { return $user->hasPermissionTo('view documents') - && $user->hasProjectAccess($document->project_id); + && $user->hasProjectAccess($document->project_id) + && $user->hasPermissionToResource($document->resource(), 'view'); } /** @@ -38,7 +42,7 @@ class DocumentPolicy */ public function update(User $user, Document $document): bool { - return false; + return $user->hasPermissionToResource($document->resource(), 'edit'); } /** @@ -46,7 +50,7 @@ class DocumentPolicy */ public function delete(User $user, Document $document): bool { - return false; + return $user->hasPermissionTo('delete documents'); } /** diff --git a/app/Policies/FolderPolicy.php b/app/Policies/FolderPolicy.php new file mode 100644 index 0000000..bec6d85 --- /dev/null +++ b/app/Policies/FolderPolicy.php @@ -0,0 +1,39 @@ +can('manage-projects') && + $user->projects->contains($folder->project_id); + } + + return $user->can('manage-projects'); + } + + public function move(User $user, Folder $folder) + { + return $user->can('manage-projects') && + $user->projects->contains($folder->project_id); + } + + public function delete(User $user, Folder $folder) + { + return $user->can('delete-projects') && + $user->projects->contains($folder->project_id); + } +} diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php index 32253f1..43d32d4 100644 --- a/app/Policies/ProjectPolicy.php +++ b/app/Policies/ProjectPolicy.php @@ -74,5 +74,10 @@ class ProjectPolicy $project->managers->contains($user->id) || $project->users->contains($user->id); } + + public function managePermissions(User $user, Project $project) + { + return $user->hasPermissionToResource($project, 'manage_permissions'); + } } diff --git a/app/Policies/RolePolicy.php b/app/Policies/RolePolicy.php index 3ec6c17..2857b99 100644 --- a/app/Policies/RolePolicy.php +++ b/app/Policies/RolePolicy.php @@ -2,7 +2,7 @@ namespace App\Policies; -use App\Models\Role; +use Spatie\Permission\Models\Role; use App\Models\User; use Illuminate\Auth\Access\Response; @@ -13,7 +13,7 @@ class RolePolicy */ public function viewAny(User $user): bool { - return $user->hasPermissionTo('manage roles'); + return $user->hasPermissionTo('view roles'); } /** @@ -29,7 +29,7 @@ class RolePolicy */ public function create(User $user): bool { - return $user->hasPermissionTo('manage roles'); + return $user->hasPermissionTo('create roles'); } /** @@ -37,7 +37,7 @@ class RolePolicy */ public function update(User $user, Role $role): bool { - return $user->hasPermissionTo('manage roles') && !$role->is_protected; + return $user->hasPermissionTo('edit roles') && !$role->is_protected; } /** @@ -45,7 +45,7 @@ class RolePolicy */ public function delete(User $user, Role $role): bool { - return $user->hasPermissionTo('manage roles') && !$role->is_protected; + return $user->hasPermissionTo('delete roles') && !$role->is_protected; } /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8d9be48..16a7dc1 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,33 +2,14 @@ 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 Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\Validator; 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. */ @@ -42,10 +23,23 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + // Configuración de componentes Blade Blade::componentNamespace('App\\View\\Components', 'icons'); Blade::component('multiselect', \App\View\Components\Multiselect::class); + // Registro de componentes Livewire Livewire::component('project-show', \App\Http\Livewire\ProjectShow::class); + Livewire::component('file-upload', \App\Http\Livewire\FileUpload::class); + Livewire::component('toolbar', \App\Http\Livewire\Toolbar::class); + + // Validación personalizada + Validator::extend('max_upload_size', function ($attribute, $value, $parameters, $validator) { + $maxSize = env('MAX_UPLOAD_SIZE', 51200); // 50MB por defecto + $totalSize = array_reduce($value, function($sum, $file) { + return $sum + $file->getSize(); + }, 0); + + return $totalSize <= ($maxSize * 1024); + }); } } diff --git a/app/Providers/AppServiceProvider.php.back b/app/Providers/AppServiceProvider.php.back new file mode 100644 index 0000000..002dbb1 --- /dev/null +++ b/app/Providers/AppServiceProvider.php.back @@ -0,0 +1,66 @@ + UserPolicy::class, + User::class => ProfilePolicy::class, + Role::class => RolePolicy::class, + Permission::class => PermissionPolicy::class, + Document::class => DocumentPolicy::class, + Project::class => ProjectPolicy::class, + Folder::class => FolderPolicy::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); + Livewire::component('project-show', \App\Http\Livewire\FileUpload::class); + Livewire::component('toolbar', \App\Http\Livewire\Toolbar::class); + + Validator::extend('max_upload_size', function ($attribute, $value, $parameters, $validator) { + $maxSize = env('MAX_UPLOAD_SIZE', 51200); // Default 50MB + $totalSize = array_reduce($value, function($sum, $file) { + return $sum + $file->getSize(); + }, 0); + + return $totalSize <= ($maxSize * 1024); + }); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..c22f2a1 --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,62 @@ + + */ + protected $policies = [ + User::class => UserPolicy::class, + + Project::class => ProjectPolicy::class, + Folder::class => FolderPolicy::class, + Document::class => DocumentPolicy::class, + + Role::class => RolePolicy::class, + Permission::class => PermissionPolicy::class, + ]; + + + + /** + * Register services. + */ + public function register(): void + { + // + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + $this->registerPolicies(); + + // Configuración adicional de gates aquí si es necesario + Gate::before(function ($user, $ability) { + return $user->hasRole('admin') ? true : null; + }); + } +} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index d80d3e8..0646111 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,6 +2,7 @@ return [ App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, App\Providers\EventServiceProvider::class, App\Providers\VoltServiceProvider::class, ]; diff --git a/composer.lock b/composer.lock index 2b02ad2..54b2f60 100644 --- a/composer.lock +++ b/composer.lock @@ -1137,16 +1137,16 @@ }, { "name": "laravel/framework", - "version": "v12.9.2", + "version": "v12.10.2", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "3db59aa0f382c349c78a92f3e5b5522e00e3301b" + "reference": "0f123cc857bc177abe4d417448d4f7164f71802a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/3db59aa0f382c349c78a92f3e5b5522e00e3301b", - "reference": "3db59aa0f382c349c78a92f3e5b5522e00e3301b", + "url": "https://api.github.com/repos/laravel/framework/zipball/0f123cc857bc177abe4d417448d4f7164f71802a", + "reference": "0f123cc857bc177abe4d417448d4f7164f71802a", "shasum": "" }, "require": { @@ -1348,7 +1348,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-04-16T15:44:19+00:00" + "time": "2025-04-24T14:11:20+00:00" }, { "name": "laravel/prompts", @@ -1411,16 +1411,16 @@ }, { "name": "laravel/sanctum", - "version": "v4.0.8", + "version": "v4.1.0", "source": { "type": "git", "url": "https://github.com/laravel/sanctum.git", - "reference": "ec1dd9ddb2ab370f79dfe724a101856e0963f43c" + "reference": "4e4ced5023e9d8949214e0fb43d9f4bde79c7166" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sanctum/zipball/ec1dd9ddb2ab370f79dfe724a101856e0963f43c", - "reference": "ec1dd9ddb2ab370f79dfe724a101856e0963f43c", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/4e4ced5023e9d8949214e0fb43d9f4bde79c7166", + "reference": "4e4ced5023e9d8949214e0fb43d9f4bde79c7166", "shasum": "" }, "require": { @@ -1471,7 +1471,7 @@ "issues": "https://github.com/laravel/sanctum/issues", "source": "https://github.com/laravel/sanctum" }, - "time": "2025-01-26T19:34:36+00:00" + "time": "2025-04-22T13:53:47+00:00" }, { "name": "laravel/serializable-closure", @@ -2153,16 +2153,16 @@ }, { "name": "livewire/flux", - "version": "v2.1.4", + "version": "v2.1.5", "source": { "type": "git", "url": "https://github.com/livewire/flux.git", - "reference": "a19709fc94f5a1b795ce24ad42662bd398c19371" + "reference": "e24f05be20fa1a0ca027a11c2eea763cc539c82e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/flux/zipball/a19709fc94f5a1b795ce24ad42662bd398c19371", - "reference": "a19709fc94f5a1b795ce24ad42662bd398c19371", + "url": "https://api.github.com/repos/livewire/flux/zipball/e24f05be20fa1a0ca027a11c2eea763cc539c82e", + "reference": "e24f05be20fa1a0ca027a11c2eea763cc539c82e", "shasum": "" }, "require": { @@ -2210,9 +2210,9 @@ ], "support": { "issues": "https://github.com/livewire/flux/issues", - "source": "https://github.com/livewire/flux/tree/v2.1.4" + "source": "https://github.com/livewire/flux/tree/v2.1.5" }, - "time": "2025-04-14T11:59:19+00:00" + "time": "2025-04-24T22:52:25+00:00" }, { "name": "livewire/livewire", @@ -3722,16 +3722,16 @@ }, { "name": "spatie/image", - "version": "3.8.1", + "version": "3.8.3", "source": { "type": "git", "url": "https://github.com/spatie/image.git", - "reference": "80e907bc64fbb7ce87346e97c14534d7dad5d559" + "reference": "54a7331a4d1ba7712603dd058522613506d2dfe0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/image/zipball/80e907bc64fbb7ce87346e97c14534d7dad5d559", - "reference": "80e907bc64fbb7ce87346e97c14534d7dad5d559", + "url": "https://api.github.com/repos/spatie/image/zipball/54a7331a4d1ba7712603dd058522613506d2dfe0", + "reference": "54a7331a4d1ba7712603dd058522613506d2dfe0", "shasum": "" }, "require": { @@ -3779,7 +3779,7 @@ "spatie" ], "support": { - "source": "https://github.com/spatie/image/tree/3.8.1" + "source": "https://github.com/spatie/image/tree/3.8.3" }, "funding": [ { @@ -3791,7 +3791,7 @@ "type": "github" } ], - "time": "2025-03-27T13:01:00+00:00" + "time": "2025-04-25T08:04:51+00:00" }, { "name": "spatie/image-optimizer", @@ -7396,16 +7396,16 @@ }, { "name": "laravel/sail", - "version": "v1.41.0", + "version": "v1.41.1", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec" + "reference": "e5692510f1ef8e0f5096cde2b885d558f8d86592" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec", - "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec", + "url": "https://api.github.com/repos/laravel/sail/zipball/e5692510f1ef8e0f5096cde2b885d558f8d86592", + "reference": "e5692510f1ef8e0f5096cde2b885d558f8d86592", "shasum": "" }, "require": { @@ -7455,7 +7455,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2025-01-24T15:45:36+00:00" + "time": "2025-04-22T13:39:39+00:00" }, { "name": "mockery/mockery", @@ -7953,27 +7953,27 @@ }, { "name": "pestphp/pest-plugin-laravel", - "version": "v3.1.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-laravel.git", - "reference": "1c4e994476375c72aa7aebaaa97aa98f5d5378cd" + "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/1c4e994476375c72aa7aebaaa97aa98f5d5378cd", - "reference": "1c4e994476375c72aa7aebaaa97aa98f5d5378cd", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/6801be82fd92b96e82dd72e563e5674b1ce365fc", + "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc", "shasum": "" }, "require": { - "laravel/framework": "^11.39.1|^12.0.0", - "pestphp/pest": "^3.7.4", + "laravel/framework": "^11.39.1|^12.9.2", + "pestphp/pest": "^3.8.2", "php": "^8.2.0" }, "require-dev": { "laravel/dusk": "^8.2.13|dev-develop", - "orchestra/testbench": "^9.9.0|^10.0.0", - "pestphp/pest-dev-tools": "^3.3.0" + "orchestra/testbench": "^9.9.0|^10.2.1", + "pestphp/pest-dev-tools": "^3.4.0" }, "type": "library", "extra": { @@ -8011,7 +8011,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v3.1.0" + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v3.2.0" }, "funding": [ { @@ -8023,7 +8023,7 @@ "type": "github" } ], - "time": "2025-01-24T13:22:39+00:00" + "time": "2025-04-21T07:40:53+00:00" }, { "name": "pestphp/pest-plugin-mutate", @@ -9913,23 +9913,23 @@ }, { "name": "ta-tikoma/phpunit-architecture-test", - "version": "0.8.4", + "version": "0.8.5", "source": { "type": "git", "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", - "reference": "89f0dea1cb0f0d5744d3ec1764a286af5e006636" + "reference": "cf6fb197b676ba716837c886baca842e4db29005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/89f0dea1cb0f0d5744d3ec1764a286af5e006636", - "reference": "89f0dea1cb0f0d5744d3ec1764a286af5e006636", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005", + "reference": "cf6fb197b676ba716837c886baca842e4db29005", "shasum": "" }, "require": { "nikic/php-parser": "^4.18.0 || ^5.0.0", "php": "^8.1.0", "phpdocumentor/reflection-docblock": "^5.3.0", - "phpunit/phpunit": "^10.5.5 || ^11.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", "symfony/finder": "^6.4.0 || ^7.0.0" }, "require-dev": { @@ -9966,9 +9966,9 @@ ], "support": { "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", - "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.4" + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5" }, - "time": "2024-01-05T14:10:56+00:00" + "time": "2025-04-20T20:23:40+00:00" }, { "name": "theseer/tokenizer", diff --git a/database/migrations/2025_04_27_093329_create_groups_table.php b/database/migrations/2025_04_27_093329_create_groups_table.php new file mode 100644 index 0000000..abd7e3c --- /dev/null +++ b/database/migrations/2025_04_27_093329_create_groups_table.php @@ -0,0 +1,48 @@ +id(); + $table->string('name')->unique(); + $table->text('description')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('group_user', function (Blueprint $table) { + $table->foreignId('group_id')->constrained()->onDelete('cascade'); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->timestamps(); + + $table->primary(['group_id', 'user_id']); + }); + + Schema::create('group_has_permissions', function (Blueprint $table) { + $table->foreignId('group_id')->constrained()->onDelete('cascade'); + $table->foreignId('permission_id')->constrained()->onDelete('cascade'); + $table->timestamps(); + + $table->primary(['group_id', 'permission_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('groups'); + Schema::dropIfExists('group_user'); + Schema::dropIfExists('group_has_permissions'); + } +}; diff --git a/database/migrations/2025_04_22_134728_create_category_project_table.php b/database/migrations/2025_04_27_163903_add_deleted_at_to_users_table.php similarity index 54% rename from database/migrations/2025_04_22_134728_create_category_project_table.php rename to database/migrations/2025_04_27_163903_add_deleted_at_to_users_table.php index b91331d..9dc1aea 100644 --- a/database/migrations/2025_04_22_134728_create_category_project_table.php +++ b/database/migrations/2025_04_27_163903_add_deleted_at_to_users_table.php @@ -11,11 +11,8 @@ return new class extends Migration */ public function up(): void { - Schema::create('category_project', function (Blueprint $table) { - $table->id(); - $table->foreignId('project_id')->constrained(); - $table->foreignId('category_id')->constrained(); - $table->timestamps(); + Schema::table('users', function (Blueprint $table) { + $table->softDeletes(); }); } @@ -24,6 +21,9 @@ return new class extends Migration */ public function down(): void { - Schema::dropIfExists('category_project'); + Schema::table('users', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); } + }; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 2bb052b..f99fa4d 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -14,12 +14,34 @@ class PermissionSeeder extends Seeder public function run() { $permissions = [ + // Permissions for Projects + 'create projects', + 'edit projects', + 'delete projects', + 'view projects', + + // Permissions for Documents 'create projects', 'edit projects', 'delete projects', 'view projects', - 'manage users', 'approve documents', + + 'manage users', + + // Permissions for roles + 'view roles', + 'create roles', + 'edit roles', + 'delete roles', + + // Permissions for permissions + 'view permissions', + 'create permissions', + 'edit permissions', + 'delete permissions', + 'assign permissions', + 'revoke permissions', ]; foreach ($permissions as $permission) { diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 79dec46..21505bd 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Models\User; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; use Spatie\Permission\Models\Permission; @@ -15,18 +16,71 @@ class RolePermissionSeeder extends Seeder public function run(): void { // database/seeders/RolePermissionSeeder.php - $admin = Role::firstOrCreate([ - 'name' => 'admin', - 'guard_name' => 'web' - ]); - $permission = Permission::firstOrCreate([ - 'name' => 'create projects', - 'guard_name' => 'web' - ]); - - // Asignar TODOS los permisos - $admin->givePermissionTo(Permission::all()); + // Crear rol de administrador + $adminRole = Role::updateOrCreate( + ['name' => 'admin'], + //['description' => 'Administrador del sistema'] + ); + + // Obtener o crear todos los permisos existentes + $permissions = Permission::all(); + + if ($permissions->isEmpty()) { + // Crear permisos básicos si no existen + $permissions = collect([ + 'view projects', + 'edit projects', + 'delete projects', + 'view roles', + 'create roles', + 'edit roles', + 'delete roles', + 'view permissions', + 'create permissions', + 'edit permissions', + 'delete permissions', + 'assign permissions', + 'revoke permissions', + + ])->map(function ($permission) { + return Permission::updateOrCreate( + ['name' => $permission], + ['guard_name' => 'web'] + ); + }); + } + + // Sincronizar todos los permisos con el rol admin + $allPermissions = Permission::all(); + $adminRole->syncPermissions($allPermissions); + $adminRole->syncPermissions($permissions); + + // Crear usuario admin si no existe + /*User::updateOrCreate( + ['email' => env('ADMIN_EMAIL', 'admin@example.com')], + [ + 'name' => 'Administrador', + 'password' => bcrypt(env('ADMIN_PASSWORD', 'password')), + 'email_verified_at' => now() + ] + )->assignRole($adminRole);*/ + $adminEmail = env('ADMIN_EMAIL', 'admin@example.com'); + $user = User::where('email', $adminEmail)->first(); + if ($user) { + // Asignar rol solo si no lo tiene + if (!$user->hasRole($adminRole)) { + $user->assignRole($adminRole); + } + } else { + // Crear solo si no existe + User::create([ + 'name' => 'admin', + 'email' => $adminEmail, + 'password' => bcrypt(env('ADMIN_PASSWORD', '12345678')), + 'email_verified_at' => now() + ])->assignRole($adminRole); + } } } diff --git a/package-lock.json b/package-lock.json index 03a4400..ca4863d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "dependencies": { + "@heroicons/react": "^2.0.18", "@tailwindcss/vite": "^4.0.7", "@yaireo/tagify": "^4.34.0", "autoprefixer": "^10.4.20", @@ -421,6 +422,14 @@ "node": ">=18" } }, + "node_modules/@heroicons/react": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.0.18.tgz", + "integrity": "sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.8", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", diff --git a/package.json b/package.json index 6d40612..b10ccac 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "dev": "vite" }, "dependencies": { + "@heroicons/react": "^2.0.18", "@tailwindcss/vite": "^4.0.7", "@yaireo/tagify": "^4.34.0", "autoprefixer": "^10.4.20", diff --git a/resources/css/app.css b/resources/css/app.css index ad6eeed..65ef9c1 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -64,3 +64,22 @@ select:focus[data-flux-control] { /* \[:where(&)\]:size-4 { @apply size-4; } */ + +/* resources/css/app.css */ +@layer components { + .permission-card { + @apply transition-all duration-200 ease-in-out transform hover:scale-[1.02] hover:shadow-lg; + } + + .dark .permission-card { + @apply hover:bg-gray-700; + } + + .permission-badge { + @apply px-2 py-1 rounded-full text-xs font-medium flex items-center space-x-1; + } +} + +.btn-primary { + @apply inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 active:bg-blue-900 focus:outline-none focus:border-blue-900 focus:ring focus:ring-blue-300 disabled:opacity-25 transition; +} diff --git a/resources/js/folder-dnd.js b/resources/js/folder-dnd.js new file mode 100644 index 0000000..f2731c4 --- /dev/null +++ b/resources/js/folder-dnd.js @@ -0,0 +1,62 @@ +// resources/js/folder-dnd.js +document.addEventListener('DOMContentLoaded', () => { + const folders = document.querySelectorAll('.folder-item'); + + folders.forEach(folder => { + folder.draggable = true; + + folder.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('text/plain', folder.dataset.folderId); + folder.classList.add('opacity-50'); + }); + + folder.addEventListener('dragend', () => { + folder.classList.remove('opacity-50'); + }); + }); + + const dropZones = document.querySelectorAll('[data-drop-zone]'); + + dropZones.forEach(zone => { + zone.addEventListener('dragover', (e) => { + e.preventDefault(); + zone.classList.add('bg-blue-50', 'border-blue-200'); + }); + + zone.addEventListener('dragleave', () => { + zone.classList.remove('bg-blue-50', 'border-blue-200'); + }); + + zone.addEventListener('drop', async (e) => { + e.preventDefault(); + const folderId = e.dataTransfer.getData('text/plain'); + const newParentId = zone.dataset.folderId; + + try { + const response = await fetch(`/folders/${folderId}/move`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content + }, + body: JSON.stringify({ parent_id: newParentId }) + }); + + if (response.ok) { + window.location.reload(); + } + } catch (error) { + console.error('Error moving folder:', error); + } + + zone.classList.remove('bg-blue-50', 'border-blue-200'); + }); + }); +}); + +document.querySelectorAll('.folder-item').forEach(item => { + item.addEventListener('click', function() { + const folderId = this.dataset.folderId; + Livewire.emit('folderSelected', folderId); + }); +}); \ No newline at end of file diff --git a/resources/views/components/folder-item.blade.php b/resources/views/components/folder-item.blade.php index 7a5112c..0816752 100644 --- a/resources/views/components/folder-item.blade.php +++ b/resources/views/components/folder-item.blade.php @@ -1,21 +1,32 @@ -@props(['folder', 'level' => 0]) +@props(['folder', 'currentFolder', 'expandedFolders', 'level' => 0]) -
  • -
    -
    - - {{ $folder->name }} +
  • +
    +
    + + + {{ $folder->name }}
    - @if($folder->children->isNotEmpty()) - - @endif
    - @if($folder->children->isNotEmpty()) -