feat(issues): notificaciones, plantillas de checklist, alertas de vencimiento y reporte desde el mapa
- Notificaciones (DB): asignación de incidencia (IssueAssigned), asignación de tarea (IssueTaskAssigned), comentario (IssueCommented) y cambio de estado (IssueStatusChanged) a reporter+asignado excluyendo al actor. - Plantillas de checklist: tabla issue_checklist_templates + modelo, gestor CRUD (IssueChecklistManager, ruta projects.issues.checklists) y "Aplicar plantilla" en el detalle (alta masiva de tareas). - Alertas de vencimiento: columna overdue_notified_at + scope overdue, comando issues:notify-overdue (programado a diario) que avisa al asignado una sola vez; badge "vencidas" en la tabla y resaltado por tarea en el detalle. - Reporte desde el mapa: botón "Incidencia" en el panel del feature seleccionado → formulario con feature pre-vinculado (IssueForm lee ?feature=). Tests: IssuesEnhancementsTest (7). Suite 57 passing (solo 2 pre-existentes sqlite). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
<div class="max-w-3xl mx-auto">
|
||||
|
||||
<a href="{{ route('projects.issues', $project) }}" wire:navigate
|
||||
class="btn btn-ghost btn-sm gap-1 mb-3">
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" /> Volver a incidencias
|
||||
</a>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-5">
|
||||
<div>
|
||||
<h1 class="text-xl font-bold">Plantillas de checklist</h1>
|
||||
<p class="text-sm text-base-content/60">Listas de tareas reutilizables para incidencias recurrentes · {{ $project->name }}</p>
|
||||
</div>
|
||||
<button wire:click="newTemplate" class="btn btn-primary btn-sm gap-2">
|
||||
<x-heroicon-o-plus class="w-4 h-4" /> Nueva plantilla
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- List --}}
|
||||
@if($templates->isEmpty())
|
||||
<div class="flex flex-col items-center justify-center py-12 text-base-content/40">
|
||||
<x-heroicon-o-clipboard-document-check class="w-14 h-14 mb-3" />
|
||||
<p class="font-semibold">Sin plantillas</p>
|
||||
<p class="text-sm">Crea una lista de tareas reutilizable para aplicarla a tus incidencias.</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
@foreach($templates as $t)
|
||||
<div wire:key="tpl-{{ $t->id }}" class="card bg-base-100 border border-base-300">
|
||||
<div class="card-body p-4 flex-row items-center justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold">{{ $t->name }}</div>
|
||||
<div class="text-xs text-base-content/50">{{ count($t->items ?: []) }} tareas: {{ Str::limit(implode(' · ', $t->items ?: []), 90) }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<button wire:click="edit({{ $t->id }})" class="btn btn-xs btn-ghost" title="Editar">
|
||||
<x-heroicon-o-pencil class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button wire:click="delete({{ $t->id }})" wire:confirm="¿Eliminar la plantilla '{{ $t->name }}'?"
|
||||
class="btn btn-xs btn-error btn-outline" title="Eliminar">
|
||||
<x-heroicon-o-trash class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Form modal --}}
|
||||
@if($showForm)
|
||||
<div class="fixed inset-0 z-40 bg-black/50" wire:click="$set('showForm', false)"></div>
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-base-100 rounded-box shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between p-4 border-b border-base-300">
|
||||
<h3 class="font-bold">{{ $editingId ? 'Editar plantilla' : 'Nueva plantilla' }}</h3>
|
||||
<button wire:click="$set('showForm', false)" class="btn btn-sm btn-ghost btn-circle">
|
||||
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<form wire:submit.prevent="save" class="p-4 space-y-3">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-medium">Nombre <span class="text-error">*</span></span></label>
|
||||
<input type="text" wire:model="name"
|
||||
class="input input-bordered w-full @error('name') input-error @enderror"
|
||||
placeholder="Ej.: Reparación de grieta" />
|
||||
@error('name')<span class="text-xs text-error">{{ $message }}</span>@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-medium">Tareas</span></label>
|
||||
<div class="space-y-2">
|
||||
@foreach($items as $i => $item)
|
||||
<div wire:key="item-{{ $i }}" class="flex items-center gap-2">
|
||||
<input type="text" wire:model="items.{{ $i }}"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="Descripción de la tarea..." />
|
||||
<button type="button" wire:click="removeItemLine({{ $i }})"
|
||||
class="btn btn-sm btn-ghost text-error">
|
||||
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@error('items')<span class="text-xs text-error">{{ $message }}</span>@enderror
|
||||
<button type="button" wire:click="addItemLine" class="btn btn-xs btn-ghost gap-1 mt-2 w-fit">
|
||||
<x-heroicon-o-plus class="w-3.5 h-3.5" /> Añadir tarea
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2 pt-2 border-t border-base-300">
|
||||
<button type="button" wire:click="$set('showForm', false)" class="btn btn-ghost btn-sm">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Guardar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -161,6 +161,20 @@
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@if(count($checklistTemplates))
|
||||
<div class="flex items-center gap-2 mt-2 pt-2 border-t border-base-200">
|
||||
<span class="text-xs text-base-content/50">Aplicar plantilla:</span>
|
||||
<select wire:model="applyTemplateId" class="select select-bordered select-xs">
|
||||
<option value="">Elegir...</option>
|
||||
@foreach($checklistTemplates as $tpl)
|
||||
<option value="{{ $tpl->id }}">{{ $tpl->name }} ({{ count($tpl->items ?: []) }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button wire:click="applyTemplate" class="btn btn-xs btn-outline" @disabled(!$applyTemplateId)>Aplicar</button>
|
||||
</div>
|
||||
@error('applyTemplateId')<span class="text-xs text-error">{{ $message }}</span>@enderror
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
</h1>
|
||||
<p class="text-sm text-base-content/60 mb-4">{{ $project->name }}</p>
|
||||
|
||||
@if($featureName)
|
||||
<div class="alert alert-info py-2 mb-4 text-sm">
|
||||
<x-heroicon-o-map-pin class="w-4 h-4" />
|
||||
<span>Vinculada al elemento del mapa: <strong>{{ $featureName }}</strong></span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit.prevent="save" class="space-y-4">
|
||||
|
||||
{{-- Título --}}
|
||||
|
||||
@@ -7,16 +7,28 @@
|
||||
<h2 class="text-xl font-bold">Incidencias del proyecto</h2>
|
||||
<p class="text-sm text-base-content/60">Gestión de incidencias y problemas</p>
|
||||
</div>
|
||||
@can('create issues')
|
||||
<a
|
||||
href="{{ route('projects.issues.create', $project) }}"
|
||||
wire:navigate
|
||||
class="btn btn-primary btn-sm gap-2"
|
||||
>
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
Nueva incidencia
|
||||
</a>
|
||||
@endcan
|
||||
<div class="flex items-center gap-2">
|
||||
@can('edit issues')
|
||||
<a
|
||||
href="{{ route('projects.issues.checklists', $project) }}"
|
||||
wire:navigate
|
||||
class="btn btn-ghost btn-sm gap-2"
|
||||
>
|
||||
<x-heroicon-o-clipboard-document-check class="w-4 h-4" />
|
||||
Plantillas
|
||||
</a>
|
||||
@endcan
|
||||
@can('create issues')
|
||||
<a
|
||||
href="{{ route('projects.issues.create', $project) }}"
|
||||
wire:navigate
|
||||
class="btn btn-primary btn-sm gap-2"
|
||||
>
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
Nueva incidencia
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ================================================================
|
||||
|
||||
Reference in New Issue
Block a user