404 lines
21 KiB
PHP
404 lines
21 KiB
PHP
|
|
<div>
|
||
|
|
{{-- ================================================================
|
||
|
|
HEADER
|
||
|
|
================================================================ --}}
|
||
|
|
<div class="flex flex-wrap items-center justify-between gap-3 mb-5">
|
||
|
|
<div>
|
||
|
|
<h2 class="text-xl font-bold">Issues del proyecto</h2>
|
||
|
|
<p class="text-sm text-base-content/60">Gestión de incidencias y problemas</p>
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
wire:click="openForm()"
|
||
|
|
class="btn btn-primary btn-sm gap-2"
|
||
|
|
>
|
||
|
|
<x-heroicon-o-plus class="w-4 h-4" />
|
||
|
|
Nuevo Issue
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- ================================================================
|
||
|
|
STATS BAR
|
||
|
|
================================================================ --}}
|
||
|
|
@php
|
||
|
|
$countOpen = $issues->where('status', 'open')->count();
|
||
|
|
$countInReview = $issues->where('status', 'in_review')->count();
|
||
|
|
$countResolved = $issues->where('status', 'resolved')->count();
|
||
|
|
$countClosed = $issues->where('status', 'closed')->count();
|
||
|
|
@endphp
|
||
|
|
|
||
|
|
<div class="flex flex-wrap gap-2 mb-5">
|
||
|
|
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||
|
|
<div class="stat-title text-xs">Abiertos</div>
|
||
|
|
<div class="stat-value text-error text-2xl">{{ $countOpen }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||
|
|
<div class="stat-title text-xs">En revisión</div>
|
||
|
|
<div class="stat-value text-warning text-2xl">{{ $countInReview }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||
|
|
<div class="stat-title text-xs">Resueltos</div>
|
||
|
|
<div class="stat-value text-success text-2xl">{{ $countResolved }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||
|
|
<div class="stat-title text-xs">Cerrados</div>
|
||
|
|
<div class="stat-value text-base-content/50 text-2xl">{{ $countClosed }}</div>
|
||
|
|
</div>
|
||
|
|
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||
|
|
<div class="stat-title text-xs">Total</div>
|
||
|
|
<div class="stat-value text-2xl">{{ $issues->count() }}</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- ================================================================
|
||
|
|
ISSUES TABLE
|
||
|
|
================================================================ --}}
|
||
|
|
@if($issues->isEmpty())
|
||
|
|
<div class="flex flex-col items-center justify-center py-16 text-base-content/40">
|
||
|
|
<x-heroicon-o-bug-ant class="w-16 h-16 mb-3" />
|
||
|
|
<p class="text-lg font-semibold">Sin issues registrados</p>
|
||
|
|
<p class="text-sm">Crea el primer issue con el botón "Nuevo Issue".</p>
|
||
|
|
</div>
|
||
|
|
@else
|
||
|
|
<div class="overflow-x-auto rounded-box border border-base-300">
|
||
|
|
<table class="table table-sm w-full">
|
||
|
|
<thead class="bg-base-200">
|
||
|
|
<tr>
|
||
|
|
<th class="w-28">Prioridad</th>
|
||
|
|
<th>Título</th>
|
||
|
|
<th class="hidden md:table-cell">Feature</th>
|
||
|
|
<th class="w-28">Estado</th>
|
||
|
|
<th class="hidden lg:table-cell w-36">Asignado a</th>
|
||
|
|
<th class="hidden lg:table-cell w-28">Fecha</th>
|
||
|
|
<th class="w-36 text-right">Acciones</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
@foreach($issues as $issue)
|
||
|
|
<tr wire:key="issue-{{ $issue->id }}" class="hover">
|
||
|
|
{{-- Prioridad --}}
|
||
|
|
<td>
|
||
|
|
@php
|
||
|
|
$pClass = match($issue->priority) {
|
||
|
|
'critical' => 'badge-purple',
|
||
|
|
'high' => 'badge-error',
|
||
|
|
'medium' => 'badge-warning',
|
||
|
|
'low' => 'badge-ghost',
|
||
|
|
default => 'badge-ghost',
|
||
|
|
};
|
||
|
|
$pLabel = match($issue->priority) {
|
||
|
|
'critical' => 'Crítico',
|
||
|
|
'high' => 'Alto',
|
||
|
|
'medium' => 'Medio',
|
||
|
|
'low' => 'Bajo',
|
||
|
|
default => ucfirst($issue->priority),
|
||
|
|
};
|
||
|
|
@endphp
|
||
|
|
<span
|
||
|
|
class="badge badge-sm font-semibold
|
||
|
|
{{ $issue->priority === 'critical' ? 'text-white' : '' }}"
|
||
|
|
style="background-color: {{ $issue->priority_color }}; color: {{ $issue->priority === 'critical' || $issue->priority === 'high' ? '#fff' : '#1f2937' }}; border-color: transparent;"
|
||
|
|
>
|
||
|
|
{{ $pLabel }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
{{-- Título + descripción breve --}}
|
||
|
|
<td>
|
||
|
|
<div class="font-medium text-sm leading-tight">{{ $issue->title }}</div>
|
||
|
|
@if($issue->description)
|
||
|
|
<div class="text-xs text-base-content/50 truncate max-w-xs">{{ Str::limit($issue->description, 60) }}</div>
|
||
|
|
@endif
|
||
|
|
@if($issue->reporter)
|
||
|
|
<div class="text-xs text-base-content/40 mt-0.5">
|
||
|
|
Reportado por {{ $issue->reporter->name }}
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
</td>
|
||
|
|
|
||
|
|
{{-- Feature --}}
|
||
|
|
<td class="hidden md:table-cell">
|
||
|
|
@if($issue->feature)
|
||
|
|
<span class="badge badge-outline badge-sm">{{ $issue->feature->name }}</span>
|
||
|
|
@else
|
||
|
|
<span class="text-base-content/30 text-xs">—</span>
|
||
|
|
@endif
|
||
|
|
</td>
|
||
|
|
|
||
|
|
{{-- Estado --}}
|
||
|
|
<td>
|
||
|
|
@php
|
||
|
|
$sLabel = match($issue->status) {
|
||
|
|
'open' => 'Abierto',
|
||
|
|
'in_review' => 'En revisión',
|
||
|
|
'resolved' => 'Resuelto',
|
||
|
|
'closed' => 'Cerrado',
|
||
|
|
default => ucfirst($issue->status),
|
||
|
|
};
|
||
|
|
@endphp
|
||
|
|
<span
|
||
|
|
class="badge badge-sm"
|
||
|
|
style="background-color: {{ $issue->status_color }}; color: #fff; border-color: transparent;"
|
||
|
|
>
|
||
|
|
{{ $sLabel }}
|
||
|
|
</span>
|
||
|
|
</td>
|
||
|
|
|
||
|
|
{{-- Asignado a --}}
|
||
|
|
<td class="hidden lg:table-cell">
|
||
|
|
@if($issue->assignee)
|
||
|
|
<div class="flex items-center gap-1.5">
|
||
|
|
<span class="w-6 h-6 rounded-full bg-primary text-primary-content flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||
|
|
{{ strtoupper(substr($issue->assignee->name, 0, 1)) }}
|
||
|
|
</span>
|
||
|
|
<span class="text-sm truncate">{{ $issue->assignee->name }}</span>
|
||
|
|
</div>
|
||
|
|
@else
|
||
|
|
<span class="text-base-content/30 text-xs">Sin asignar</span>
|
||
|
|
@endif
|
||
|
|
</td>
|
||
|
|
|
||
|
|
{{-- Fecha --}}
|
||
|
|
<td class="hidden lg:table-cell text-xs text-base-content/50">
|
||
|
|
{{ $issue->created_at->format('d/m/Y') }}
|
||
|
|
@if($issue->resolved_at)
|
||
|
|
<div class="text-success">Res. {{ $issue->resolved_at->format('d/m/Y') }}</div>
|
||
|
|
@endif
|
||
|
|
</td>
|
||
|
|
|
||
|
|
{{-- Acciones --}}
|
||
|
|
<td class="text-right">
|
||
|
|
<div class="flex items-center justify-end gap-1 flex-wrap">
|
||
|
|
{{-- Editar --}}
|
||
|
|
<button
|
||
|
|
wire:click="openForm({{ $issue->id }})"
|
||
|
|
class="btn btn-xs btn-ghost tooltip"
|
||
|
|
data-tip="Editar"
|
||
|
|
>
|
||
|
|
<x-heroicon-o-pencil class="w-3.5 h-3.5" />
|
||
|
|
</button>
|
||
|
|
|
||
|
|
{{-- Resolver --}}
|
||
|
|
@if(in_array($issue->status, ['open', 'in_review']))
|
||
|
|
<button
|
||
|
|
wire:click="resolve({{ $issue->id }})"
|
||
|
|
wire:loading.attr="disabled"
|
||
|
|
wire:target="resolve({{ $issue->id }})"
|
||
|
|
class="btn btn-xs btn-success tooltip"
|
||
|
|
data-tip="Marcar como resuelto"
|
||
|
|
>
|
||
|
|
<x-heroicon-o-check class="w-3.5 h-3.5" />
|
||
|
|
</button>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
{{-- Cerrar --}}
|
||
|
|
@if($issue->status !== 'closed')
|
||
|
|
<button
|
||
|
|
wire:click="close({{ $issue->id }})"
|
||
|
|
wire:loading.attr="disabled"
|
||
|
|
wire:target="close({{ $issue->id }})"
|
||
|
|
class="btn btn-xs btn-neutral tooltip"
|
||
|
|
data-tip="Cerrar issue"
|
||
|
|
>
|
||
|
|
<x-heroicon-o-x-mark class="w-3.5 h-3.5" />
|
||
|
|
</button>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
{{-- Eliminar --}}
|
||
|
|
<button
|
||
|
|
wire:click="delete({{ $issue->id }})"
|
||
|
|
wire:loading.attr="disabled"
|
||
|
|
wire:target="delete({{ $issue->id }})"
|
||
|
|
wire:confirm="¿Eliminar este issue? Esta acción no se puede deshacer."
|
||
|
|
class="btn btn-xs btn-error btn-outline tooltip"
|
||
|
|
data-tip="Eliminar"
|
||
|
|
>
|
||
|
|
<x-heroicon-o-trash class="w-3.5 h-3.5" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
@endforeach
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
{{-- ================================================================
|
||
|
|
MODAL FORM (create / edit)
|
||
|
|
================================================================ --}}
|
||
|
|
@if($showForm)
|
||
|
|
{{-- Overlay --}}
|
||
|
|
<div
|
||
|
|
class="fixed inset-0 z-40 bg-black/50"
|
||
|
|
wire:click="closeForm()"
|
||
|
|
></div>
|
||
|
|
|
||
|
|
{{-- Modal panel --}}
|
||
|
|
<div
|
||
|
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||
|
|
role="dialog"
|
||
|
|
aria-modal="true"
|
||
|
|
>
|
||
|
|
<div class="bg-base-100 rounded-box shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||
|
|
|
||
|
|
{{-- Modal header --}}
|
||
|
|
<div class="flex items-center justify-between p-5 border-b border-base-300">
|
||
|
|
<h3 class="text-lg font-bold">
|
||
|
|
{{ $editingIssue ? 'Editar Issue' : 'Nuevo Issue' }}
|
||
|
|
</h3>
|
||
|
|
<button wire:click="closeForm()" class="btn btn-sm btn-ghost btn-circle">
|
||
|
|
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- Modal body --}}
|
||
|
|
<form wire:submit.prevent="save" class="p-5 space-y-4">
|
||
|
|
|
||
|
|
{{-- Título --}}
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text font-medium">Título <span class="text-error">*</span></span>
|
||
|
|
</label>
|
||
|
|
<input
|
||
|
|
type="text"
|
||
|
|
wire:model="title"
|
||
|
|
class="input input-bordered w-full @error('title') input-error @enderror"
|
||
|
|
placeholder="Describe brevemente el problema..."
|
||
|
|
autofocus
|
||
|
|
/>
|
||
|
|
@error('title')
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text-alt text-error">{{ $message }}</span>
|
||
|
|
</label>
|
||
|
|
@enderror
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- Descripción --}}
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text font-medium">Descripción</span>
|
||
|
|
</label>
|
||
|
|
<textarea
|
||
|
|
wire:model="description"
|
||
|
|
class="textarea textarea-bordered w-full h-24 resize-y @error('description') textarea-error @enderror"
|
||
|
|
placeholder="Detalla el problema, contexto, pasos para reproducirlo..."
|
||
|
|
></textarea>
|
||
|
|
@error('description')
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text-alt text-error">{{ $message }}</span>
|
||
|
|
</label>
|
||
|
|
@enderror
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- Prioridad + Estado --}}
|
||
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text font-medium">Prioridad <span class="text-error">*</span></span>
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
wire:model="priority"
|
||
|
|
class="select select-bordered w-full @error('priority') select-error @enderror"
|
||
|
|
>
|
||
|
|
<option value="low">Baja</option>
|
||
|
|
<option value="medium">Media</option>
|
||
|
|
<option value="high">Alta</option>
|
||
|
|
<option value="critical">Crítica</option>
|
||
|
|
</select>
|
||
|
|
@error('priority')
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text-alt text-error">{{ $message }}</span>
|
||
|
|
</label>
|
||
|
|
@enderror
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text font-medium">Estado <span class="text-error">*</span></span>
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
wire:model.live="status"
|
||
|
|
class="select select-bordered w-full @error('status') select-error @enderror"
|
||
|
|
>
|
||
|
|
<option value="open">Abierto</option>
|
||
|
|
<option value="in_review">En revisión</option>
|
||
|
|
<option value="resolved">Resuelto</option>
|
||
|
|
<option value="closed">Cerrado</option>
|
||
|
|
</select>
|
||
|
|
@error('status')
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text-alt text-error">{{ $message }}</span>
|
||
|
|
</label>
|
||
|
|
@enderror
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- Asignado a --}}
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text font-medium">Asignado a</span>
|
||
|
|
</label>
|
||
|
|
<select
|
||
|
|
wire:model="assignedTo"
|
||
|
|
class="select select-bordered w-full @error('assignedTo') select-error @enderror"
|
||
|
|
>
|
||
|
|
<option value="">Sin asignar</option>
|
||
|
|
@foreach($projectUsers as $user)
|
||
|
|
<option value="{{ $user->id }}">{{ $user->name }} – {{ $user->email }}</option>
|
||
|
|
@endforeach
|
||
|
|
</select>
|
||
|
|
@error('assignedTo')
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text-alt text-error">{{ $message }}</span>
|
||
|
|
</label>
|
||
|
|
@enderror
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{{-- Notas de resolución (visible when status = resolved or closed) --}}
|
||
|
|
@if(in_array($status, ['resolved', 'closed']))
|
||
|
|
<div class="form-control">
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text font-medium">Notas de resolución</span>
|
||
|
|
<span class="label-text-alt text-base-content/50">Opcional</span>
|
||
|
|
</label>
|
||
|
|
<textarea
|
||
|
|
wire:model="resolutionNotes"
|
||
|
|
class="textarea textarea-bordered w-full h-20 resize-y @error('resolutionNotes') textarea-error @enderror"
|
||
|
|
placeholder="Describe cómo se resolvió el problema..."
|
||
|
|
></textarea>
|
||
|
|
@error('resolutionNotes')
|
||
|
|
<label class="label">
|
||
|
|
<span class="label-text-alt text-error">{{ $message }}</span>
|
||
|
|
</label>
|
||
|
|
@enderror
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
{{-- Modal footer --}}
|
||
|
|
<div class="flex items-center justify-end gap-3 pt-2 border-t border-base-300">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
wire:click="closeForm()"
|
||
|
|
class="btn btn-ghost"
|
||
|
|
>
|
||
|
|
Cancelar
|
||
|
|
</button>
|
||
|
|
<button
|
||
|
|
type="submit"
|
||
|
|
wire:loading.attr="disabled"
|
||
|
|
wire:target="save"
|
||
|
|
class="btn btn-primary gap-2"
|
||
|
|
>
|
||
|
|
<span wire:loading.remove wire:target="save">
|
||
|
|
<x-heroicon-o-check class="w-4 h-4" />
|
||
|
|
</span>
|
||
|
|
<span wire:loading wire:target="save" class="loading loading-spinner loading-sm"></span>
|
||
|
|
{{ $editingIssue ? 'Actualizar Issue' : 'Crear Issue' }}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
</div>
|