Files
construprogress/tests/Feature/IssuesEnhancementsTest.php
T
javier 8c774d075d 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>
2026-06-18 12:51:41 +02:00

200 lines
7.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Livewire\IssueChecklistManager;
use App\Livewire\IssueDetail;
use App\Livewire\IssueForm;
use App\Models\Feature;
use App\Models\Issue;
use App\Models\IssueChecklistTemplate;
use App\Models\IssueTask;
use App\Models\Layer;
use App\Models\Phase;
use App\Models\Project;
use App\Models\User;
use App\Notifications\IssueCommentedNotification;
use App\Notifications\IssueStatusChangedNotification;
use App\Notifications\IssueTaskAssignedNotification;
use App\Notifications\IssueTaskOverdueNotification;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Notification;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Tests\TestCase;
class IssuesEnhancementsTest extends TestCase
{
use RefreshDatabase;
private User $user;
private User $assignee;
private Project $project;
protected function setUp(): void
{
parent::setUp();
foreach (['view issues', 'create issues', 'edit issues', 'delete issues', 'upload media'] as $p) {
Permission::findOrCreate($p);
}
$this->user = User::factory()->create();
$this->user->givePermissionTo(['view issues', 'create issues', 'edit issues', 'delete issues', 'upload media']);
$this->assignee = User::factory()->create();
$this->project = Project::create([
'reference' => 'ENH-1',
'name' => 'Proyecto Enh',
'address' => 'Calle 1',
'lat' => 40.0,
'lng' => -3.0,
'start_date' => now()->toDateString(),
'end_date_estimated' => now()->addMonths(3)->toDateString(),
'status' => 'in_progress',
'created_by' => $this->user->id,
]);
$this->project->users()->attach($this->user->id, ['role_in_project' => 'supervisor']);
$this->project->users()->attach($this->assignee->id, ['role_in_project' => 'worker']);
}
private function makeIssue(array $attrs = []): Issue
{
return Issue::create(array_merge([
'project_id' => $this->project->id,
'title' => 'Incidencia enh',
'status' => 'open',
'priority' => 'medium',
'reported_by' => $this->user->id,
], $attrs));
}
private function makeFeature(): Feature
{
$phase = Phase::create([
'project_id' => $this->project->id, 'name' => 'F', 'order' => 1,
'color' => '#000', 'progress_percent' => 0,
]);
$layer = Layer::create([
'project_id' => $this->project->id, 'phase_id' => $phase->id,
'name' => 'L', 'color' => '#111', 'uploaded_by' => $this->user->id,
]);
return Feature::create([
'layer_id' => $layer->id, 'name' => 'Muro norte',
'geometry' => ['type' => 'Point', 'coordinates' => [-3.0, 40.0]],
'progress' => 0, 'status' => 'planned',
]);
}
// ── Notifications ────────────────────────────────────────────────────────────
public function test_assigning_a_task_notifies_the_assignee(): void
{
Notification::fake();
$issue = $this->makeIssue();
Livewire::actingAs($this->user)
->test(IssueDetail::class, ['project' => $this->project, 'issue' => $issue])
->set('newTaskTitle', 'Sanear zona')
->set('newTaskAssignee', $this->assignee->id)
->call('addTask');
Notification::assertSentTo($this->assignee, IssueTaskAssignedNotification::class);
}
public function test_commenting_notifies_the_assignee(): void
{
Notification::fake();
$issue = $this->makeIssue(['assigned_to' => $this->assignee->id]);
Livewire::actingAs($this->user)
->test(IssueDetail::class, ['project' => $this->project, 'issue' => $issue])
->set('newComment', 'Revisado en obra')
->call('addComment');
Notification::assertSentTo($this->assignee, IssueCommentedNotification::class);
}
public function test_resolving_notifies_stakeholders(): void
{
Notification::fake();
$issue = $this->makeIssue(['assigned_to' => $this->assignee->id]);
Livewire::actingAs($this->user)
->test(IssueDetail::class, ['project' => $this->project, 'issue' => $issue])
->call('verifyResolve');
Notification::assertSentTo($this->assignee, IssueStatusChangedNotification::class);
$this->assertEquals('resolved', $issue->fresh()->status);
}
// ── Checklist templates ────────────────────────────────────────────────────────
public function test_checklist_manager_creates_a_template(): void
{
Livewire::actingAs($this->user)
->test(IssueChecklistManager::class, ['project' => $this->project])
->set('name', 'Reparación grieta')
->set('items', ['Picar', 'Sellar', 'Pintar'])
->call('save');
$this->assertDatabaseHas('issue_checklist_templates', [
'project_id' => $this->project->id, 'name' => 'Reparación grieta',
]);
}
public function test_applying_a_template_creates_the_tasks(): void
{
$issue = $this->makeIssue();
$template = IssueChecklistTemplate::create([
'project_id' => $this->project->id,
'name' => 'Plantilla',
'items' => ['Picar', 'Sellar', 'Pintar'],
]);
Livewire::actingAs($this->user)
->test(IssueDetail::class, ['project' => $this->project, 'issue' => $issue])
->set('applyTemplateId', $template->id)
->call('applyTemplate');
$this->assertEquals(3, $issue->tasks()->count());
$this->assertEqualsCanonicalizing(['Picar', 'Sellar', 'Pintar'], $issue->tasks()->pluck('title')->all());
}
// ── Overdue alerts ───────────────────────────────────────────────────────────
public function test_overdue_command_notifies_assignee_once(): void
{
Notification::fake();
$issue = $this->makeIssue();
$task = $issue->tasks()->create([
'title' => 'Tarea vencida',
'assigned_to' => $this->assignee->id,
'due_date' => now()->subDays(2)->toDateString(),
'is_done' => false,
'uuid' => (string) \Illuminate\Support\Str::uuid(),
]);
$this->artisan('issues:notify-overdue')->assertSuccessful();
$this->artisan('issues:notify-overdue')->assertSuccessful(); // idempotent
Notification::assertSentTo($this->assignee, IssueTaskOverdueNotification::class);
Notification::assertCount(1);
$this->assertNotNull($task->fresh()->overdue_notified_at);
}
// ── Report issue from a map feature ──────────────────────────────────────────
public function test_issue_form_prelinks_feature_from_query(): void
{
$feature = $this->makeFeature();
// The create form mounts during the GET request, where it reads ?feature=.
$this->actingAs($this->user)
->get(route('projects.issues.create', ['project' => $this->project, 'feature' => $feature->id]))
->assertOk()
->assertSee('Muro norte')
->assertSee('Vinculada al elemento del mapa');
}
}