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');
}
}