7d854ffb0a
- 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>
296 lines
13 KiB
PHP
296 lines
13 KiB
PHP
<div>
|
|
<div class="mb-6">
|
|
<h2 class="text-xl font-bold mb-4">{{ __('Reports and Analytics') }}</h2>
|
|
<div class="mb-4">\n <div class="flex space-x-3">\n <a href="{{ route("reports.export.projects") }}"\n class="btn btn-success btn-sm flex items-center">\n <i class="fas fa-file-excel mr-1"></i> {{ __('Export') }} {{ __('Projects') }}\n </a>\n <a href="{{ route("reports.export.phases") }}"\n class="btn btn-info btn-sm flex items-center">\n <i class="fas fa-file-excel mr-1"></i> {{ __('Export') }} {{ __('Phases') }}\n </a>\n <a href="{{ route("reports.export.inspections") }}"\n class="btn btn-warning btn-sm flex items-center">\n <i class="fas fa-file-excel mr-1"></i> {{ __('Export') }} {{ __('Inspections') }}\n </a>\n </div>\n </div>
|
|
|
|
<div class="flex flex-wrap gap-4 mb-6">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-medium">{{ __('Time range:') }}</span>
|
|
<select wire:model="dateRange" class="border border-gray-300 rounded px-3 py-1">
|
|
<option value="week">{{ __('This week') }}</option>
|
|
<option value="month" selected>{{ __('This month') }}</option>
|
|
<option value="quarter">{{ __('This quarter') }}</option>
|
|
<option value="year">{{ __('This year') }}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button wire:click="loadChartData"
|
|
class="btn btn-primary btn-sm">
|
|
{{ __('Update') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
@if(isset($chartData['months']))
|
|
<div class="grid gap-6 mb-8">
|
|
{{-- Project progress chart --}}
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<h3 class="text-lg font-semibold mb-4">{{ __('Project Progress (last 6 months)') }}</h3>
|
|
<div style="position: relative; height: 300px;">
|
|
<canvas id="projectProgressChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Inspections by type chart --}}
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<h3 class="text-lg font-semibold mb-4">{{ __('Inspections by Type') }}</h3>
|
|
<div style="position: relative; height: 300px;">
|
|
<canvas id="inspectionTypesChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Projects by status chart --}}
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<h3 class="text-lg font-semibold mb-4">{{ __('Projects by Status') }}</h3>
|
|
<div style="position: relative; height: 300px;">
|
|
<canvas id="projectsByStatusChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Average progress by project chart --}}
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<h3 class="text-lg font-semibold mb-4">{{ __('Average Progress by Project') }}</h3>
|
|
<div style="position: relative; height: 300px;">
|
|
<canvas id="projectPhaseProgressChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Key metrics cards --}}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<div class="text-sm text-gray-500 uppercase tracking-wide mb-2">
|
|
{{ __('Total Active Projects') }}
|
|
</div>
|
|
<div class="text-2xl font-bold">
|
|
{{ \App\Models\Project::where('status', 'in_progress')->count() }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<div class="text-sm text-gray-500 uppercase tracking-wide mb-2">
|
|
{{ __('Inspections This Month') }}
|
|
</div>
|
|
<div class="text-2xl font-bold">
|
|
{{ \App\Models\Inspection::whereDate('created_at', '>=', now()->startOfMonth())->count() }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<div class="text-sm text-gray-500 uppercase tracking-wide mb-2">
|
|
{{ __('Average Progress') }}
|
|
</div>
|
|
<div class="text-2xl font-bold">
|
|
@php
|
|
$avgProgress = \App\Models\Phase::whereNotNull('progress_percent')
|
|
->avg('progress_percent') ?? 0;
|
|
@endphp
|
|
{{ number_format($avgProgress, 1) }}%
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-lg shadow p-4">
|
|
<div class="text-sm text-gray-500 uppercase tracking-wide mb-2">
|
|
{{ __('Completed Projects') }}
|
|
</div>
|
|
<div class="text-2xl font-bold">
|
|
{{ \App\Models\Project::where('status', 'completed')->count() }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="bg-white rounded-lg shadow p-6 text-center">
|
|
<p class="text-gray-500">{{ __('Loading data...') }}</p>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<script>
|
|
// Wait for Alpine to initialize
|
|
document.addEventListener('alpine:init', () => {
|
|
// Initialize charts when data is available
|
|
window.addEventListener('livewire:load', function() {
|
|
initializeCharts();
|
|
});
|
|
|
|
window.addEventListener('livewire:updated', function() {
|
|
initializeCharts();
|
|
});
|
|
|
|
function initializeCharts() {
|
|
if (typeof Chart === 'undefined') {
|
|
console.warn('Chart.js not loaded');
|
|
return;
|
|
}
|
|
|
|
// Destroy existing charts if they exist
|
|
const chartIds = ['projectProgressChart', 'inspectionTypesChart', 'projectsByStatusChart', 'projectPhaseProgressChart'];
|
|
chartIds.forEach(id => {
|
|
const ctx = document.getElementById(id);
|
|
if (ctx) {
|
|
// Check if chart instance exists and destroy it
|
|
if (ctx.chart instanceof Chart) {
|
|
ctx.chart.destroy();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Project Progress Chart (Line)
|
|
const projectProgressCtx = document.getElementById('projectProgressChart');
|
|
if (projectProgressCtx) {
|
|
new Chart(projectProgressCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: @json($chartData['months'] ?? []),
|
|
datasets: @json($chartData['projectProgress'] ?? [])
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 100,
|
|
title: {
|
|
display: true,
|
|
text: '{{ __("Progress") }} (%)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Inspection Types Chart (Bar)
|
|
const inspectionTypesCtx = document.getElementById('inspectionTypesChart');
|
|
if (inspectionTypesCtx) {
|
|
new Chart(inspectionTypesCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: @json($chartData['inspectionTypes']['labels'] ?? []),
|
|
datasets: [{
|
|
label: '{{ __("Inspections") }}',
|
|
data: @json($chartData['inspectionTypes']['data'] ?? []),
|
|
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
title: {
|
|
display: true,
|
|
text: '{{ __("Total") }}'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Projects by Status Chart (Pie/Doughnut)
|
|
const projectsByStatusCtx = document.getElementById('projectsByStatusChart');
|
|
if (projectsByStatusCtx) {
|
|
new Chart(projectsByStatusCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: @json($chartData['projectsByStatus']['labels'] ?? []),
|
|
datasets: [{
|
|
label: '{{ __("Projects by Status") }}',
|
|
data: @json($chartData['projectsByStatus']['data'] ?? []),
|
|
backgroundColor: [
|
|
'rgba(255, 99, 132, 0.5)',
|
|
'rgba(54, 162, 235, 0.5)',
|
|
'rgba(255, 206, 86, 0.5)',
|
|
'rgba(75, 192, 192, 0.5)',
|
|
'rgba(153, 102, 255, 0.5)',
|
|
'rgba(255, 159, 64, 0.5)'
|
|
],
|
|
borderColor: [
|
|
'rgba(255, 99, 132, 1)',
|
|
'rgba(54, 162, 235, 1)',
|
|
'rgba(255, 206, 86, 1)',
|
|
'rgba(75, 192, 192, 1)',
|
|
'rgba(153, 102, 255, 1)',
|
|
'rgba(255, 159, 64, 1)'
|
|
],
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
title: {
|
|
display: false
|
|
},
|
|
legend: {
|
|
position: 'bottom'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Project Phase Progress Chart (Bar - Horizontal)
|
|
const projectPhaseProgressCtx = document.getElementById('projectPhaseProgressChart');
|
|
if (projectPhaseProgressCtx) {
|
|
// Sort by progress descending
|
|
const sortedData = (@json($chartData['projectPhaseProgress'] ?? [])).sort((a, b) => b.progress - a.progress);
|
|
|
|
new Chart(projectPhaseProgressCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: sortedData.map(item => item.name),
|
|
datasets: [{
|
|
label: '{{ __("Average Progress") }} (%)',
|
|
data: sortedData.map(item => item.progress),
|
|
backgroundColor: 'rgba(75, 192, 192, 0.5)',
|
|
borderColor: 'rgba(75, 192, 192, 1)',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
indexAxis: 'y', // Horizontal bars
|
|
plugins: {
|
|
title: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
beginAtZero: true,
|
|
max: 100,
|
|
title: {
|
|
display: true,
|
|
text: '{{ __("Progress") }} (%)'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
</script>
|