setPrimaryKey('id') ->setDefaultSort('created_at', 'desc') ->setSortingPillsEnabled(false) ->setAdditionalSelects(['issues.id as id', 'issues.created_at as created_at']); } public function builder(): Builder { // Defence in depth: only members (or super-admin) may list a project's issues. $user = Auth::user(); abort_unless( $user->can('view issues') && ( $user->can('manage all') || \App\Models\Project::whereKey($this->projectId)->whereHas('users', fn ($q) => $q->where('user_id', $user->id))->exists() ), 403 ); return Issue::where('issues.project_id', $this->projectId) ->with(['feature', 'reporter', 'assignee']) ->withCount([ 'comments', 'media', 'tasks', 'tasks as tasks_done_count' => fn (Builder $q) => $q->where('is_done', true), 'tasks as overdue_tasks_count' => fn (Builder $q) => $q->where('is_done', false) ->whereNotNull('due_date') ->whereDate('due_date', '<', now()->toDateString()), ]); } public function columns(): array { return [ Column::make('Prioridad', 'priority') ->sortable() ->format(function ($value, $row) { $label = ['low' => 'Bajo', 'medium' => 'Medio', 'high' => 'Alto', 'critical' => 'CrΓ­tico'][$value] ?? ucfirst($value); $textColor = in_array($value, ['critical', 'high']) ? '#fff' : '#1f2937'; return ''.$label.''; }) ->html(), Column::make('TΓ­tulo', 'title') ->sortable() ->searchable() ->format(function ($value, $row) { $url = route('projects.issues.show', [$this->projectId, $row->id]); $html = ''.e($value).''; if ($row->description) { $html .= '
'.e(Str::limit($row->description, 60)).'
'; } $meta = []; if ($row->reporter) $meta[] = 'Reportado por '.e($row->reporter->name); if ($row->comments_count) $meta[] = 'πŸ’¬ '.$row->comments_count; if ($row->media_count) $meta[] = 'πŸ“· '.$row->media_count; if ($meta) { $html .= '
'.implode(' Β· ', $meta).'
'; } if ($row->tasks_count) { $pct = (int) round($row->tasks_done_count / $row->tasks_count * 100); $html .= '
'.$row->tasks_done_count.'/'.$row->tasks_count.' tareas
'; } if ($row->overdue_tasks_count) { $html .= '
⏰ '.$row->overdue_tasks_count.' vencida'.($row->overdue_tasks_count > 1 ? 's' : '').'
'; } return $html; }) ->html(), Column::make('Feature') ->label(fn ($row) => $row->feature ? ''.e($row->feature->name).'' : 'β€”') ->html(), Column::make('Estado', 'status') ->sortable() ->format(function ($value, $row) { $label = ['open' => 'Abierto', 'in_review' => 'En revisiΓ³n', 'resolved' => 'Resuelto', 'closed' => 'Cerrado'][$value] ?? ucfirst($value); return ''.$label.''; }) ->html(), Column::make('Asignado a') ->label(fn ($row) => $row->assignee ? ''.e($row->assignee->name).'' : 'Sin asignar') ->html(), Column::make('Fecha', 'created_at') ->sortable() ->format(function ($value, $row) { $html = $row->created_at->format('d/m/Y'); if ($row->resolved_at) { $html .= '
Res. '.$row->resolved_at->format('d/m/Y').'
'; } return $html; }) ->html(), Column::make('Acciones') ->label(function ($row) { $user = Auth::user(); $detail = route('projects.issues.show', [$this->projectId, $row->id]); $edit = route('projects.issues.edit', [$this->projectId, $row->id]); $html = '
'; $html .= ' '; if ($user->can('edit issues')) { $html .= ' '; if (in_array($row->status, ['open', 'in_review'])) { $html .= ''; } if ($row->status !== 'closed') { $html .= ''; } } if ($user->can('delete issues')) { $html .= ''; } $html .= '
'; return $html; }) ->html(), ]; } public function filters(): array { return [ SelectFilter::make('Estado', 'status') ->options([ '' => 'Estado: todos', 'open' => 'Abierto', 'in_review' => 'En revisiΓ³n', 'resolved' => 'Resuelto', 'closed' => 'Cerrado', ]) ->filter(fn (Builder $query, string $value) => $query->where('issues.status', $value)), SelectFilter::make('Prioridad', 'priority') ->options([ '' => 'Prioridad: todas', 'critical' => 'CrΓ­tica', 'high' => 'Alta', 'medium' => 'Media', 'low' => 'Baja', ]) ->filter(fn (Builder $query, string $value) => $query->where('issues.priority', $value)), ]; } // ── Row actions ──────────────────────────────────────────────────────────────── private function findIssue(int $id): Issue { return Issue::where('project_id', $this->projectId)->findOrFail($id); } public function resolve(int $id): void { abort_unless(Auth::user()->can('edit issues'), 403); $issue = $this->findIssue($id); $issue->update(['status' => 'resolved', 'resolved_at' => $issue->resolved_at ?? now()]); $this->dispatch('issuesChanged'); $this->dispatch('notify', 'Incidencia marcada como resuelta'); } public function close(int $id): void { abort_unless(Auth::user()->can('edit issues'), 403); $issue = $this->findIssue($id); $issue->update(['status' => 'closed', 'resolved_at' => $issue->resolved_at ?? now()]); $this->dispatch('issuesChanged'); $this->dispatch('notify', 'Incidencia cerrada'); } public function deleteIssue(int $id): void { abort_unless(Auth::user()->can('delete issues'), 403); $this->findIssue($id)->delete(); $this->dispatch('issuesChanged'); $this->dispatch('notify', 'Incidencia eliminada'); } }