6e66f707d5
Full restore of the7d854ffsnapshot (2026-06-16 18:05, before the security review). Forward commit, no history rewrite —f8a1310and all later commits remain recoverable in history. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
397 lines
22 KiB
PHP
397 lines
22 KiB
PHP
<div>
|
||
<x-slot name="header">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center gap-3">
|
||
<a href="{{ route('projects.list') }}" class="btn btn-ghost btn-sm px-2">
|
||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||
</a>
|
||
<div>
|
||
<h2 class="font-bold text-xl leading-tight">{{ $project->name }}</h2>
|
||
@if($project->description)
|
||
<p class="text-sm text-gray-500 leading-tight mt-0.5">{{ Str::limit($project->description, 80) }}</p>
|
||
@endif
|
||
</div>
|
||
@php
|
||
$statusCfg = match($project->status) {
|
||
'in_progress' => ['badge-primary', 'En progreso'],
|
||
'completed' => ['badge-success', 'Completado'],
|
||
'paused' => ['badge-warning', 'Pausado'],
|
||
'planning' => ['badge-ghost', 'Planificación'],
|
||
default => ['badge-ghost', ucfirst(str_replace('_',' ',$project->status))],
|
||
};
|
||
@endphp
|
||
<span class="badge {{ $statusCfg[0] }} badge-sm">{{ $statusCfg[1] }}</span>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<a href="{{ route('projects.map', $project) }}" class="btn btn-outline btn-sm gap-1">
|
||
<x-heroicon-o-map class="w-4 h-4" />
|
||
Mapa
|
||
</a>
|
||
<a href="{{ route('projects.gantt', $project) }}" class="btn btn-outline btn-sm gap-1">
|
||
<x-heroicon-o-calendar-days class="w-4 h-4" />
|
||
Gantt
|
||
</a>
|
||
<a href="{{ route('projects.issues', $project) }}" class="btn btn-outline btn-sm gap-1">
|
||
<x-heroicon-o-exclamation-triangle class="w-4 h-4" />
|
||
Issues
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</x-slot>
|
||
|
||
<div class="py-6">
|
||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||
|
||
{{-- ── KPIs fila 1 ────────────────────────────────────────────────────── --}}
|
||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||
|
||
{{-- Avance global --}}
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex-1 min-w-0">
|
||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Avance global</p>
|
||
<p class="mt-1 text-3xl font-bold text-green-600">{{ $stats['global_progress'] }}%</p>
|
||
<div class="mt-2 w-full bg-gray-200 rounded-full h-1.5">
|
||
<div class="bg-green-500 h-1.5 rounded-full transition-all" style="width: {{ $stats['global_progress'] }}%"></div>
|
||
</div>
|
||
</div>
|
||
<div class="p-3 bg-green-100 rounded-full ml-3 shrink-0">
|
||
<x-heroicon-o-chart-bar class="w-5 h-5 text-green-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Fases --}}
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Fases</p>
|
||
<p class="mt-1 text-3xl font-bold text-blue-600">{{ $stats['total_phases'] }}</p>
|
||
@if($stats['delayed_phases'] > 0)
|
||
<p class="text-xs text-red-500 mt-0.5">{{ $stats['delayed_phases'] }} con retraso</p>
|
||
@else
|
||
<p class="text-xs text-gray-400 mt-0.5">Sin retrasos</p>
|
||
@endif
|
||
</div>
|
||
<div class="p-3 {{ $stats['delayed_phases'] > 0 ? 'bg-red-100' : 'bg-blue-100' }} rounded-full">
|
||
<x-heroicon-o-rectangle-stack class="w-5 h-5 {{ $stats['delayed_phases'] > 0 ? 'text-red-500' : 'text-blue-600' }}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Elementos --}}
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Elementos</p>
|
||
<p class="mt-1 text-3xl font-bold text-indigo-600">{{ $stats['total_features'] }}</p>
|
||
<p class="text-xs text-gray-400 mt-0.5">
|
||
{{ $stats['completed_features'] }} completados
|
||
· {{ $stats['verified_features'] }} verificados
|
||
</p>
|
||
</div>
|
||
<div class="p-3 bg-indigo-100 rounded-full">
|
||
<x-heroicon-o-map-pin class="w-5 h-5 text-indigo-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Issues --}}
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Issues abiertos</p>
|
||
<p class="mt-1 text-3xl font-bold {{ $stats['open_issues'] > 0 ? 'text-orange-600' : 'text-gray-400' }}">
|
||
{{ $stats['open_issues'] }}
|
||
</p>
|
||
@if($stats['critical_issues'] > 0)
|
||
<p class="text-xs text-red-600 font-semibold mt-0.5">{{ $stats['critical_issues'] }} críticos</p>
|
||
@else
|
||
<p class="text-xs text-gray-400 mt-0.5">0 críticos</p>
|
||
@endif
|
||
</div>
|
||
<div class="p-3 {{ $stats['open_issues'] > 0 ? 'bg-orange-100' : 'bg-gray-100' }} rounded-full">
|
||
<x-heroicon-o-exclamation-triangle class="w-5 h-5 {{ $stats['open_issues'] > 0 ? 'text-orange-600' : 'text-gray-400' }}" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
{{-- ── KPIs fila 2: Inspecciones ────────────────────────────────────────── --}}
|
||
<div class="grid grid-cols-3 gap-4">
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-4 flex-row items-center gap-4">
|
||
<div class="p-3 bg-gray-100 rounded-full shrink-0">
|
||
<x-heroicon-o-clipboard-document-list class="w-5 h-5 text-gray-600" />
|
||
</div>
|
||
<div>
|
||
<p class="text-xs text-gray-500 uppercase tracking-wider">Total inspecciones</p>
|
||
<p class="text-2xl font-bold">{{ $stats['total_inspections'] }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-4 flex-row items-center gap-4">
|
||
<div class="p-3 bg-green-100 rounded-full shrink-0">
|
||
<x-heroicon-o-check-circle class="w-5 h-5 text-green-600" />
|
||
</div>
|
||
<div>
|
||
<p class="text-xs text-gray-500 uppercase tracking-wider">Aprobadas</p>
|
||
<p class="text-2xl font-bold text-green-600">{{ $stats['passed_inspections'] }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-4 flex-row items-center gap-4">
|
||
<div class="p-3 {{ $stats['failed_inspections'] > 0 ? 'bg-red-100' : 'bg-gray-100' }} rounded-full shrink-0">
|
||
<x-heroicon-o-x-circle class="w-5 h-5 {{ $stats['failed_inspections'] > 0 ? 'text-red-600' : 'text-gray-400' }}" />
|
||
</div>
|
||
<div>
|
||
<p class="text-xs text-gray-500 uppercase tracking-wider">Rechazadas</p>
|
||
<p class="text-2xl font-bold {{ $stats['failed_inspections'] > 0 ? 'text-red-600' : 'text-gray-400' }}">
|
||
{{ $stats['failed_inspections'] }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- ── Main grid: fases + actividad reciente ───────────────────────────── --}}
|
||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||
|
||
{{-- LEFT 2/3: Fases con progreso --}}
|
||
<div class="lg:col-span-2 space-y-4">
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<div class="flex justify-between items-center mb-4">
|
||
<h3 class="font-semibold text-base flex items-center gap-2">
|
||
<x-heroicon-o-rectangle-stack class="w-4 h-4" />
|
||
Fases del proyecto
|
||
</h3>
|
||
<a href="{{ route('projects.gantt', $project) }}" class="btn btn-xs btn-outline gap-1">
|
||
<x-heroicon-o-calendar-days class="w-3.5 h-3.5" />
|
||
Gantt
|
||
</a>
|
||
</div>
|
||
|
||
@if($phases->isEmpty())
|
||
<p class="text-sm text-gray-400 text-center py-6">Sin fases aún.</p>
|
||
@else
|
||
<div class="space-y-3">
|
||
@foreach($phases as $phase)
|
||
@php
|
||
$pct = round($phase->progress_percent ?? 0);
|
||
$isDelayed = $phase->planned_end && $phase->planned_end < now() && $pct < 100;
|
||
$barColor = $isDelayed ? 'bg-red-500' : ($pct >= 100 ? 'bg-green-500' : 'bg-blue-500');
|
||
$featureCount = $phase->layers->sum('features_count');
|
||
@endphp
|
||
<div class="border border-base-200 rounded-lg p-3 hover:border-primary/40 transition-colors">
|
||
<div class="flex items-start justify-between gap-2 mb-2">
|
||
<div class="flex-1 min-w-0">
|
||
<p class="font-semibold text-sm truncate">{{ $phase->name }}</p>
|
||
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-500 mt-0.5">
|
||
<span>{{ $phase->layers_count }} capa(s)</span>
|
||
<span>·</span>
|
||
<span>{{ $featureCount }} elementos</span>
|
||
@if($phase->planned_start && $phase->planned_end)
|
||
<span>·</span>
|
||
<span>{{ $phase->planned_start->format('d/m/y') }} – {{ $phase->planned_end->format('d/m/y') }}</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-2 shrink-0">
|
||
@if($isDelayed)
|
||
<span class="badge badge-error badge-xs">Retraso</span>
|
||
@elseif($pct >= 100)
|
||
<span class="badge badge-success badge-xs">Completada</span>
|
||
@endif
|
||
<span class="text-sm font-bold {{ $isDelayed ? 'text-red-600' : ($pct >= 100 ? 'text-green-600' : 'text-blue-600') }}">
|
||
{{ $pct }}%
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||
<div class="{{ $barColor }} h-2 rounded-full transition-all" style="width: {{ $pct }}%"></div>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Empresas participantes --}}
|
||
@if($companies->isNotEmpty())
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<h3 class="font-semibold text-base flex items-center gap-2 mb-3">
|
||
<x-heroicon-o-building-office-2 class="w-4 h-4" />
|
||
Empresas participantes
|
||
</h3>
|
||
<div class="flex flex-wrap gap-3">
|
||
@foreach($companies as $company)
|
||
<div class="flex items-center gap-2 border border-base-200 rounded-lg px-3 py-2">
|
||
@if($company->logo_path && \Illuminate\Support\Facades\Storage::disk('public')->exists($company->logo_path))
|
||
<img src="{{ \Illuminate\Support\Facades\Storage::disk('public')->url($company->logo_path) }}"
|
||
alt="" class="w-7 h-7 object-contain rounded" />
|
||
@else
|
||
<x-heroicon-o-building-office class="w-5 h-5 opacity-40" />
|
||
@endif
|
||
<div>
|
||
<p class="text-xs font-semibold leading-tight">{{ $company->apodo ?: $company->name }}</p>
|
||
@if($company->pivot->role_in_project)
|
||
<p class="text-xs text-gray-400">{{ $company->pivot->role_in_project }}</p>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
|
||
{{-- RIGHT 1/3: Actividad reciente --}}
|
||
<div class="space-y-5">
|
||
|
||
{{-- Equipo --}}
|
||
@if($teamMembers->isNotEmpty())
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<h3 class="font-semibold text-base flex items-center gap-2 mb-3">
|
||
<x-heroicon-o-users class="w-4 h-4" />
|
||
Equipo ({{ $teamMembers->count() }})
|
||
</h3>
|
||
<div class="space-y-2">
|
||
@foreach($teamMembers->take(8) as $member)
|
||
<div class="flex items-center gap-2">
|
||
<div class="avatar placeholder shrink-0">
|
||
<div class="bg-neutral text-neutral-content rounded-full w-7">
|
||
<span class="text-xs">{{ strtoupper(substr($member->name, 0, 1)) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<p class="text-sm font-medium truncate">{{ $member->name }}</p>
|
||
@if($member->pivot->role_in_project)
|
||
<p class="text-xs text-gray-400 truncate">{{ $member->pivot->role_in_project }}</p>
|
||
@endif
|
||
</div>
|
||
@foreach($member->roles->take(1) as $role)
|
||
<span class="badge badge-xs {{ $role->name === 'Admin' ? 'badge-error' : 'badge-ghost' }}">{{ $role->name }}</span>
|
||
@endforeach
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Issues recientes --}}
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="font-semibold text-base flex items-center gap-2">
|
||
<x-heroicon-o-exclamation-triangle class="w-4 h-4 text-orange-500" />
|
||
Issues abiertos
|
||
</h3>
|
||
<a href="{{ route('projects.issues', $project) }}" class="btn btn-xs btn-outline">Ver todos</a>
|
||
</div>
|
||
@if($recentIssues->isEmpty())
|
||
<div class="text-center py-4 text-gray-400">
|
||
<x-heroicon-o-check-circle class="w-7 h-7 mx-auto mb-1 opacity-25" />
|
||
<p class="text-xs">Sin issues abiertos</p>
|
||
</div>
|
||
@else
|
||
<div class="space-y-2">
|
||
@foreach($recentIssues as $issue)
|
||
@php
|
||
$pCfg = match($issue->priority ?? 'medium') {
|
||
'critical' => 'badge-error',
|
||
'high' => 'badge-warning',
|
||
'medium' => 'badge-info',
|
||
default => 'badge-ghost',
|
||
};
|
||
@endphp
|
||
<div class="p-2.5 rounded-lg bg-base-200">
|
||
<div class="flex items-start gap-1.5">
|
||
<span class="badge badge-xs {{ $pCfg }} shrink-0 mt-0.5">{{ ucfirst($issue->priority ?? 'medium') }}</span>
|
||
<p class="text-xs font-medium truncate">{{ $issue->title }}</p>
|
||
</div>
|
||
@if($issue->feature)
|
||
<p class="text-xs text-gray-400 mt-0.5 truncate">
|
||
<x-heroicon-o-map-pin class="w-2.5 h-2.5 inline" /> {{ $issue->feature->name }}
|
||
</p>
|
||
@endif
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Inspecciones recientes --}}
|
||
<div class="card bg-base-100 shadow">
|
||
<div class="card-body p-5">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="font-semibold text-base flex items-center gap-2">
|
||
<x-heroicon-o-clipboard-document-list class="w-4 h-4 text-yellow-500" />
|
||
Inspecciones recientes
|
||
</h3>
|
||
<a href="{{ route('projects.map', $project) }}?tab=inspections" class="btn btn-xs btn-outline">Ver en mapa</a>
|
||
</div>
|
||
@if($recentInspections->isEmpty())
|
||
<div class="text-center py-4 text-gray-400">
|
||
<x-heroicon-o-clipboard-document-list class="w-7 h-7 mx-auto mb-1 opacity-25" />
|
||
<p class="text-xs">Sin inspecciones</p>
|
||
</div>
|
||
@else
|
||
<div class="space-y-2">
|
||
@foreach($recentInspections as $ins)
|
||
@php
|
||
$iCfg = match($ins->result ?? '') {
|
||
'pass' => ['badge-success', 'OK'],
|
||
'fail' => ['badge-error', 'Fallo'],
|
||
default => ['badge-ghost', 'Pendiente'],
|
||
};
|
||
@endphp
|
||
<div class="p-2.5 rounded-lg bg-base-200">
|
||
<div class="flex items-start justify-between gap-1">
|
||
<p class="text-xs font-medium truncate flex-1">
|
||
{{ $ins->template?->name ?? 'Inspección' }}
|
||
</p>
|
||
<span class="badge badge-xs {{ $iCfg[0] }} shrink-0">{{ $iCfg[1] }}</span>
|
||
</div>
|
||
<div class="flex items-center justify-between mt-0.5">
|
||
@if($ins->feature)
|
||
<p class="text-xs text-gray-400 truncate">
|
||
<x-heroicon-o-map-pin class="w-2.5 h-2.5 inline" /> {{ $ins->feature->name }}
|
||
</p>
|
||
@endif
|
||
<p class="text-xs text-gray-400 shrink-0 ml-1">{{ $ins->created_at->diffForHumans() }}</p>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
{{-- end right --}}
|
||
|
||
</div>
|
||
{{-- end main grid --}}
|
||
|
||
</div>
|
||
</div>
|
||
</div>{{-- end root --}}
|