feat: i18n, language switcher fix, DataTable improvements, blade translations

- Translation system: lang/es/ PHP files (auth, validation, pagination, passwords)
- Rappasoft vendor translations published (lang/vendor/livewire-tables/es/)
- JSON files synced to 391 keys (EN + ES, full parity)
- APP_LOCALE changed to 'es', users.locale column default changed to 'es'
- Language switcher fixed: JS event + window.location.reload() avoids /livewire/update redirect
- SetLocale middleware fallback uses config('app.locale') instead of hardcoded 'en'
- setSortingPillsEnabled(false) on ProjectTable, CompanyTable, UserTable
- Translated 17 blade views: project-map, template-manager, layer-manager,
  company-management, phase-list, media-manager, reports-dashboard,
  client-projects, layer-upload, project-form, project-map-editor-tab,
  admin/users, projects/media, projects/templates, layouts/client
- Navigation 'Empresas' link uses __('Companies')
- Fixed typo key 'Fases and layers' -> 'Phases and layers'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 18:05:53 +02:00
parent 052e1397df
commit 7d854ffb0a
85 changed files with 8499 additions and 1339 deletions
+345 -99
View File
@@ -5,109 +5,355 @@
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
{{-- Stats cards --}}
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm text-gray-500 uppercase tracking-wide">{{ __('Active projects') }}</div>
<div class="text-3xl font-bold mt-1">{{ $stats['active_projects'] }}</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm text-gray-500 uppercase tracking-wide">{{ __('Total projects') }}</div>
<div class="text-3xl font-bold mt-1">{{ $stats['total_projects'] }}</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm text-gray-500 uppercase tracking-wide">{{ __('Total phases') }}</div>
<div class="text-3xl font-bold mt-1">{{ $stats['total_phases'] }}</div>
</div>
<div class="bg-white rounded-lg shadow p-6">
<div class="text-sm text-gray-500 uppercase tracking-wide">{{ __('Total features') }}</div>
<div class="text-3xl font-bold mt-1">{{ $stats['total_features'] }}</div>
</div>
</div>
<div class="py-8">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
{{-- Global progress bar --}}
<div class="bg-white rounded-lg shadow p-6 mb-8">
<h3 class="text-lg font-semibold mb-2">{{ __('Global progress') }}</h3>
<div class="w-full bg-gray-200 rounded-full h-4">
<div class="bg-primary h-4 rounded-full transition-all" style="width: {{ $stats['global_progress'] }}%"></div>
</div>
<p class="text-right text-sm text-gray-500 mt-1">{{ $stats['global_progress'] }}%</p>
</div>
{{-- ============================================================
ROW 1: Project stats (4 columns)
============================================================ --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- Recent projects --}}
<div class="bg-white rounded-lg shadow p-6 mb-8">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">{{ __('Recent projects') }}</h3>
<a href="{{ route('projects.list') }}" class="text-sm text-primary hover:underline">{{ __('View Map') }}</a>
</div>
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>{{ __('Name') }}</th>
<th>{{ __('Status') }}</th>
<th>{{ __('Phases') }}</th>
<th>{{ __('Progress') }}</th>
<th></th>
</tr>
</thead>
<tbody>
@forelse($recentProjects as $project)
<tr>
<td class="font-medium">{{ $project->name }}</td>
<td>
@php
$badgeClass = match($project->status) {
'planning' => 'badge-ghost',
'in_progress' => 'badge-primary',
'paused' => 'badge-warning',
'completed' => 'badge-success',
default => 'badge-ghost'
};
@endphp
<span class="badge {{ $badgeClass }}">{{ __(ucfirst(str_replace('_', ' ', $project->status))) }}</span>
</td>
<td>{{ $project->phases_count }}</td>
<td>
@php $avg = $project->phases->avg('progress_percent'); @endphp
<div class="flex items-center gap-2">
<div class="w-24 bg-gray-200 rounded-full h-2.5">
<div class="bg-primary h-2.5 rounded-full" style="width: {{ $avg }}%"></div>
</div>
<span class="text-xs">{{ round($avg) }}%</span>
</div>
</td>
<td>
<a href="{{ route('projects.map', $project) }}" class="btn btn-xs btn-outline">{{ __('Map') }}</a>
</td>
</tr>
@empty
<tr><td colspan="5" class="text-center text-gray-400 py-4">{{ __('No results') }}</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
{{-- Recent inspections --}}
@if($recentInspections->isNotEmpty())
<div class="bg-white rounded-lg shadow p-6">
<h3 class="text-lg font-semibold mb-4">{{ __('Recent inspections') }}</h3>
<div class="space-y-2">
@foreach($recentInspections as $inspection)
<div class="border rounded p-3 flex justify-between items-center">
<div>
<span class="font-medium">{{ $inspection->template?->name ?? __('Inspection') }}</span>
<span class="text-sm text-gray-500 ml-2">{{ $inspection->feature?->name }}</span>
</div>
<span class="text-xs text-gray-400">{{ $inspection->created_at->diffForHumans() }}</span>
{{-- 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>
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Proyectos activos</p>
<p class="mt-1 text-3xl font-bold text-blue-600">
{{ $stats['active_projects'] }}
<span class="text-lg font-normal text-gray-400">/ {{ $stats['total_projects'] }}</span>
</p>
</div>
@endforeach
<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="card bg-base-100 shadow hover:shadow-md transition-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-2">
<div class="bg-green-500 h-2 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-6 h-6 text-green-600" />
</div>
</div>
</div>
</div>
@endif
{{-- 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>
</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>
</x-app-layout>
</x-app-layout>