feat(phases): modal crear/editar fase + tabla Rappasoft
- PhaseList pasa a contenedor: botón "Agregar fase" + modal crear/editar con todos los parámetros (nombre, descripción, orden, color, progreso, fechas previstas y reales) y validación. Antes "Agregar fase" creaba directamente 'Nueva fase'. - PhaseTable (Rappasoft): orden, nombre+descripción, barra de progreso, fechas, color y acciones (editar abre el modal vía evento, actualizar progreso, eliminar); búsqueda y ordenación. Gateado por 'manage phases' + acceso al proyecto. Tests: PhaseManagementTest (4). Suite 65 passing (solo 2 pre-existentes sqlite). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+116
-15
@@ -2,40 +2,141 @@
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
use App\Models\Project;
|
||||
use App\Models\Phase;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class PhaseList extends Component
|
||||
{
|
||||
public Project $project;
|
||||
public $phases;
|
||||
|
||||
// Modal state
|
||||
public bool $showForm = false;
|
||||
public $editingId = null;
|
||||
|
||||
// Form fields
|
||||
public string $name = '';
|
||||
public string $description = '';
|
||||
public string $color = '#3b82f6';
|
||||
public int $order = 1;
|
||||
public int $progressPercent = 0;
|
||||
public string $plannedStart = '';
|
||||
public string $plannedEnd = '';
|
||||
public string $actualStart = '';
|
||||
public string $actualEnd = '';
|
||||
|
||||
public function mount(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
$this->phases = $project->phases;
|
||||
}
|
||||
|
||||
public function addPhase()
|
||||
protected function rules(): array
|
||||
{
|
||||
$this->project->phases()->create([
|
||||
'name' => 'Nueva fase',
|
||||
'order' => $this->phases->count() + 1,
|
||||
'color' => '#'.substr(md5(rand()), 0, 6)
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'nullable|string',
|
||||
'color' => 'required|string|max:7',
|
||||
'order' => 'required|integer|min:0',
|
||||
'progressPercent' => 'required|integer|min:0|max:100',
|
||||
'plannedStart' => 'nullable|date',
|
||||
'plannedEnd' => 'nullable|date|after_or_equal:plannedStart',
|
||||
'actualStart' => 'nullable|date',
|
||||
'actualEnd' => 'nullable|date|after_or_equal:actualStart',
|
||||
];
|
||||
}
|
||||
|
||||
protected $validationAttributes = [
|
||||
'name' => 'nombre',
|
||||
'color' => 'color',
|
||||
'order' => 'orden',
|
||||
'progressPercent' => 'progreso',
|
||||
'plannedStart' => 'inicio previsto',
|
||||
'plannedEnd' => 'fin previsto',
|
||||
'actualStart' => 'inicio real',
|
||||
'actualEnd' => 'fin real',
|
||||
];
|
||||
|
||||
public function openForm($phaseId = null): void
|
||||
{
|
||||
abort_unless(Auth::user()->can('manage phases'), 403);
|
||||
$this->resetForm();
|
||||
|
||||
if ($phaseId) {
|
||||
$phase = $this->project->phases()->findOrFail($phaseId);
|
||||
$this->editingId = $phase->id;
|
||||
$this->name = $phase->name;
|
||||
$this->description = $phase->description ?? '';
|
||||
$this->color = $phase->color ?? '#3b82f6';
|
||||
$this->order = (int) $phase->order;
|
||||
$this->progressPercent = (int) $phase->progress_percent;
|
||||
$this->plannedStart = $phase->planned_start?->format('Y-m-d') ?? '';
|
||||
$this->plannedEnd = $phase->planned_end?->format('Y-m-d') ?? '';
|
||||
$this->actualStart = $phase->actual_start?->format('Y-m-d') ?? '';
|
||||
$this->actualEnd = $phase->actual_end?->format('Y-m-d') ?? '';
|
||||
} else {
|
||||
$this->order = (int) $this->project->phases()->max('order') + 1;
|
||||
$this->color = '#' . substr(md5((string) rand()), 0, 6);
|
||||
}
|
||||
|
||||
$this->showForm = true;
|
||||
}
|
||||
|
||||
/** Opened from the table's edit button. */
|
||||
#[On('phase-edit')]
|
||||
public function editPhase($id): void
|
||||
{
|
||||
$this->openForm($id);
|
||||
}
|
||||
|
||||
public function closeForm(): void
|
||||
{
|
||||
$this->showForm = false;
|
||||
$this->resetForm();
|
||||
}
|
||||
|
||||
private function resetForm(): void
|
||||
{
|
||||
$this->reset([
|
||||
'editingId', 'name', 'description', 'plannedStart', 'plannedEnd', 'actualStart', 'actualEnd',
|
||||
]);
|
||||
$this->phases = $this->project->phases()->get();
|
||||
session()->flash('message', 'Fase agregada');
|
||||
$this->color = '#3b82f6';
|
||||
$this->order = 1;
|
||||
$this->progressPercent = 0;
|
||||
$this->resetErrorBag();
|
||||
}
|
||||
|
||||
public function deletePhase($phaseId)
|
||||
public function save(): void
|
||||
{
|
||||
Phase::find($phaseId)->delete();
|
||||
$this->phases = $this->project->phases()->get();
|
||||
abort_unless(Auth::user()->can('manage phases'), 403);
|
||||
$this->validate();
|
||||
|
||||
$data = [
|
||||
'name' => $this->name,
|
||||
'description' => $this->description ?: null,
|
||||
'color' => $this->color,
|
||||
'order' => $this->order,
|
||||
'progress_percent' => $this->progressPercent,
|
||||
'planned_start' => $this->plannedStart ?: null,
|
||||
'planned_end' => $this->plannedEnd ?: null,
|
||||
'actual_start' => $this->actualStart ?: null,
|
||||
'actual_end' => $this->actualEnd ?: null,
|
||||
];
|
||||
|
||||
if ($this->editingId) {
|
||||
$this->project->phases()->findOrFail($this->editingId)->update($data);
|
||||
} else {
|
||||
$this->project->phases()->create($data);
|
||||
}
|
||||
|
||||
$this->closeForm();
|
||||
$this->dispatch('phases-changed');
|
||||
$this->dispatch('notify', 'Fase guardada correctamente');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.phase-list');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Phase;
|
||||
use App\Models\Project;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Attributes\On;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
|
||||
class PhaseTable extends DataTableComponent
|
||||
{
|
||||
protected $model = Phase::class;
|
||||
|
||||
public int $projectId;
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->setPrimaryKey('id')
|
||||
->setDefaultSort('order', 'asc')
|
||||
->setSortingPillsEnabled(false)
|
||||
->setAdditionalSelects(['phases.id as id', 'phases.order as order']);
|
||||
}
|
||||
|
||||
/** Re-render when the parent (PhaseList) creates/edits a phase. */
|
||||
#[On('phases-changed')]
|
||||
public function refreshRows(): void
|
||||
{
|
||||
// no-op: triggers a re-render so the builder re-runs with fresh data.
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
$user = Auth::user();
|
||||
abort_unless(
|
||||
$user->can('manage all') || $user->can('edit projects') ||
|
||||
Project::whereKey($this->projectId)->whereHas('users', fn ($q) => $q->where('user_id', $user->id))->exists(),
|
||||
403
|
||||
);
|
||||
|
||||
return Phase::where('phases.project_id', $this->projectId);
|
||||
}
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Orden', 'order')->sortable(),
|
||||
|
||||
Column::make('Nombre', 'name')
|
||||
->sortable()
|
||||
->searchable()
|
||||
->format(function ($value, $row) {
|
||||
$html = '<span class="font-medium">'.e($value).'</span>';
|
||||
if ($row->description) {
|
||||
$html .= '<div class="text-xs text-base-content/50 truncate max-w-xs">'.e(\Illuminate\Support\Str::limit($row->description, 60)).'</div>';
|
||||
}
|
||||
return $html;
|
||||
})
|
||||
->html(),
|
||||
|
||||
Column::make('Progreso', 'progress_percent')
|
||||
->sortable()
|
||||
->format(fn ($value) =>
|
||||
'<div class="flex items-center gap-2 min-w-[110px]">
|
||||
<progress class="progress progress-primary w-24 h-2" value="'.(int) $value.'" max="100"></progress>
|
||||
<span class="text-xs text-base-content/60 w-8 text-right">'.(int) $value.'%</span>
|
||||
</div>')
|
||||
->html(),
|
||||
|
||||
Column::make('Fechas')
|
||||
->label(function ($row) {
|
||||
$ps = $row->planned_start?->format('d/m/Y');
|
||||
$pe = $row->planned_end?->format('d/m/Y');
|
||||
if (! $ps && ! $pe) {
|
||||
return '<span class="text-base-content/30 text-xs">—</span>';
|
||||
}
|
||||
return '<span class="text-xs">'.($ps ?: '?').' → '.($pe ?: '?').'</span>';
|
||||
})
|
||||
->html(),
|
||||
|
||||
Column::make('Color', 'color')
|
||||
->format(fn ($value) =>
|
||||
'<div class="w-6 h-6 rounded border border-base-300" style="background:'.e($value).'" title="'.e($value).'"></div>')
|
||||
->html(),
|
||||
|
||||
Column::make('Acciones')
|
||||
->label(function ($row) {
|
||||
$user = Auth::user();
|
||||
$progress = route('phases.progress', $row->id);
|
||||
|
||||
$html = '<div class="flex items-center justify-end gap-1">';
|
||||
$html .= '<a href="'.$progress.'" class="btn btn-xs btn-outline btn-info" title="Actualizar progreso" wire:navigate>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
|
||||
</a>';
|
||||
if ($user->can('manage phases')) {
|
||||
$html .= '<button wire:click="$dispatch(\'phase-edit\', { id: '.$row->id.' })" class="btn btn-xs btn-ghost" title="Editar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||
</button>';
|
||||
$html .= '<button wire:click="deletePhase('.$row->id.')" wire:confirm="¿Eliminar la fase \''.e($row->name).'\'? Esta acción no se puede deshacer."
|
||||
class="btn btn-xs btn-error btn-outline" title="Eliminar">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
</button>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
})
|
||||
->html(),
|
||||
];
|
||||
}
|
||||
|
||||
public function deletePhase(int $id): void
|
||||
{
|
||||
abort_unless(Auth::user()->can('manage phases'), 403);
|
||||
Phase::where('project_id', $this->projectId)->findOrFail($id)->delete();
|
||||
$this->dispatch('phases-changed');
|
||||
$this->dispatch('notify', 'Fase eliminada');
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,115 @@
|
||||
<div>
|
||||
@if(session()->has('message'))
|
||||
<div class="alert alert-success mb-2">{{ session('message') }}</div>
|
||||
@endif
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr><th>{{ __('Name') }}</th><th>{{ __('Progress') }}</th><th>{{ __('Color') }}</th><th>{{ __('Actions') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($phases as $phase)
|
||||
<tr>
|
||||
<td>{{ $phase->name }}</td>
|
||||
<td>
|
||||
<div class="w-32 bg-gray-200 rounded-full h-2">
|
||||
<div class="bg-primary h-2 rounded-full" style="width: {{ $phase->progress_percent }}%"></div>
|
||||
<div class="flex items-center justify-between gap-3 mb-4">
|
||||
<div>
|
||||
<h3 class="font-bold">{{ __('Phases') }}</h3>
|
||||
<p class="text-sm text-base-content/60">Fases del proyecto y su progreso</p>
|
||||
</div>
|
||||
@can('manage phases')
|
||||
<button wire:click="openForm()" class="btn btn-primary btn-sm gap-2">
|
||||
<x-heroicon-o-plus class="w-4 h-4" /> {{ __('Add Phase') }}
|
||||
</button>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
{{-- Tabla Rappasoft de fases --}}
|
||||
<livewire:phase-table :project-id="$project->id" :key="'phase-table-'.$project->id" />
|
||||
|
||||
{{-- ================================================================
|
||||
MODAL crear / editar fase
|
||||
================================================================ --}}
|
||||
@if($showForm)
|
||||
<div class="fixed inset-0 z-40 bg-black/50" wire:click="closeForm"></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-2xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between p-5 border-b border-base-300">
|
||||
<h3 class="text-lg font-bold">{{ $editingId ? 'Editar fase' : 'Nueva fase' }}</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>
|
||||
|
||||
<form wire:submit.prevent="save" class="p-5 space-y-4">
|
||||
{{-- Nombre --}}
|
||||
<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" autofocus
|
||||
class="input input-bordered w-full @error('name') input-error @enderror"
|
||||
placeholder="Ej.: Cimentación" />
|
||||
@error('name')<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-20 resize-y"
|
||||
placeholder="Detalle de la fase..."></textarea>
|
||||
</div>
|
||||
|
||||
{{-- Orden + Color + Progreso --}}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-medium">Orden <span class="text-error">*</span></span></label>
|
||||
<input type="number" min="0" wire:model="order"
|
||||
class="input input-bordered w-full @error('order') input-error @enderror" />
|
||||
@error('order')<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>@enderror
|
||||
</div>
|
||||
{{ $phase->progress_percent }}%
|
||||
</td>
|
||||
<td><div class="w-6 h-6 rounded" style="background: {{ $phase->color }}"></div></td>
|
||||
<td>
|
||||
<a href="{{ route('phases.progress', $phase) }}" class="btn btn-xs btn-info">{{ __('Update') }}</a>
|
||||
<button wire:click="deletePhase({{ $phase->id }})" class="btn btn-xs btn-error">{{ __('Delete') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<button wire:click="addPhase" class="btn btn-sm btn-secondary mt-2">+ {{ __('Add Phase') }}</button>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text font-medium">Color</span></label>
|
||||
<input type="color" wire:model="color"
|
||||
class="input input-bordered w-full h-12 p-1 @error('color') input-error @enderror" />
|
||||
@error('color')<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">Progreso (%)</span></label>
|
||||
<input type="number" min="0" max="100" wire:model="progressPercent"
|
||||
class="input input-bordered w-full @error('progressPercent') input-error @enderror" />
|
||||
@error('progressPercent')<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Fechas previstas --}}
|
||||
<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">Inicio previsto</span></label>
|
||||
<input type="date" wire:model="plannedStart"
|
||||
class="input input-bordered w-full @error('plannedStart') input-error @enderror" />
|
||||
@error('plannedStart')<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">Fin previsto</span></label>
|
||||
<input type="date" wire:model="plannedEnd"
|
||||
class="input input-bordered w-full @error('plannedEnd') input-error @enderror" />
|
||||
@error('plannedEnd')<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Fechas reales --}}
|
||||
<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">Inicio real</span></label>
|
||||
<input type="date" wire:model="actualStart"
|
||||
class="input input-bordered w-full @error('actualStart') input-error @enderror" />
|
||||
@error('actualStart')<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">Fin real</span></label>
|
||||
<input type="date" wire:model="actualEnd"
|
||||
class="input input-bordered w-full @error('actualEnd') input-error @enderror" />
|
||||
@error('actualEnd')<label class="label"><span class="label-text-alt text-error">{{ $message }}</span></label>@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" class="btn btn-primary gap-2" wire:loading.attr="disabled" wire:target="save">
|
||||
<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>
|
||||
{{ $editingId ? 'Actualizar fase' : 'Crear fase' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Livewire\PhaseList;
|
||||
use App\Livewire\PhaseTable;
|
||||
use App\Models\Phase;
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PhaseManagementTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private User $user;
|
||||
private Project $project;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
foreach (['manage phases', 'edit projects'] as $p) {
|
||||
Permission::findOrCreate($p);
|
||||
}
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->user->givePermissionTo(['manage phases', 'edit projects']);
|
||||
|
||||
$this->project = Project::create([
|
||||
'reference' => 'PH-1',
|
||||
'name' => 'Proyecto Fases',
|
||||
'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']);
|
||||
}
|
||||
|
||||
public function test_create_phase_via_modal_persists_all_fields(): void
|
||||
{
|
||||
Livewire::actingAs($this->user)
|
||||
->test(PhaseList::class, ['project' => $this->project])
|
||||
->call('openForm')
|
||||
->set('name', 'Cimentación')
|
||||
->set('description', 'Zapatas y muros')
|
||||
->set('color', '#ff0000')
|
||||
->set('order', 2)
|
||||
->set('progressPercent', 30)
|
||||
->set('plannedStart', '2026-07-01')
|
||||
->set('plannedEnd', '2026-08-01')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('phases', [
|
||||
'project_id' => $this->project->id,
|
||||
'name' => 'Cimentación',
|
||||
'color' => '#ff0000',
|
||||
'order' => 2,
|
||||
'progress_percent' => 30,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_edit_phase_loads_and_updates(): void
|
||||
{
|
||||
$phase = $this->project->phases()->create([
|
||||
'name' => 'Vieja', 'order' => 1, 'color' => '#3b82f6', 'progress_percent' => 0,
|
||||
]);
|
||||
|
||||
Livewire::actingAs($this->user)
|
||||
->test(PhaseList::class, ['project' => $this->project])
|
||||
->call('editPhase', $phase->id)
|
||||
->assertSet('name', 'Vieja')
|
||||
->set('name', 'Nueva')
|
||||
->set('progressPercent', 80)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$phase->refresh();
|
||||
$this->assertEquals('Nueva', $phase->name);
|
||||
$this->assertEquals(80, $phase->progress_percent);
|
||||
}
|
||||
|
||||
public function test_phase_table_lists_and_deletes(): void
|
||||
{
|
||||
$phase = $this->project->phases()->create([
|
||||
'name' => 'Estructura', 'order' => 1, 'color' => '#3b82f6', 'progress_percent' => 0,
|
||||
]);
|
||||
|
||||
Livewire::actingAs($this->user)
|
||||
->test(PhaseTable::class, ['projectId' => $this->project->id])
|
||||
->assertOk()
|
||||
->assertSee('Estructura')
|
||||
->call('deletePhase', $phase->id);
|
||||
|
||||
$this->assertDatabaseMissing('phases', ['id' => $phase->id, 'deleted_at' => null]);
|
||||
}
|
||||
|
||||
public function test_create_phase_requires_manage_phases_permission(): void
|
||||
{
|
||||
$outsider = User::factory()->create();
|
||||
$this->project->users()->attach($outsider->id, ['role_in_project' => 'viewer']);
|
||||
|
||||
Livewire::actingAs($outsider)
|
||||
->test(PhaseList::class, ['project' => $this->project])
|
||||
->call('openForm')
|
||||
->assertForbidden();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user