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:
@@ -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 class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
||||||
|
|
||||||
{{-- Proyectos activos --}}
|
|
||||||
<a href="{{ route('projects.list') }}" 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>
|
<div>
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Proyectos activos</p>
|
<h3 class="text-lg font-bold">Hola, {{ $user?->first_name ?? auth()->user()->name }}</h3>
|
||||||
<p class="mt-1 text-3xl font-bold text-blue-600">
|
<p class="text-sm text-base-content/60">Un resumen rápido de lo tuyo.</p>
|
||||||
{{ $stats['active_projects'] }}
|
|
||||||
<span class="text-lg font-normal text-gray-400">/ {{ $stats['total_projects'] }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<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>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{{-- Avance global --}}
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
<div 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 class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-1">
|
||||||
|
<h3 class="font-bold flex items-center gap-2">
|
||||||
|
<x-heroicon-o-folder class="w-5 h-5" /> Mis proyectos
|
||||||
|
<span class="badge badge-sm">{{ $projectsCount }}</span>
|
||||||
|
</h3>
|
||||||
|
<a href="{{ route('projects.index') }}" wire:navigate class="link link-primary text-sm">Ver todos</a>
|
||||||
|
</div>
|
||||||
|
@forelse($projects as $project)
|
||||||
|
@php $pct = round($project->phases->avg('progress_percent') ?? 0); @endphp
|
||||||
|
<a href="{{ route('projects.dashboard', $project) }}" wire:navigate
|
||||||
|
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="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Avance global</p>
|
<div class="font-medium text-sm truncate">{{ $project->name }}</div>
|
||||||
<p class="mt-1 text-3xl font-bold text-green-600">{{ $stats['global_progress'] }}%</p>
|
<progress class="progress progress-primary w-full h-1.5 mt-1" value="{{ $pct }}" max="100"></progress>
|
||||||
<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">
|
|
||||||
<x-heroicon-o-chart-bar class="w-6 h-6 text-green-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Fases con retraso --}}
|
|
||||||
<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">Fases con retraso</p>
|
|
||||||
<p class="mt-1 text-3xl font-bold {{ $stats['delayed_phases'] > 0 ? 'text-red-600' : 'text-gray-400' }}">
|
|
||||||
{{ $stats['delayed_phases'] }}
|
|
||||||
</p>
|
|
||||||
@if($stats['delayed_phases'] > 0)
|
|
||||||
<p class="text-xs text-red-500 mt-0.5">Requiere atención</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-gray-100' }} rounded-full">
|
|
||||||
<x-heroicon-o-clock class="w-6 h-6 {{ $stats['delayed_phases'] > 0 ? 'text-red-600' : 'text-gray-400' }}" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{-- Elementos totales --}}
|
|
||||||
<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">Elementos totales</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['total_phases'] }} fases</p>
|
|
||||||
</div>
|
|
||||||
<div class="p-3 bg-indigo-100 rounded-full">
|
|
||||||
<x-heroicon-o-map-pin class="w-6 h-6 text-indigo-600" />
|
|
||||||
</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>
|
</a>
|
||||||
|
@empty
|
||||||
|
<p class="text-sm text-base-content/40 py-3">No tienes proyectos asignados.</p>
|
||||||
|
@endforelse
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if($recentProjects->isEmpty())
|
{{-- ───────────── Mis tareas ───────────── --}}
|
||||||
<div class="text-center py-10 text-gray-400">
|
<div class="card bg-base-100 border border-base-300">
|
||||||
<x-heroicon-o-building-office class="w-12 h-12 mx-auto mb-2 opacity-30" />
|
<div class="card-body p-4">
|
||||||
<p>No hay proyectos disponibles</p>
|
<h3 class="font-bold flex items-center gap-2 mb-1">
|
||||||
|
<x-heroicon-o-clipboard-document-check class="w-5 h-5" /> Mis tareas
|
||||||
|
<span class="badge badge-sm">{{ $myTasksCount }}</span>
|
||||||
|
</h3>
|
||||||
|
@forelse($myTasks as $task)
|
||||||
|
<a href="{{ $task->issue ? route('projects.issues.show', [$task->issue->project_id, $task->issue_id]) : '#' }}"
|
||||||
|
wire:navigate
|
||||||
|
class="flex items-center gap-2 py-2 border-b border-base-200 last:border-0 hover:bg-base-200/50 rounded px-1">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="text-sm truncate">{{ $task->title }}</div>
|
||||||
|
<div class="text-xs text-base-content/50 truncate">
|
||||||
|
{{ $task->issue?->project?->name ?? '—' }}
|
||||||
</div>
|
</div>
|
||||||
@else
|
</div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
@if($task->due_date)
|
||||||
@foreach($recentProjects as $project)
|
<span class="text-xs whitespace-nowrap {{ $task->is_overdue ? 'text-error font-semibold' : 'text-base-content/50' }}">
|
||||||
@php
|
{{ $task->due_date->format('d/m/Y') }}
|
||||||
$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>
|
</span>
|
||||||
</div>
|
@endif
|
||||||
|
|
||||||
<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>
|
||||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-xs btn-outline gap-1">
|
@empty
|
||||||
<x-heroicon-o-map class="w-3 h-3" />
|
<p class="text-sm text-base-content/40 py-3">No tienes tareas pendientes. 🎉</p>
|
||||||
Mapa
|
@endforelse
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ───────────── Mis incidencias ───────────── --}}
|
||||||
|
<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-exclamation-triangle class="w-5 h-5" /> Incidencias asignadas
|
||||||
|
<span class="badge badge-sm">{{ $myIssuesCount }}</span>
|
||||||
|
</h3>
|
||||||
|
@forelse($myIssues as $issue)
|
||||||
|
@php
|
||||||
|
$sLabel = ['open'=>'Abierto','in_review'=>'En revisión'][$issue->status] ?? $issue->status;
|
||||||
|
@endphp
|
||||||
|
<a href="{{ route('projects.issues.show', [$issue->project_id, $issue->id]) }}" wire:navigate
|
||||||
|
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;"> </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>
|
</a>
|
||||||
</div>
|
@empty
|
||||||
</div>
|
<p class="text-sm text-base-content/40 py-3">No tienes incidencias asignadas.</p>
|
||||||
@endforeach
|
@endforelse
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- RIGHT COLUMN (1/3): Issues + Inspections --}}
|
{{-- ───────────── Notificaciones ───────────── --}}
|
||||||
<div class="lg:col-span-1 space-y-5">
|
<div class="card bg-base-100 border border-base-300">
|
||||||
|
<div class="card-body p-4">
|
||||||
{{-- Issues recientes --}}
|
<h3 class="font-bold flex items-center gap-2 mb-1">
|
||||||
<div class="card bg-base-100 shadow">
|
<x-heroicon-o-bell class="w-5 h-5" /> Notificaciones
|
||||||
<div class="card-body p-5">
|
@if($unreadCount)<span class="badge badge-error badge-sm">{{ $unreadCount }}</span>@endif
|
||||||
<div class="flex items-center justify-between mb-3">
|
</h3>
|
||||||
<h3 class="text-base font-semibold">Issues recientes</h3>
|
@forelse($notifications as $note)
|
||||||
<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
|
@php
|
||||||
$priorityConfig = match($issue->priority ?? 'medium') {
|
$d = $note->data;
|
||||||
'critical' => ['badge' => 'badge-error', 'label' => 'Crítico'],
|
$url = (isset($d['project_id'], $d['issue_id']))
|
||||||
'high' => ['badge' => 'badge-warning', 'label' => 'Alto'],
|
? route('projects.issues.show', [$d['project_id'], $d['issue_id']])
|
||||||
'medium' => ['badge' => 'badge-info', 'label' => 'Medio'],
|
: null;
|
||||||
'low' => ['badge' => 'badge-ghost', 'label' => 'Bajo'],
|
|
||||||
default => ['badge' => 'badge-ghost', 'label' => ucfirst($issue->priority ?? '')],
|
|
||||||
};
|
|
||||||
@endphp
|
@endphp
|
||||||
<div class="flex items-start gap-2 p-2.5 rounded-lg bg-base-200 hover:bg-base-300 transition-colors">
|
<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="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-1.5 mb-0.5">
|
<div class="text-sm">{{ $d['message'] ?? 'Notificación' }}</div>
|
||||||
<span class="badge badge-xs {{ $priorityConfig['badge'] }}">{{ $priorityConfig['label'] }}</span>
|
<div class="text-xs text-base-content/40">{{ $note->created_at->diffForHumans() }}</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm font-medium truncate" title="{{ $issue->title }}">{{ $issue->title }}</p>
|
</a>
|
||||||
<p class="text-xs text-gray-500 truncate">
|
@empty
|
||||||
@if($issue->feature)
|
<p class="text-sm text-base-content/40 py-3">Sin notificaciones.</p>
|
||||||
<x-heroicon-o-map-pin class="w-3 h-3 inline" /> {{ $issue->feature->name }}
|
@endforelse
|
||||||
@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>
|
</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
@@ -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');
|
||||||
|
|||||||
@@ -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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user