Files
construprogress/resources/views/livewire/reports/report-builder.blade.php
T
Javier Braña ee32544525 feat(reports): complete reporting system with deviations, progress curves, multi-format export
- Added planned/actual/baseline dates to Phase and Feature models
- Created ProgressSnapshot model + migration for historical tracking
- Implemented CaptureDailyProgressSnapshot job (scheduled daily at 06:30)
- Added DeviationCalculator logic (SPI, planned progress, deviation days)
- ReportGenerator service with complete data aggregation
- ReportController with builder, preview, generate (HTML/Excel)
- ReportBuilder Livewire component with date range, entity selection, format
- ProjectReportExport with 10 sheets (Summary, Phases, Features, Inspections, Issues, Tasks, Deviations, Media, Progress Curve, Parameters)
- Blade partials for all sections (header, summary, phases, features, inspections, issues, tasks, media, deviations, progress curve)
- New routes: /projects/{project}/reports/*
- Added 'generate reports' permission
- All 101 tests passing
2026-08-24 12:32:08 +02:00

179 lines
9.5 KiB
PHP

@extends('layouts.app')
@section('content')
<div class="container mx-auto px-4 py-8">
{{-- Header --}}
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-8">
<div>
<h1 class="text-2xl font-bold text-gray-800">
{{ __('Generar Informe') }}: {{ $project->name }}
</h1>
<p class="text-gray-500 mt-1">
{{ $project->address ?? 'Sin dirección' }}
@if($project->reference)
&nbsp;|&nbsp; Ref: {{ $project->reference }}
@endif
</p>
</div>
<a href="{{ route('projects.map', $project) }}" class="btn btn-ghost btn-sm">
<x-heroicon-o-arrow-left class="w-4 h-4 mr-1" /> {{ __('Volver al mapa') }}
</a>
</div>
{{-- Report Builder Form --}}
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<form wire:submit.prevent="generateReport" class="space-y-6">
{{-- Date Range --}}
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<x-heroicon-o-calendar-days class="w-5 h-5 text-blue-600" />
{{ __('Rango de fechas') }}
</h3>
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('Fecha desde') }}
</label>
<input type="date"
wire:model="filters.date_from"
class="input input-bordered w-full"
placeholder="dd/mm/yyyy">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">
{{ __('Fecha hasta') }}
</label>
<input type="date"
wire:model="filters.date_to"
class="input input-bordered w-full"
placeholder="dd/mm/yyyy">
</div>
<div class="md:col-span-4 flex flex-wrap gap-2">
<button type="button" wire:click="setDateRange('week')" class="btn btn-sm btn-outline">
{{ __('Esta semana') }}
</button>
<button type="button" wire:click="setDateRange('month')" class="btn btn-sm btn-outline">
{{ __('Este mes') }}
</button>
<button type="button" wire:click="setDateRange('quarter')" class="btn btn-sm btn-outline">
{{ __('Este trimestre') }}
</button>
<button type="button" wire:click="setDateRange('year')" class="btn btn-sm btn-outline">
{{ __('Este año') }}
</button>
<button type="button" wire:click="clearDateRange" class="btn btn-sm btn-ghost">
{{ __('Limpiar') }}
</button>
</div>
</div>
</div>
{{-- Entity Types --}}
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<x-heroicon-o-squares-2x2 class="w-5 h-5 text-blue-600" />
{{ __('Secciones a incluir') }}
</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-3">
@foreach($availableEntities as $key => $label)
<label class="flex items-center gap-2 cursor-pointer p-3 border rounded-lg hover:bg-gray-50 transition-colors">
<input type="checkbox"
wire:model="filters.entity_types"
value="{{ $key }}"
class="checkbox checkbox-primary">
<span class="text-sm font-medium">{{ $label }}</span>
</label>
@endforeach
</div>
</div>
{{-- Options --}}
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<x-heroicon-o-cog-6-tooth class="w-5 h-5 text-blue-600" />
{{ __('Opciones') }}
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="filters.include_photos" class="checkbox checkbox-primary">
<span class="text-sm">{{ __('Incluir fotos') }}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" wire:model="filters.include_charts" class="checkbox checkbox-primary">
<span class="text-sm">{{ __('Incluir gráficos (curva S)') }}</span>
</label>
</div>
</div>
{{-- Format Selection --}}
<div>
<h3 class="text-lg font-semibold text-gray-700 mb-4 flex items-center gap-2">
<x-heroicon-o-document-arrow-down class="w-5 h-5 text-blue-600" />
{{ __('Formato de salida') }}
</h3>
<div class="flex flex-wrap gap-4">
<label class="flex items-center gap-2 cursor-pointer p-4 border-2 rounded-lg {{ $filters['format'] === 'html' ? 'border-blue-500 bg-blue-50' : 'border-gray-200 hover:border-gray-300' }} transition-colors">
<input type="radio" wire:model="filters.format" value="html" class="radio radio-primary">
<div>
<span class="font-medium">{{ __('HTML (Imprimible / PDF)') }}</span>
<p class="text-xs text-gray-500">{{ __('Visualizar en navegador, imprimir o guardar como PDF') }}</p>
</div>
</label>
<label class="flex items-center gap-2 cursor-pointer p-4 border-2 rounded-lg {{ $filters['format'] === 'excel' ? 'border-green-500 bg-green-50' : 'border-gray-200 hover:border-gray-300' }} transition-colors">
<input type="radio" wire:model="filters.format" value="excel" class="radio radio-success">
<div>
<span class="font-medium">{{ __('Excel (.xlsx)') }}</span>
<p class="text-xs text-gray-500">{{ __('Múltiples hojas: Resumen, Fases, Elementos, Inspecciones, Issues, Tareas, Desvíos, Media, Curva S, Parámetros') }}</p>
</div>
</label>
</div>
</div>
{{-- Actions --}}
<div class="flex flex-wrap gap-4 pt-6 border-t border-gray-200">
<button type="submit"
wire:loading.attr="disabled"
class="btn btn-primary gap-2">
<x-heroicon-o-document-arrow-down class="w-5 h-5" />
{{ $filters['format'] === 'excel' ? __('Descargar Excel') : __('Generar Informe HTML') }}
</button>
<button type="button"
wire:click="previewReport"
wire:loading.attr="disabled"
class="btn btn-outline gap-2">
<x-heroicon-o-eye class="w-5 h-5" />
{{ __('Previsualizar') }}
</button>
</div>
</form>
{{-- Preview Modal --}}
@if($showPreview)
<div class="fixed inset-0 z-50 overflow-y-auto" wire:ignore.self>
<div class="flex min-h-full items-center justify-center p-4">
<div class="fixed inset-0 bg-black/50" wire:click="closePreview"></div>
<div class="relative bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
<div class="flex items-center justify-between p-4 border-b">
<h3 class="text-lg font-semibold">{{ __('Previsualización del Informe') }}</h3>
<button wire:click="closePreview" class="btn btn-ghost btn-sm">
<x-heroicon-o-x-mark class="w-5 h-5" />
</button>
</div>
<div class="p-4 overflow-y-auto max-h-[70vh]">
@if($previewData)
@include('reports.partials._preview', ['data' => $previewData])
@else
<div class="text-center py-8 text-gray-500">
<x-heroicon-o-arrow-path class="w-8 h-8 mx-auto animate-spin text-blue-500 mb-2" />
<p>{{ __('Generando previsualización...') }}</p>
</div>
@endif
</div>
</div>
</div>
</div>
@endif
</div>
</div>
@endsection