- Create SendScheduledReport job (runs daily 08:00) - Create ScheduledReportMail Mailable with Blade template - Add receive_reports boolean to users table (migration) - Register job in routes/console.php Tests: 101 passing (319 assertions)
157 lines
9.5 KiB
PHP
157 lines
9.5 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Informe Programado - {{ $project->name }}</title>
|
|
<style>
|
|
body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 14px; color: #1f2937; line-height: 1.6; margin: 0; padding: 20px; background: #f8fafc; }
|
|
.container { max-width: 700px; margin: 0 auto; background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); overflow: hidden; }
|
|
.header { background: #1e3a8a; color: #fff; padding: 24px; }
|
|
.header h1 { margin: 0; font-size: 24px; font-weight: 700; }
|
|
.header p { margin: 8px 0 0; opacity: 0.9; font-size: 14px; }
|
|
.content { padding: 24px; }
|
|
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 24px; }
|
|
.stat { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; text-align: center; }
|
|
.stat-value { font-size: 28px; font-weight: 700; color: #2563eb; line-height: 1; }
|
|
.stat-label { font-size: 12px; color: #6b7280; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.03em; }
|
|
.stat-value.completed { color: #22c55e; }
|
|
.stat-value.issues { color: #ef4444; }
|
|
.section { margin-bottom: 24px; }
|
|
.section-title { font-size: 16px; font-weight: 700; color: #1e3a8a; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px; margin-bottom: 16px; }
|
|
.phase { border: 1px solid #e5e7eb; border-radius: 8px; margin-bottom: 12px; overflow: hidden; }
|
|
.phase-header { background: #f8fafc; padding: 12px 16px; border-left: 4px solid #3b82f6; display: flex; justify-content: space-between; align-items: center; }
|
|
.phase-name { font-weight: 700; color: #1e3a8a; font-size: 14px; }
|
|
.phase-progress { font-weight: 700; color: #3b82f6; }
|
|
.phase-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
.phase-table th { background: #f1f5f9; padding: 8px 12px; text-align: left; font-weight: 600; color: #374151; border-bottom: 1px solid #cbd5e1; }
|
|
.phase-table td { padding: 8px 12px; border-bottom: 1px solid #e5e7eb; }
|
|
.badge { display: inline-block; padding: 2px 8px; border-radius: 9999px; font-size: 10px; font-weight: 600; text-transform: uppercase; }
|
|
.badge-planned { background: #f3f4f6; color: #6b7280; }
|
|
.badge-in_progress { background: #fef3c7; color: #92400e; }
|
|
.badge-completed { background: #d1fae5; color: #065f46; }
|
|
.badge-verified { background: #ede9fe; color: #5b21b6; }
|
|
.progress-bar { background: #e5e7eb; border-radius: 4px; height: 6px; width: 80px; overflow: hidden; }
|
|
.progress-bar-fill { height: 100%; border-radius: 4px; background: #22c55e; }
|
|
.footer { background: #f8fafc; border-top: 1px solid #e5e7eb; padding: 16px 24px; font-size: 12px; color: #9ca3af; text-align: center; }
|
|
@media print { .container { box-shadow: none; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>{{ $project->name }}</h1>
|
|
<p>Informe programado generado el {{ now()->format('d/m/Y H:i') }}</p>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<div class="stats">
|
|
<div class="stat">
|
|
<div class="stat-value">{{ $data['stats']['total_features'] ?? 0 }}</div>
|
|
<div class="stat-label">Total elementos</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value completed">{{ $data['stats']['completed_features'] ?? 0 }}</div>
|
|
<div class="stat-label">Completados</div>
|
|
</div>
|
|
<div class="stat">
|
|
<div class="stat-value">{{ round($data['stats']['avg_progress'] ?? 0) }}%</div>
|
|
<div class="stat-label">Progreso medio</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Detalle por Fase</div>
|
|
@forelse($data['phases'] as $phase)
|
|
@php
|
|
$phaseFeatures = $phase->layers->flatMap(fn($l) => $l->features);
|
|
$phaseColor = $phase->color ?? '#3b82f6';
|
|
@endphp
|
|
<div class="phase" style="border-left-color:{{ $phaseColor }};">
|
|
<div class="phase-header" style="border-left-color:{{ $phaseColor }};">
|
|
<div>
|
|
<div class="phase-name">{{ $phase->name }}</div>
|
|
<div style="font-size:12px;color:#6b7280;">
|
|
@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="progress-bar" style="margin-top:4px;">
|
|
<div class="progress-bar-fill" style="width:{{ min(100, $phase->progress_percent ?? 0) }}%;background:{{ $phaseColor }};"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@if($phaseFeatures->count() > 0)
|
|
<table class="phase-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Elemento</th>
|
|
<th>Estado</th>
|
|
<th>Progreso</th>
|
|
<th>Responsable</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($phaseFeatures as $feature)
|
|
<tr>
|
|
<td>{{ $feature->name ?? 'Sin nombre' }}</td>
|
|
<td>
|
|
<span class="badge badge-{{ $feature->status ?? 'planned' }}">
|
|
{{ 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 class="progress-bar-fill" style="width:{{ min(100, $feature->progress ?? 0) }}%;background:{{ $phaseColor }};"></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>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
@else
|
|
<div style="padding:12px 16px;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
|
|
</div>
|
|
|
|
<div class="section">
|
|
<div class="section-title">Parámetros del Informe</div>
|
|
<p><strong>Rango de fechas:</strong> {{ $filters->date_from ? $filters->date_from->format('d/m/Y') : 'Sin fecha desde' }} — {{ $filters->date_to ? $filters->date_to->format('d/m/Y') : 'Sin fecha hasta' }}</p>
|
|
<p><strong>Secciones:</strong> {{ implode(', ', collect($filters->entity_types)->map(fn ($k) => ucfirst(str_replace('_', ' ', $k)))->toArray()) }}</p>
|
|
<p><strong>Incluir fotos:</strong> {{ $filters->include_photos ? 'Sí' : 'No' }}</p>
|
|
<p><strong>Incluir gráficos:</strong> {{ $filters->include_charts ? 'Sí' : 'No' }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
ConstruProgress — Sistema de Gestión de Obras<br>
|
|
Informe generado automáticamente el {{ now()->format('d/m/Y H:i') }}
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html> |