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:
@@ -0,0 +1,431 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Informe de Proyecto — {{ $project->name }}</title>
|
||||
<style>
|
||||
/* --------------------------------------------------------
|
||||
Base styles
|
||||
-------------------------------------------------------- */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Arial, Helvetica, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #1f2937;
|
||||
background: #fff;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
a { color: #2563eb; text-decoration: none; }
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Layout
|
||||
-------------------------------------------------------- */
|
||||
.page-wrapper {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Header
|
||||
-------------------------------------------------------- */
|
||||
.report-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
border-bottom: 2px solid #2563eb;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.logo-placeholder {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #dbeafe;
|
||||
border: 2px solid #93c5fd;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 10px;
|
||||
color: #2563eb;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.report-header-info {
|
||||
flex: 1;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.report-subtitle {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.report-meta {
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.report-meta strong { display: block; color: #1f2937; font-size: 13px; }
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Section titles
|
||||
-------------------------------------------------------- */
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 14px;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Stats summary
|
||||
-------------------------------------------------------- */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
color: #2563eb;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Tables
|
||||
-------------------------------------------------------- */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f1f5f9;
|
||||
text-align: left;
|
||||
padding: 7px 10px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
border-bottom: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr:last-child td { border-bottom: none; }
|
||||
|
||||
tr:hover td { background: #f9fafb; }
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Phase section
|
||||
-------------------------------------------------------- */
|
||||
.phase-block {
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phase-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background: #f8fafc;
|
||||
border-left: 5px solid #3b82f6;
|
||||
}
|
||||
|
||||
.phase-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
}
|
||||
|
||||
.phase-progress-bar-wrap {
|
||||
background: #e5e7eb;
|
||||
border-radius: 6px;
|
||||
height: 8px;
|
||||
width: 160px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.phase-progress-bar {
|
||||
height: 8px;
|
||||
border-radius: 6px;
|
||||
background: #22c55e;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.phase-meta {
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Status badges
|
||||
-------------------------------------------------------- */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 7px;
|
||||
border-radius: 9999px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1.6;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.badge-planned { background: #f3f4f6; color: #6b7280; }
|
||||
.badge-started { background: #dbeafe; color: #1d4ed8; }
|
||||
.badge-in_progress { background: #fef3c7; color: #92400e; }
|
||||
.badge-completed { background: #d1fae5; color: #065f46; }
|
||||
.badge-verified { background: #ede9fe; color: #5b21b6; }
|
||||
.badge-default { background: #f3f4f6; color: #6b7280; }
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Print button (hidden on print)
|
||||
-------------------------------------------------------- */
|
||||
.print-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 18px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.print-btn:hover { background: #1d4ed8; }
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Print media rules
|
||||
-------------------------------------------------------- */
|
||||
@media print {
|
||||
.print-btn { display: none !important; }
|
||||
|
||||
body { font-size: 11px; }
|
||||
|
||||
.page-wrapper { max-width: 100%; padding: 16px; }
|
||||
|
||||
.report-header { page-break-inside: avoid; }
|
||||
|
||||
.phase-block { page-break-inside: avoid; }
|
||||
|
||||
a { color: inherit; }
|
||||
|
||||
.stats-grid { grid-template-columns: repeat(5, 1fr); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-wrapper">
|
||||
|
||||
{{-- Print button (hidden on print) --}}
|
||||
<button class="print-btn" onclick="window.print()">
|
||||
🖶 {{ __('Imprimir / Guardar PDF') }}
|
||||
</button>
|
||||
|
||||
{{-- ====================================================
|
||||
HEADER
|
||||
===================================================== --}}
|
||||
<div class="report-header">
|
||||
<div class="logo-placeholder">LOGO<br>EMPRESA</div>
|
||||
<div class="report-header-info">
|
||||
<div class="report-title">{{ $project->name }}</div>
|
||||
@if($project->address)
|
||||
<div class="report-subtitle">{{ $project->address }}</div>
|
||||
@endif
|
||||
<div class="report-subtitle" style="margin-top:8px;">
|
||||
@if($project->start_date)
|
||||
Inicio: <strong style="color:#1f2937">{{ $project->start_date->format('d/m/Y') }}</strong>
|
||||
@endif
|
||||
@if($project->end_date_estimated)
|
||||
• Fin estimado: <strong style="color:#1f2937">{{ $project->end_date_estimated->format('d/m/Y') }}</strong>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-meta">
|
||||
<strong>Informe de Proyecto</strong>
|
||||
Generado el {{ now()->format('d/m/Y H:i') }}<br>
|
||||
Estado:
|
||||
<span class="badge {{ $project->status === 'completed' ? 'badge-completed' : ($project->status === 'in_progress' ? 'badge-in_progress' : 'badge-planned') }}">
|
||||
{{ ucfirst(str_replace('_', ' ', $project->status ?? 'N/A')) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ====================================================
|
||||
SUMMARY STATS
|
||||
===================================================== --}}
|
||||
<div class="section-title">Resumen General</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ $stats['total_features'] }}</div>
|
||||
<div class="stat-label">Total elementos</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color:#22c55e;">{{ $stats['completed_features'] }}</div>
|
||||
<div class="stat-label">Completados</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color:#f59e0b;">{{ $stats['avg_progress'] }}%</div>
|
||||
<div class="stat-label">Progreso medio</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color:#6366f1;">{{ $stats['total_inspections'] }}</div>
|
||||
<div class="stat-label">Inspecciones</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value" style="color:{{ $stats['open_issues'] > 0 ? '#ef4444' : '#22c55e' }};">{{ $stats['open_issues'] }}</div>
|
||||
<div class="stat-label">Issues abiertos</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ====================================================
|
||||
PHASES
|
||||
===================================================== --}}
|
||||
<div class="section-title">Detalle por Fase</div>
|
||||
|
||||
@forelse($phases as $phase)
|
||||
@php
|
||||
$phaseFeatures = $phase->layers->flatMap(fn($l) => $l->features);
|
||||
$phaseColor = $phase->color ?? '#3b82f6';
|
||||
@endphp
|
||||
<div class="phase-block" style="border-left-color:{{ $phaseColor }};">
|
||||
<div class="phase-header" style="border-left-color:{{ $phaseColor }};">
|
||||
<div>
|
||||
<div class="phase-name">{{ $phase->name }}</div>
|
||||
<div class="phase-meta">
|
||||
@if($phase->planned_start)
|
||||
{{ $phase->planned_start->format('d/m/Y') }}
|
||||
—
|
||||
{{ $phase->planned_end?->format('d/m/Y') ?? 'Sin fecha fin' }}
|
||||
@else
|
||||
Sin fechas planificadas
|
||||
@endif
|
||||
• {{ $phaseFeatures->count() }} elementos
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;">
|
||||
<div style="font-size:16px;font-weight:700;color:{{ $phaseColor }};">{{ $phase->progress_percent ?? 0 }}%</div>
|
||||
<div class="phase-progress-bar-wrap" style="margin-top:4px;">
|
||||
<div class="phase-progress-bar"
|
||||
style="width:{{ min(100, $phase->progress_percent ?? 0) }}%;background:{{ $phaseColor }};"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($phaseFeatures->count() > 0)
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Elemento</th>
|
||||
<th>Estado</th>
|
||||
<th>Progreso</th>
|
||||
<th>Responsable</th>
|
||||
<th>Última inspección</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($phaseFeatures as $feature)
|
||||
@php
|
||||
$lastInspection = $feature->inspections->sortByDesc('created_at')->first();
|
||||
@endphp
|
||||
<tr>
|
||||
<td>{{ $feature->name ?? 'Sin nombre' }}</td>
|
||||
<td>
|
||||
<span class="badge badge-{{ $feature->status ?? 'default' }}">
|
||||
{{ match($feature->status) {
|
||||
'planned' => 'Planificado',
|
||||
'started' => 'Iniciado',
|
||||
'in_progress' => 'En progreso',
|
||||
'completed' => 'Completado',
|
||||
'verified' => 'Verificado',
|
||||
default => ($feature->status ?? 'N/A'),
|
||||
} }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex;align-items:center;gap:6px;">
|
||||
<div style="flex:1;background:#e5e7eb;border-radius:4px;height:6px;min-width:60px;">
|
||||
<div style="height:6px;border-radius:4px;background:{{ $phaseColor }};width:{{ min(100, $feature->progress ?? 0) }}%;"></div>
|
||||
</div>
|
||||
<span style="font-size:11px;color:#6b7280;white-space:nowrap;">{{ $feature->progress ?? 0 }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ $feature->responsible ?? ($feature->responsibleUser?->name ?? '—') }}</td>
|
||||
<td>{{ $lastInspection?->created_at?->format('d/m/Y') ?? '—' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
@else
|
||||
<div style="padding:12px 14px;color:#9ca3af;font-style:italic;font-size:12px;">
|
||||
Sin elementos registrados en esta fase.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div style="padding:16px;color:#9ca3af;text-align:center;font-style:italic;">
|
||||
No hay fases registradas en este proyecto.
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
{{-- ====================================================
|
||||
Footer
|
||||
===================================================== --}}
|
||||
<div style="margin-top:32px;padding-top:12px;border-top:1px solid #e5e7eb;display:flex;justify-content:space-between;font-size:11px;color:#9ca3af;">
|
||||
<span>ConstProgress — Sistema de Gestión de Obras</span>
|
||||
<span>{{ now()->format('d/m/Y H:i') }}</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user