feat(home): dashboard ligero renombrado a "Home"

- Sustituye el panel saturado de estadísticas por una vista "Home" centrada en lo
  del usuario: mis proyectos (compacto), mis tareas pendientes, incidencias asignadas
  y notificaciones recientes. Sin inundar de detalle (mejor con muchos proyectos).
- Etiqueta "Dashboard" -> "Home" en el menú y la cabecera.
- Elimina el orderByRaw FIELD() (solo MySQL) del antiguo dashboard.
- ExampleTest: ahora valida la conducta real (la raíz redirige a login).

Suite: 71 passing (0 fallos; resueltas las 2 pre-existentes de SQLite).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 17:24:30 +02:00
parent 7d390872c3
commit 7e997bc6aa
4 changed files with 132 additions and 390 deletions
+106 -334
View File
@@ -1,359 +1,131 @@
<x-app-layout> <x-app-layout>
<x-slot name="header"> <x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight"> <h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }} {{ __('Home') }}
</h2> </h2>
</x-slot> </x-slot>
<div class="py-8"> <div class="py-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6"> <div class="max-w-6xl mx-auto sm:px-6 lg:px-8 space-y-6">
{{-- ============================================================ {{-- Saludo --}}
ROW 1: Project stats (4 columns) <div>
============================================================ --}} <h3 class="text-lg font-bold">Hola, {{ $user?->first_name ?? auth()->user()->name }}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <p class="text-sm text-base-content/60">Un resumen rápido de lo tuyo.</p>
</div>
{{-- Proyectos activos --}} <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<a href="{{ route('projects.list') }}" class="card bg-base-100 shadow hover:shadow-md transition-shadow">
<div class="card-body p-5"> {{-- ───────────── Mis proyectos ───────────── --}}
<div class="flex items-center justify-between"> <div class="card bg-base-100 border border-base-300">
<div> <div class="card-body p-4">
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Proyectos activos</p> <div class="flex items-center justify-between mb-1">
<p class="mt-1 text-3xl font-bold text-blue-600"> <h3 class="font-bold flex items-center gap-2">
{{ $stats['active_projects'] }} <x-heroicon-o-folder class="w-5 h-5" /> Mis proyectos
<span class="text-lg font-normal text-gray-400">/ {{ $stats['total_projects'] }}</span> <span class="badge badge-sm">{{ $projectsCount }}</span>
</p> </h3>
</div> <a href="{{ route('projects.index') }}" wire:navigate class="link link-primary text-sm">Ver todos</a>
<div class="p-3 bg-blue-100 rounded-full">
<x-heroicon-o-building-office class="w-6 h-6 text-blue-600" />
</div>
</div> </div>
</div> @forelse($projects as $project)
</a> @php $pct = round($project->phases->avg('progress_percent') ?? 0); @endphp
<a href="{{ route('projects.dashboard', $project) }}" wire:navigate
{{-- Avance global --}} class="flex items-center gap-3 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1">
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow"> <div class="flex-1 min-w-0">
<div class="card-body p-5"> <div class="font-medium text-sm truncate">{{ $project->name }}</div>
<div class="flex items-center justify-between"> <progress class="progress progress-primary w-full h-1.5 mt-1" value="{{ $pct }}" max="100"></progress>
<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-2">
<div class="bg-green-500 h-2 rounded-full transition-all" style="width: {{ $stats['global_progress'] }}%"></div>
</div> </div>
</div> <span class="text-xs text-base-content/50 w-9 text-right">{{ $pct }}%</span>
<div class="p-3 bg-green-100 rounded-full ml-3 shrink-0"> </a>
<x-heroicon-o-chart-bar class="w-6 h-6 text-green-600" /> @empty
</div> <p class="text-sm text-base-content/40 py-3">No tienes proyectos asignados.</p>
</div> @endforelse
</div> </div>
</div> </div>
{{-- Fases con retraso --}} {{-- ───────────── Mis tareas ───────────── --}}
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow"> <div class="card bg-base-100 border border-base-300">
<div class="card-body p-5"> <div class="card-body p-4">
<div class="flex items-center justify-between"> <h3 class="font-bold flex items-center gap-2 mb-1">
<div> <x-heroicon-o-clipboard-document-check class="w-5 h-5" /> Mis tareas
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Fases con retraso</p> <span class="badge badge-sm">{{ $myTasksCount }}</span>
<p class="mt-1 text-3xl font-bold {{ $stats['delayed_phases'] > 0 ? 'text-red-600' : 'text-gray-400' }}"> </h3>
{{ $stats['delayed_phases'] }} @forelse($myTasks as $task)
</p> <a href="{{ $task->issue ? route('projects.issues.show', [$task->issue->project_id, $task->issue_id]) : '#' }}"
@if($stats['delayed_phases'] > 0) wire:navigate
<p class="text-xs text-red-500 mt-0.5">Requiere atención</p> class="flex items-center gap-2 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1">
@else <div class="flex-1 min-w-0">
<p class="text-xs text-gray-400 mt-0.5">Sin retrasos</p> <div class="text-sm truncate">{{ $task->title }}</div>
<div class="text-xs text-base-content/50 truncate">
{{ $task->issue?->project?->name ?? '—' }}
</div>
</div>
@if($task->due_date)
<span class="text-xs whitespace-nowrap {{ $task->is_overdue ? 'text-error font-semibold' : 'text-base-content/50' }}">
{{ $task->due_date->format('d/m/Y') }}
</span>
@endif @endif
</div> </a>
<div class="p-3 {{ $stats['delayed_phases'] > 0 ? 'bg-red-100' : 'bg-gray-100' }} rounded-full"> @empty
<x-heroicon-o-clock class="w-6 h-6 {{ $stats['delayed_phases'] > 0 ? 'text-red-600' : 'text-gray-400' }}" /> <p class="text-sm text-base-content/40 py-3">No tienes tareas pendientes. 🎉</p>
</div> @endforelse
</div>
</div> </div>
</div> </div>
{{-- Elementos totales --}} {{-- ───────────── Mis incidencias ───────────── --}}
<div class="card bg-base-100 shadow hover:shadow-md transition-shadow"> <div class="card bg-base-100 border border-base-300">
<div class="card-body p-5"> <div class="card-body p-4">
<div class="flex items-center justify-between"> <h3 class="font-bold flex items-center gap-2 mb-1">
<div> <x-heroicon-o-exclamation-triangle class="w-5 h-5" /> Incidencias asignadas
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Elementos totales</p> <span class="badge badge-sm">{{ $myIssuesCount }}</span>
<p class="mt-1 text-3xl font-bold text-indigo-600">{{ $stats['total_features'] }}</p> </h3>
<p class="text-xs text-gray-400 mt-0.5">{{ $stats['total_phases'] }} fases</p> @forelse($myIssues as $issue)
</div> @php
<div class="p-3 bg-indigo-100 rounded-full"> $sLabel = ['open'=>'Abierto','in_review'=>'En revisión'][$issue->status] ?? $issue->status;
<x-heroicon-o-map-pin class="w-6 h-6 text-indigo-600" /> @endphp
</div> <a href="{{ route('projects.issues.show', [$issue->project_id, $issue->id]) }}" wire:navigate
</div> class="flex items-center gap-2 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1">
<span class="badge badge-xs shrink-0" style="background-color: {{ $issue->priority_color }}; color:#fff; border:0;">&nbsp;</span>
<div class="flex-1 min-w-0">
<div class="text-sm truncate">{{ $issue->title }}</div>
<div class="text-xs text-base-content/50 truncate">{{ $issue->project?->name ?? '—' }}</div>
</div>
<span class="badge badge-ghost badge-xs whitespace-nowrap">{{ $sLabel }}</span>
</a>
@empty
<p class="text-sm text-base-content/40 py-3">No tienes incidencias asignadas.</p>
@endforelse
</div>
</div>
{{-- ───────────── Notificaciones ───────────── --}}
<div class="card bg-base-100 border border-base-300">
<div class="card-body p-4">
<h3 class="font-bold flex items-center gap-2 mb-1">
<x-heroicon-o-bell class="w-5 h-5" /> Notificaciones
@if($unreadCount)<span class="badge badge-error badge-sm">{{ $unreadCount }}</span>@endif
</h3>
@forelse($notifications as $note)
@php
$d = $note->data;
$url = (isset($d['project_id'], $d['issue_id']))
? route('projects.issues.show', [$d['project_id'], $d['issue_id']])
: null;
@endphp
<a href="{{ $url ?? '#' }}" @if($url) wire:navigate @endif
class="flex items-start gap-2 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1 {{ is_null($note->read_at) ? 'font-medium' : '' }}">
<span class="mt-1 w-2 h-2 rounded-full shrink-0 {{ is_null($note->read_at) ? 'bg-error' : 'bg-base-300' }}"></span>
<div class="flex-1 min-w-0">
<div class="text-sm">{{ $d['message'] ?? 'Notificación' }}</div>
<div class="text-xs text-base-content/40">{{ $note->created_at->diffForHumans() }}</div>
</div>
</a>
@empty
<p class="text-sm text-base-content/40 py-3">Sin notificaciones.</p>
@endforelse
</div> </div>
</div> </div>
</div> </div>
{{-- ============================================================
ROW 2: Issues & Inspections (4 columns)
============================================================ --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- Issues abiertos --}}
<div class="card bg-base-100 shadow hover:shadow-md transition-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 text-orange-600">{{ $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 bg-orange-100 rounded-full">
<x-heroicon-o-exclamation-triangle class="w-6 h-6 text-orange-600" />
</div>
</div>
</div>
</div>
{{-- Inspecciones pendientes --}}
<div class="card bg-base-100 shadow hover:shadow-md transition-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">Insp. pendientes</p>
<p class="mt-1 text-3xl font-bold text-yellow-600">{{ $stats['pending_inspections'] }}</p>
<p class="text-xs text-gray-400 mt-0.5">Por realizar</p>
</div>
<div class="p-3 bg-yellow-100 rounded-full">
<x-heroicon-o-clipboard-document-list class="w-6 h-6 text-yellow-600" />
</div>
</div>
</div>
</div>
{{-- Inspecciones completadas --}}
<div class="card bg-base-100 shadow hover:shadow-md transition-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">Insp. completadas</p>
<p class="mt-1 text-3xl font-bold text-green-600">{{ $stats['completed_inspections'] }}</p>
<p class="text-xs text-gray-400 mt-0.5">Aprobadas</p>
</div>
<div class="p-3 bg-green-100 rounded-full">
<x-heroicon-o-check-circle class="w-6 h-6 text-green-600" />
</div>
</div>
</div>
</div>
{{-- Inspecciones rechazadas --}}
<div class="card bg-base-100 shadow hover:shadow-md transition-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">Insp. rechazadas</p>
<p class="mt-1 text-3xl font-bold {{ $stats['rejected_inspections'] > 0 ? 'text-red-600' : 'text-gray-400' }}">
{{ $stats['rejected_inspections'] }}
</p>
<p class="text-xs text-gray-400 mt-0.5">Requieren revisión</p>
</div>
<div class="p-3 {{ $stats['rejected_inspections'] > 0 ? 'bg-red-100' : 'bg-gray-100' }} rounded-full">
<x-heroicon-o-x-circle class="w-6 h-6 {{ $stats['rejected_inspections'] > 0 ? 'text-red-600' : 'text-gray-400' }}" />
</div>
</div>
</div>
</div>
</div>
{{-- ============================================================
MAIN CONTENT: Two-column layout
============================================================ --}}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{{-- LEFT COLUMN (2/3): Recent projects --}}
<div class="lg:col-span-2">
<div class="card bg-base-100 shadow">
<div class="card-body p-5">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Proyectos recientes</h3>
<a href="{{ route('projects.list') }}" class="btn btn-sm btn-outline btn-primary">
Ver todos
</a>
</div>
@if($recentProjects->isEmpty())
<div class="text-center py-10 text-gray-400">
<x-heroicon-o-building-office class="w-12 h-12 mx-auto mb-2 opacity-30" />
<p>No hay proyectos disponibles</p>
</div>
@else
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
@foreach($recentProjects as $project)
@php
$avg = $project->phases->avg('progress_percent') ?? 0;
$statusConfig = match($project->status) {
'in_progress' => ['badge' => 'badge-primary', 'bar' => 'bg-blue-500', 'label' => 'En progreso'],
'completed' => ['badge' => 'badge-success', 'bar' => 'bg-green-500', 'label' => 'Completado'],
'paused' => ['badge' => 'badge-warning', 'bar' => 'bg-yellow-500', 'label' => 'Pausado'],
'planning' => ['badge' => 'badge-ghost', 'bar' => 'bg-gray-400', 'label' => 'Planificación'],
default => ['badge' => 'badge-ghost', 'bar' => 'bg-gray-400', 'label' => ucfirst(str_replace('_', ' ', $project->status))],
};
@endphp
<div class="border border-base-200 rounded-lg p-4 hover:border-primary hover:shadow-sm transition-all">
<div class="flex items-start justify-between mb-2">
<h4 class="font-semibold text-sm leading-tight flex-1 mr-2 truncate" title="{{ $project->name }}">
{{ $project->name }}
</h4>
<span class="badge badge-sm {{ $statusConfig['badge'] }} shrink-0">
{{ $statusConfig['label'] }}
</span>
</div>
<div class="flex items-center gap-1 text-xs text-gray-500 mb-3">
<x-heroicon-o-rectangle-stack class="w-3.5 h-3.5" />
<span>{{ $project->phases_count }} {{ $project->phases_count === 1 ? 'fase' : 'fases' }}</span>
</div>
<div class="space-y-1">
<div class="flex justify-between text-xs text-gray-500">
<span>Progreso</span>
<span class="font-medium">{{ round($avg) }}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-1.5">
<div class="{{ $statusConfig['bar'] }} h-1.5 rounded-full transition-all" style="width: {{ $avg }}%"></div>
</div>
</div>
<div class="mt-3 flex justify-end gap-1">
<a href="{{ route('projects.dashboard', $project) }}" class="btn btn-xs btn-outline gap-1">
<x-heroicon-o-squares-2x2 class="w-3 h-3" />
Dashboard
</a>
<a href="{{ route('projects.map', $project) }}" class="btn btn-xs btn-outline gap-1">
<x-heroicon-o-map class="w-3 h-3" />
Mapa
</a>
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
{{-- RIGHT COLUMN (1/3): Issues + Inspections --}}
<div class="lg:col-span-1 space-y-5">
{{-- 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="text-base font-semibold">Issues recientes</h3>
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-orange-500" />
</div>
@if(isset($recentIssues) && $recentIssues->isNotEmpty())
<div class="space-y-2">
@foreach($recentIssues as $issue)
@php
$priorityConfig = match($issue->priority ?? 'medium') {
'critical' => ['badge' => 'badge-error', 'label' => 'Crítico'],
'high' => ['badge' => 'badge-warning', 'label' => 'Alto'],
'medium' => ['badge' => 'badge-info', 'label' => 'Medio'],
'low' => ['badge' => 'badge-ghost', 'label' => 'Bajo'],
default => ['badge' => 'badge-ghost', 'label' => ucfirst($issue->priority ?? '')],
};
@endphp
<div class="flex items-start gap-2 p-2.5 rounded-lg bg-base-200 hover:bg-base-300 transition-colors">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-1.5 mb-0.5">
<span class="badge badge-xs {{ $priorityConfig['badge'] }}">{{ $priorityConfig['label'] }}</span>
</div>
<p class="text-sm font-medium truncate" title="{{ $issue->title }}">{{ $issue->title }}</p>
<p class="text-xs text-gray-500 truncate">
@if($issue->feature)
<x-heroicon-o-map-pin class="w-3 h-3 inline" /> {{ $issue->feature->name }}
@elseif($issue->project)
<x-heroicon-o-building-office class="w-3 h-3 inline" /> {{ $issue->project->name }}
@endif
</p>
@if($issue->reporter)
<p class="text-xs text-gray-400 mt-0.5">
<x-heroicon-o-user class="w-3 h-3 inline" /> {{ $issue->reporter->name }}
</p>
@endif
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-6 text-gray-400">
<x-heroicon-o-check-circle class="w-8 h-8 mx-auto mb-1 opacity-30" />
<p class="text-sm">Sin issues abiertos</p>
</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="text-base font-semibold">Inspecciones recientes</h3>
<x-heroicon-o-clipboard-document-list class="w-5 h-5 text-yellow-500" />
</div>
@if($recentInspections->isNotEmpty())
<div class="space-y-2">
@foreach($recentInspections as $inspection)
@php
$inspStatusConfig = match($inspection->status ?? 'pending') {
'completed' => ['badge' => 'badge-success', 'label' => 'Completada'],
'pending' => ['badge' => 'badge-warning', 'label' => 'Pendiente'],
'rejected' => ['badge' => 'badge-error', 'label' => 'Rechazada'],
'in_progress' => ['badge' => 'badge-info', 'label' => 'En curso'],
default => ['badge' => 'badge-ghost', 'label' => ucfirst($inspection->status ?? '')],
};
@endphp
<div class="flex items-start gap-2 p-2.5 rounded-lg bg-base-200 hover:bg-base-300 transition-colors">
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between gap-1 mb-0.5">
<p class="text-sm font-medium truncate">
{{ $inspection->template?->name ?? 'Inspección' }}
</p>
<span class="badge badge-xs {{ $inspStatusConfig['badge'] }} shrink-0">{{ $inspStatusConfig['label'] }}</span>
</div>
@if($inspection->feature)
<p class="text-xs text-gray-500 truncate">
<x-heroicon-o-map-pin class="w-3 h-3 inline" /> {{ $inspection->feature->name }}
</p>
@elseif($inspection->project)
<p class="text-xs text-gray-500 truncate">
<x-heroicon-o-building-office class="w-3 h-3 inline" /> {{ $inspection->project->name }}
</p>
@endif
<p class="text-xs text-gray-400 mt-0.5">{{ $inspection->created_at->diffForHumans() }}</p>
</div>
</div>
@endforeach
</div>
@else
<div class="text-center py-6 text-gray-400">
<x-heroicon-o-clipboard-document-list class="w-8 h-8 mx-auto mb-1 opacity-30" />
<p class="text-sm">Sin inspecciones recientes</p>
</div>
@endif
</div>
</div>
</div>
{{-- end right column --}}
</div>
{{-- end main content --}}
</div> </div>
</div> </div>
</x-app-layout> </x-app-layout>
@@ -33,7 +33,7 @@ new class extends Component
<!-- Navigation Links --> <!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"> <div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')" wire:navigate> <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }} {{ __('Home') }}
</x-nav-link> </x-nav-link>
</div> </div>
@@ -114,7 +114,7 @@ new class extends Component
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden"> <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1"> <div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')" wire:navigate> <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')" wire:navigate>
{{ __('Dashboard') }} {{ __('Home') }}
</x-responsive-nav-link> </x-responsive-nav-link>
</div> </div>
+21 -51
View File
@@ -25,65 +25,35 @@ Route::get('/', function () {
// Grupo de rutas protegidas por autenticación // Grupo de rutas protegidas por autenticación
Route::middleware(['auth'])->group(function () { Route::middleware(['auth'])->group(function () {
// Dashboard principal (vista con estadísticas y lista de proyectos) // Home: vista ligera (proyectos, tareas e incidencias del usuario + notificaciones)
Route::get('/dashboard', function () { Route::get('/dashboard', function () {
$user = \Illuminate\Support\Facades\Auth::user(); $user = \Illuminate\Support\Facades\Auth::user();
$projectIds = \App\Models\Project::accessibleBy($user)->pluck('id');
$projects = \App\Models\Project::accessibleBy($user) $projects = \App\Models\Project::accessibleBy($user)
->withCount('phases') ->with(['phases' => fn ($q) => $q->select('id', 'project_id', 'progress_percent')])
->with(['phases' => fn($q) => $q->orderBy('order')]) ->orderBy('name')->take(8)->get();
->latest()->take(6)->get(); $projectsCount = \App\Models\Project::accessibleBy($user)->count();
$activeProjects = \App\Models\Project::accessibleBy($user)->where('status', 'in_progress')->count(); $myTasks = \App\Models\IssueTask::where('assigned_to', $user->id)
$totalProjects = \App\Models\Project::accessibleBy($user)->count(); ->where('is_done', false)
$totalPhases = \App\Models\Phase::whereIn('project_id', $projectIds)->count(); ->with('issue.project')
$totalFeatures = \App\Models\Feature::whereHas('layer.phase', fn($q) => $q->whereIn('project_id', $projectIds))->count(); ->orderByRaw('due_date IS NULL, due_date ASC')
$globalProgress = \App\Models\Phase::whereIn('project_id', $projectIds)->avg('progress_percent') ?? 0; ->take(8)->get();
$myTasksCount = \App\Models\IssueTask::where('assigned_to', $user->id)->where('is_done', false)->count();
$openIssues = \App\Models\Issue::whereIn('project_id', $projectIds)->where('status', 'open')->count(); $myIssues = \App\Models\Issue::where('assigned_to', $user->id)
$criticalIssues = \App\Models\Issue::whereIn('project_id', $projectIds)->where('status', 'open')->where('priority', 'critical')->count(); ->whereIn('status', ['open', 'in_review'])
$pendingInspections = \App\Models\Inspection::whereIn('project_id', $projectIds)->where('status', 'pending')->count();
$completedInspections = \App\Models\Inspection::whereIn('project_id', $projectIds)->where('status', 'completed')->count();
$rejectedInspections = \App\Models\Inspection::whereIn('project_id', $projectIds)->where('status', 'rejected')->count();
$recentInspections = \App\Models\Inspection::whereIn('project_id', $projectIds)
->with(['template', 'feature', 'project'])
->latest()->take(5)->get();
$recentIssues = \App\Models\Issue::whereIn('project_id', $projectIds)
->with(['feature', 'reporter', 'project'])
->where('status', '!=', 'closed')
->orderByRaw("FIELD(priority,'critical','high','medium','low')")
->take(5)->get();
// Projects with delay (planned_end exceeded and not completed)
$delayedPhases = \App\Models\Phase::whereIn('project_id', $projectIds)
->whereNotNull('planned_end')
->where('planned_end', '<', now())
->where('progress_percent', '<', 100)
->with('project') ->with('project')
->count(); ->latest()->take(6)->get();
$myIssuesCount = \App\Models\Issue::where('assigned_to', $user->id)->whereIn('status', ['open', 'in_review'])->count();
return view('dashboard', [ $notifications = $user->notifications()->latest()->take(6)->get();
'stats' => [ $unreadCount = $user->unreadNotifications()->count();
'active_projects' => $activeProjects,
'total_projects' => $totalProjects, return view('dashboard', compact(
'total_phases' => $totalPhases, 'user', 'projects', 'projectsCount', 'myTasks', 'myTasksCount',
'total_features' => $totalFeatures, 'myIssues', 'myIssuesCount', 'notifications', 'unreadCount'
'global_progress' => round($globalProgress), ));
'open_issues' => $openIssues,
'critical_issues' => $criticalIssues,
'pending_inspections' => $pendingInspections,
'completed_inspections'=> $completedInspections,
'rejected_inspections' => $rejectedInspections,
'delayed_phases' => $delayedPhases,
],
'recentProjects' => $projects,
'recentInspections' => $recentInspections,
'recentIssues' => $recentIssues,
]);
})->name('dashboard'); })->name('dashboard');
Route::get('/reports/dashboard', ReportsDashboard::class)->name('reports.dashboard'); Route::get('/reports/dashboard', ReportsDashboard::class)->name('reports.dashboard');
+3 -3
View File
@@ -8,12 +8,12 @@ use Tests\TestCase;
class ExampleTest extends TestCase class ExampleTest extends TestCase
{ {
/** /**
* A basic test example. * La raíz redirige al dashboard tras 'auth'; un invitado acaba en login.
*/ */
public function test_the_application_returns_a_successful_response(): void public function test_the_root_redirects_guests_to_login(): void
{ {
$response = $this->get('/'); $response = $this->get('/');
$response->assertStatus(200); $response->assertRedirect(route('login'));
} }
} }