276 lines
15 KiB
PHP
276 lines
15 KiB
PHP
|
|
<div class="p-4 space-y-4">
|
|||
|
|
|
|||
|
|
{{-- Page header --}}
|
|||
|
|
<div class="flex items-center justify-between flex-wrap gap-2">
|
|||
|
|
<div class="flex items-center gap-3">
|
|||
|
|
<a href="{{ route('projects.map', $project) }}"
|
|||
|
|
class="btn btn-sm btn-ghost gap-1">
|
|||
|
|
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
|||
|
|
{{ __('Back to Map') }}
|
|||
|
|
</a>
|
|||
|
|
<h1 class="text-xl font-bold">{{ __('Cronograma') }}: {{ $project->name }}</h1>
|
|||
|
|
</div>
|
|||
|
|
<div class="flex items-center gap-3">
|
|||
|
|
<a href="{{ route('projects.report', $project) }}"
|
|||
|
|
target="_blank"
|
|||
|
|
class="btn btn-sm btn-outline gap-1">
|
|||
|
|
<x-heroicon-o-document-text class="w-4 h-4" />
|
|||
|
|
{{ __('Report PDF') }}
|
|||
|
|
</a>
|
|||
|
|
<span class="text-sm text-base-content/60">
|
|||
|
|
{{ $project->start_date?->format('d/m/Y') ?? __('N/A') }}
|
|||
|
|
—
|
|||
|
|
{{ $project->end_date_estimated?->format('d/m/Y') ?? __('N/A') }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- Legend --}}
|
|||
|
|
<div class="flex items-center gap-4 text-sm flex-wrap">
|
|||
|
|
<span class="flex items-center gap-1.5">
|
|||
|
|
<span class="inline-block w-5 h-3 rounded" style="background:#3b82f6"></span>
|
|||
|
|
{{ __('Planificado') }}
|
|||
|
|
</span>
|
|||
|
|
<span class="flex items-center gap-1.5">
|
|||
|
|
<span class="inline-block w-5 h-3 rounded" style="background:#22c55e"></span>
|
|||
|
|
{{ __('Real') }}
|
|||
|
|
</span>
|
|||
|
|
<span class="flex items-center gap-1.5">
|
|||
|
|
<span class="inline-block w-5 h-3 rounded border-2" style="background:#fee2e2;border-color:#ef4444"></span>
|
|||
|
|
{{ __('Retrasado') }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- Editor de fechas por fase (siempre visible) --}}
|
|||
|
|
<div class="bg-base-100 rounded-box border border-base-300 p-4 mb-4">
|
|||
|
|
<h3 class="font-semibold text-sm mb-3">Fechas planificadas y reales por fase</h3>
|
|||
|
|
<div class="space-y-3">
|
|||
|
|
@foreach($phases as $phase)
|
|||
|
|
<div x-data="{
|
|||
|
|
ps: '{{ $phase->planned_start?->format('Y-m-d') ?? '' }}',
|
|||
|
|
pe: '{{ $phase->planned_end?->format('Y-m-d') ?? '' }}',
|
|||
|
|
as_: '{{ $phase->actual_start?->format('Y-m-d') ?? '' }}',
|
|||
|
|
ae: '{{ $phase->actual_end?->format('Y-m-d') ?? '' }}'
|
|||
|
|
}" class="grid grid-cols-2 md:grid-cols-5 gap-2 items-center text-sm border-b pb-3 last:border-0">
|
|||
|
|
<div class="font-medium truncate flex items-center gap-1">
|
|||
|
|
<span class="w-2 h-2 rounded-full inline-block flex-shrink-0" style="background:{{ $phase->color ?? '#3b82f6' }}"></span>
|
|||
|
|
{{ $phase->name }}
|
|||
|
|
</div>
|
|||
|
|
<div class="form-control">
|
|||
|
|
<label class="label-text text-xs text-gray-500">Plan. inicio</label>
|
|||
|
|
<input type="date" x-model="ps" class="input input-xs input-bordered" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-control">
|
|||
|
|
<label class="label-text text-xs text-gray-500">Plan. fin</label>
|
|||
|
|
<input type="date" x-model="pe" class="input input-xs input-bordered" />
|
|||
|
|
</div>
|
|||
|
|
<div class="form-control">
|
|||
|
|
<label class="label-text text-xs text-gray-500">Real inicio</label>
|
|||
|
|
<input type="date" x-model="as_" class="input input-xs input-bordered" />
|
|||
|
|
</div>
|
|||
|
|
<div class="flex flex-col gap-1">
|
|||
|
|
<label class="label-text text-xs text-gray-500">Real fin</label>
|
|||
|
|
<div class="flex gap-1">
|
|||
|
|
<input type="date" x-model="ae" class="input input-xs input-bordered flex-1" />
|
|||
|
|
<button @click="$wire.updatePhaseDates({{ $phase->id }}, ps, pe, as_, ae)"
|
|||
|
|
class="btn btn-xs btn-primary">
|
|||
|
|
✓
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
@endforeach
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
@if(empty($ganttData))
|
|||
|
|
<div class="alert alert-info">
|
|||
|
|
<x-heroicon-o-information-circle class="w-5 h-5" />
|
|||
|
|
<span>Define fechas planificadas arriba para ver el diagrama.</span>
|
|||
|
|
</div>
|
|||
|
|
@else
|
|||
|
|
{{-- Gantt table --}}
|
|||
|
|
<div class="bg-base-100 rounded-box border border-base-300 overflow-x-auto">
|
|||
|
|
<table class="w-full text-sm" style="min-width:900px;">
|
|||
|
|
<thead>
|
|||
|
|
<tr class="border-b border-base-300">
|
|||
|
|
{{-- Phase name column --}}
|
|||
|
|
<th class="text-left px-3 py-2 font-semibold bg-base-200" style="width:200px;min-width:200px;">
|
|||
|
|
{{ __('Fase') }}
|
|||
|
|
</th>
|
|||
|
|
|
|||
|
|
{{-- Month header row --}}
|
|||
|
|
<th class="px-0 py-0 bg-base-200" style="min-width:400px;">
|
|||
|
|
@php
|
|||
|
|
$projectStart = $project->start_date ?? now()->startOfMonth();
|
|||
|
|
$projectEnd = $project->end_date_estimated ?? now()->addMonths(6);
|
|||
|
|
$totalDays = max(1, $projectStart->diffInDays($projectEnd));
|
|||
|
|
|
|||
|
|
// Build month segments
|
|||
|
|
$months = [];
|
|||
|
|
$cursor = $projectStart->copy()->startOfMonth();
|
|||
|
|
while ($cursor->lte($projectEnd)) {
|
|||
|
|
$mStart = $cursor->copy()->max($projectStart);
|
|||
|
|
$mEnd = $cursor->copy()->endOfMonth()->min($projectEnd);
|
|||
|
|
$days = max(1, $mStart->diffInDays($mEnd) + 1);
|
|||
|
|
$widthPct = round(($days / $totalDays) * 100, 2);
|
|||
|
|
$months[] = [
|
|||
|
|
'label' => $cursor->translatedFormat('M Y'),
|
|||
|
|
'width_pct' => $widthPct,
|
|||
|
|
];
|
|||
|
|
$cursor->addMonthNoOverflow();
|
|||
|
|
}
|
|||
|
|
@endphp
|
|||
|
|
<div class="flex w-full border-b border-base-300">
|
|||
|
|
@foreach($months as $month)
|
|||
|
|
<div class="text-center text-xs py-1 font-medium border-r border-base-300 last:border-r-0 truncate"
|
|||
|
|
style="width:{{ $month['width_pct'] }}%;flex-shrink:0;">
|
|||
|
|
{{ $month['label'] }}
|
|||
|
|
</div>
|
|||
|
|
@endforeach
|
|||
|
|
</div>
|
|||
|
|
</th>
|
|||
|
|
|
|||
|
|
{{-- Dates column --}}
|
|||
|
|
<th class="text-left px-3 py-2 font-semibold bg-base-200 whitespace-nowrap" style="width:160px;min-width:160px;">
|
|||
|
|
{{ __('Fechas') }}
|
|||
|
|
</th>
|
|||
|
|
|
|||
|
|
{{-- Status column --}}
|
|||
|
|
<th class="text-center px-3 py-2 font-semibold bg-base-200" style="width:110px;min-width:110px;">
|
|||
|
|
{{ __('Estado') }}
|
|||
|
|
</th>
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
<tbody>
|
|||
|
|
@foreach($ganttData as $phase)
|
|||
|
|
<tr class="border-b border-base-300 hover:bg-base-50 transition-colors {{ $phase['is_delayed'] ? 'bg-red-50' : '' }}">
|
|||
|
|
|
|||
|
|
{{-- Phase name --}}
|
|||
|
|
<td class="px-3 py-3" style="width:200px;min-width:200px;vertical-align:middle;">
|
|||
|
|
<div class="flex items-center gap-2">
|
|||
|
|
<span class="inline-block w-3 h-3 rounded-full flex-shrink-0"
|
|||
|
|
style="background:{{ $phase['color'] }}"></span>
|
|||
|
|
<span class="font-medium truncate" title="{{ $phase['name'] }}">
|
|||
|
|
{{ $phase['name'] }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
@if($phase['features_count'] > 0)
|
|||
|
|
<div class="ml-5 text-xs text-base-content/50 mt-0.5">
|
|||
|
|
{{ $phase['features_count'] }} {{ __('elementos') }}
|
|||
|
|
</div>
|
|||
|
|
@endif
|
|||
|
|
</td>
|
|||
|
|
|
|||
|
|
{{-- Gantt bar cell --}}
|
|||
|
|
<td class="px-0 py-3" style="vertical-align:middle;">
|
|||
|
|
<div class="relative w-full" style="height:36px;">
|
|||
|
|
|
|||
|
|
{{-- Month grid lines --}}
|
|||
|
|
@php $offset = 0; @endphp
|
|||
|
|
@foreach($months as $i => $month)
|
|||
|
|
@if($i > 0)
|
|||
|
|
<div class="absolute top-0 bottom-0 border-l border-base-300/50"
|
|||
|
|
style="left:{{ $offset }}%;"></div>
|
|||
|
|
@endif
|
|||
|
|
@php $offset += $month['width_pct']; @endphp
|
|||
|
|
@endforeach
|
|||
|
|
|
|||
|
|
{{-- Planned bar --}}
|
|||
|
|
<div class="absolute rounded"
|
|||
|
|
style="
|
|||
|
|
top: 4px;
|
|||
|
|
height: 13px;
|
|||
|
|
left: {{ $phase['p_start_pct'] }}%;
|
|||
|
|
width: {{ max(0.5, $phase['p_width_pct']) }}%;
|
|||
|
|
background: {{ $phase['is_delayed'] ? '#fca5a5' : $phase['color'] }};
|
|||
|
|
border: {{ $phase['is_delayed'] ? '2px solid #ef4444' : 'none' }};
|
|||
|
|
opacity: 0.85;
|
|||
|
|
"
|
|||
|
|
title="{{ __('Planificado') }}: {{ $phase['planned_start'] }} - {{ $phase['planned_end'] }}">
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- Actual bar (if exists) --}}
|
|||
|
|
@if($phase['a_start_pct'] !== null && $phase['a_width_pct'] !== null)
|
|||
|
|
<div class="absolute rounded"
|
|||
|
|
style="
|
|||
|
|
top: 19px;
|
|||
|
|
height: 13px;
|
|||
|
|
left: {{ $phase['a_start_pct'] }}%;
|
|||
|
|
width: {{ max(0.5, $phase['a_width_pct']) }}%;
|
|||
|
|
background: #22c55e;
|
|||
|
|
opacity: 0.85;
|
|||
|
|
"
|
|||
|
|
title="{{ __('Real') }}: {{ $phase['actual_start'] }} - {{ $phase['actual_end'] ?? __('En curso') }}">
|
|||
|
|
</div>
|
|||
|
|
@endif
|
|||
|
|
|
|||
|
|
{{-- Progress label --}}
|
|||
|
|
<div class="absolute inset-0 flex items-center"
|
|||
|
|
style="left: {{ $phase['p_start_pct'] }}%; width: {{ max(0.5, $phase['p_width_pct']) }}%;">
|
|||
|
|
<span class="text-xs font-bold text-white drop-shadow px-1 truncate"
|
|||
|
|
style="font-size:10px; line-height:13px; position:absolute; top:4px; left:2px;">
|
|||
|
|
{{ $phase['progress'] }}%
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
|
|||
|
|
{{-- Dates column --}}
|
|||
|
|
<td class="px-3 py-3 text-xs" style="width:160px;min-width:160px;vertical-align:middle;">
|
|||
|
|
<div class="space-y-0.5">
|
|||
|
|
<div class="flex items-center gap-1">
|
|||
|
|
<span class="inline-block w-2 h-2 rounded-full flex-shrink-0" style="background:{{ $phase['color'] }}"></span>
|
|||
|
|
<span class="text-base-content/70">{{ $phase['planned_start'] }} – {{ $phase['planned_end'] }}</span>
|
|||
|
|
</div>
|
|||
|
|
@if($phase['actual_start'])
|
|||
|
|
<div class="flex items-center gap-1">
|
|||
|
|
<span class="inline-block w-2 h-2 rounded-full flex-shrink-0" style="background:#22c55e"></span>
|
|||
|
|
<span class="text-base-content/70">
|
|||
|
|
{{ $phase['actual_start'] }} – {{ $phase['actual_end'] ?? __('En curso') }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
@endif
|
|||
|
|
</div>
|
|||
|
|
</td>
|
|||
|
|
|
|||
|
|
{{-- Status badge --}}
|
|||
|
|
<td class="px-3 py-3 text-center" style="width:110px;min-width:110px;vertical-align:middle;">
|
|||
|
|
@if($phase['is_delayed'])
|
|||
|
|
<span class="badge badge-error badge-sm gap-1">
|
|||
|
|
<x-heroicon-o-exclamation-triangle class="w-3 h-3" />
|
|||
|
|
{{ __('En retraso') }}
|
|||
|
|
</span>
|
|||
|
|
@elseif($phase['progress'] >= 100)
|
|||
|
|
<span class="badge badge-success badge-sm gap-1">
|
|||
|
|
<x-heroicon-o-check-circle class="w-3 h-3" />
|
|||
|
|
{{ __('Completado') }}
|
|||
|
|
</span>
|
|||
|
|
@elseif($phase['progress'] > 0)
|
|||
|
|
<span class="badge badge-info badge-sm">
|
|||
|
|
{{ $phase['progress'] }}%
|
|||
|
|
</span>
|
|||
|
|
@else
|
|||
|
|
<span class="badge badge-ghost badge-sm">
|
|||
|
|
{{ __('Pendiente') }}
|
|||
|
|
</span>
|
|||
|
|
@endif
|
|||
|
|
</td>
|
|||
|
|
|
|||
|
|
</tr>
|
|||
|
|
@endforeach
|
|||
|
|
</tbody>
|
|||
|
|
</table>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{{-- Summary footer --}}
|
|||
|
|
<div class="text-xs text-base-content/50 text-right">
|
|||
|
|
{{ count($ganttData) }} {{ __('fases') }}
|
|||
|
|
•
|
|||
|
|
{{ __('Actualizado') }}: {{ now()->format('d/m/Y H:i') }}
|
|||
|
|
</div>
|
|||
|
|
@endif
|
|||
|
|
|
|||
|
|
</div>
|