diff --git a/.gitignore b/.gitignore
index b71b1ea..c9625e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,4 @@
Homestead.json
Homestead.yaml
Thumbs.db
+.claude/worktrees/
diff --git a/app/Http/Controllers/ProjectReportController.php b/app/Http/Controllers/ProjectReportController.php
new file mode 100644
index 0000000..3c91c0d
--- /dev/null
+++ b/app/Http/Controllers/ProjectReportController.php
@@ -0,0 +1,36 @@
+hasRole('Admin') && !$project->users()->where('user_id', $user->id)->exists()) {
+ abort(403);
+ }
+
+ $phases = $project->phases()
+ ->with(['layers.features.inspections', 'layers.features.issues'])
+ ->orderBy('order')
+ ->get();
+
+ $stats = [
+ 'total_features' => $phases->flatMap(fn($p) => $p->layers)->flatMap(fn($l) => $l->features)->count(),
+ 'completed_features' => $phases->flatMap(fn($p) => $p->layers)->flatMap(fn($l) => $l->features)->where('status', 'completed')->count(),
+ 'total_inspections' => \App\Models\Inspection::where('project_id', $project->id)->count(),
+ 'open_issues' => \App\Models\Issue::where('project_id', $project->id)->where('status', 'open')->count(),
+ 'avg_progress' => round($phases->avg('progress_percent') ?? 0),
+ ];
+
+ $pdf_data = compact('project', 'phases', 'stats');
+
+ // Use Blade to render HTML, then return as "print" view
+ // (barryvdh/laravel-dompdf is not installed, so we render a printable HTML page)
+ return view('reports.project-report', $pdf_data);
+ }
+}
diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php
index a17bea5..9220884 100644
--- a/app/Http/Middleware/SetLocale.php
+++ b/app/Http/Middleware/SetLocale.php
@@ -41,9 +41,9 @@ class SetLocale
}
}
- // 4. Default to English
+ // 4. Default to app locale
if (!$locale) {
- $locale = 'en';
+ $locale = config('app.locale', 'es');
}
App::setLocale($locale);
diff --git a/app/Livewire/AdminUsers.php b/app/Livewire/AdminUsers.php
index ef3926e..40f21e7 100644
--- a/app/Livewire/AdminUsers.php
+++ b/app/Livewire/AdminUsers.php
@@ -9,44 +9,38 @@ use Illuminate\Support\Facades\Auth;
class AdminUsers extends Component
{
- public $users;
+ public string $search = '';
public $roles;
- public function mount()
+ public function mount(): void
{
- if (!Auth::user()->hasRole('Admin')) {
- abort(403);
- }
- $this->roles = Role::all();
- $this->loadUsers();
+ if (!Auth::user()->hasRole('Admin')) abort(403);
+ $this->roles = Role::orderBy('name')->get();
}
- public function loadUsers()
+ public function getUsersProperty()
{
- $this->users = User::with('roles')->orderBy('name')->get();
+ return User::with('roles')
+ ->when($this->search, fn($q) =>
+ $q->where(fn($q2) => $q2
+ ->where('name', 'like', '%' . $this->search . '%')
+ ->orWhere('email', 'like', '%' . $this->search . '%')))
+ ->orderBy('name')
+ ->get();
}
- public function updateRole($userId, $roleName)
+ public function deleteUser(int $userId): void
{
- $user = Auth::user();
- if (!$user->hasRole('Admin')) {
- session()->flash('error', 'Solo administradores.');
+ if ($userId === Auth::id()) {
+ $this->dispatch('notify', 'No puedes eliminarte a ti mismo.');
return;
}
-
- $targetUser = User::findOrFail($userId);
- if ($targetUser->id === $user->id && $targetUser->hasRole('Admin')) {
- session()->flash('error', 'No puedes cambiarte el rol a ti mismo.');
- return;
- }
-
- $targetUser->syncRoles([$roleName]);
- $this->loadUsers();
- $this->dispatch('notify', 'Rol actualizado.');
+ User::findOrFail($userId)->delete();
+ $this->dispatch('notify', 'Usuario eliminado.');
}
public function render()
{
return view('livewire.admin-users');
}
-}
\ No newline at end of file
+}
diff --git a/app/Livewire/CompanyForm.php b/app/Livewire/CompanyForm.php
new file mode 100644
index 0000000..43326c0
--- /dev/null
+++ b/app/Livewire/CompanyForm.php
@@ -0,0 +1,106 @@
+exists) {
+ $this->company = $company;
+ $this->name = $company->name;
+ $this->apodo = $company->apodo ?? '';
+ $this->tax_id = $company->tax_id ?? '';
+ $this->estado = $company->estado ?? 'activo';
+ $this->type = $company->type ?? 'other';
+ $this->address = $company->address ?? '';
+ $this->phone = $company->phone ?? '';
+ $this->email = $company->email ?? '';
+ $this->website = $company->website ?? '';
+ $this->notes = $company->notes ?? '';
+ }
+ }
+
+ protected function rules(): array
+ {
+ $id = $this->company?->id ?? 'NULL';
+ return [
+ 'name' => 'required|string|max:255',
+ 'apodo' => 'nullable|string|max:100',
+ 'tax_id' => "nullable|string|max:50|unique:companies,tax_id,{$id}",
+ 'estado' => 'required|in:activo,inactivo,suspendido',
+ 'type' => 'required|in:owner,constructor,subcontractor,consultant,supplier,other',
+ 'address' => 'nullable|string',
+ 'phone' => 'nullable|string|max:30',
+ 'email' => 'nullable|email|max:255',
+ 'website' => 'nullable|url|max:255',
+ 'notes' => 'nullable|string',
+ 'logo' => 'nullable|image|max:2048',
+ ];
+ }
+
+ public function save(): void
+ {
+ $this->validate();
+
+ $data = [
+ 'name' => $this->name,
+ 'apodo' => $this->apodo ?: null,
+ 'tax_id' => $this->tax_id ?: null,
+ 'estado' => $this->estado,
+ 'type' => $this->type,
+ 'address' => $this->address ?: null,
+ 'phone' => $this->phone ?: null,
+ 'email' => $this->email ?: null,
+ 'website' => $this->website ?: null,
+ 'notes' => $this->notes ?: null,
+ ];
+
+ if ($this->logo) {
+ // Delete old logo when replacing
+ if ($this->company?->logo_path) {
+ Storage::disk('public')->delete($this->company->logo_path);
+ }
+ $data['logo_path'] = $this->logo->store('company-logos', 'public');
+ }
+
+ if ($this->company && $this->company->exists) {
+ $this->company->update($data);
+ session()->flash('notify', 'Empresa actualizada correctamente.');
+ } else {
+ Company::create($data);
+ session()->flash('notify', 'Empresa creada correctamente.');
+ }
+
+ $this->redirect(route('companies.manage'), navigate: true);
+ }
+
+ public function render()
+ {
+ return view('livewire.company-form');
+ }
+}
diff --git a/app/Livewire/CompanyManagement.php b/app/Livewire/CompanyManagement.php
index 3b47d23..54b1731 100644
--- a/app/Livewire/CompanyManagement.php
+++ b/app/Livewire/CompanyManagement.php
@@ -3,234 +3,65 @@
namespace App\Livewire;
use Livewire\Component;
-use Livewire\WithFileUploads;
+use Livewire\Attributes\Layout;
use App\Models\Company;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Http\Response;
+#[Layout('layouts.app')]
class CompanyManagement extends Component
{
- use WithFileUploads;
-
- // Form state
- public $name = '';
- public $tax_id = '';
- public $address = '';
- public $email = '';
- public $website = '';
- public $type = 'other';
- public $notes = '';
- public $apodo = '';
- public $estado = 'activo';
- public $logo = null;
-
- // UI state
- public $showCreateForm = false;
- public $showEditForm = false;
- public $editingCompanyId = null;
- public $search = '';
-
- // Filter state
- public $filterType = '';
- public $filterEstado = '';
-
- // Validation rules
- protected $rules = [
- 'name' => 'required|string|max:255',
- 'apodo' => 'nullable|string|max:100',
- 'tax_id' => 'nullable|string|max:50|unique:companies,tax_id,',
- 'estado' => 'required|in:activo,inactivo,suspendido',
- 'address' => 'nullable|string',
- 'phone' => 'nullable|string|max:20',
- 'email' => 'nullable|email|max:255',
- 'website' => 'nullable|url|max:255',
- 'type' => 'required|in:owner,constructor,subcontractor,consultant,supplier,other',
- 'notes' => 'nullable|string',
- 'logo' => 'nullable|image|max:2048', // 2MB max
- ];
-
- public function mount()
- {
- $this->resetForm();
- }
-
- public function resetForm()
- {
- $this->name = '';
- $this->tax_id = '';
- $this->address = '';
- $this->phone = '';
- $this->email = '';
- $this->website = '';
- $this->type = 'other';
- $this->notes = '';
- $this->apodo = '';
- $this->estado = 'activo';
- $this->logo = null;
- $this->editingCompanyId = null;
- $this->showCreateForm = false;
- $this->showEditForm = false;
- $this->resetErrorBag();
- $this->resetValidation();
- }
-
- public function resetFilters()
- {
- $this->search = '';
- $this->filterType = '';
- $this->filterEstado = '';
- }
-
- public function toggleCreateForm()
- {
- $this->showCreateForm = !$this->showCreateForm;
- if ($this->showCreateForm) {
- $this->showEditForm = false;
- $this->resetForm();
- }
- }
-
- public function editCompany(Company $company)
- {
- $this->editingCompanyId = $company->id;
- $this->name = $company->name;
- $this->tax_id = $company->tax_id;
- $this->address = $company->address;
- $this->phone = $company->phone;
- $this->email = $company->email;
- $this->website = $company->website;
- $this->type = $company->type;
- $this->notes = $company->notes;
- $this->apodo = $company->apodo;
- $this->estado = $company->estado;
- // Note: logo is not populated for security reasons
- $this->showEditForm = true;
- $this->showCreateForm = false;
- }
-
- public function updateCompany()
- {
- $this->validate();
-
- $company = Company::findOrFail($this->editingCompanyId);
-
- $data = [
- 'name' => $this->name,
- 'tax_id' => $this->tax_id,
- 'address' => $this->address,
- 'phone' => $this->phone,
- 'email' => $this->email,
- 'website' => $this->website,
- 'type' => $this->type,
- 'notes' => $this->notes,
- ];
-
- if ($this->logo) {
- $logoPath = $this->logo->store('company-logos', 'public');
- $data['logo_path'] = $logoPath;
- }
-
- $company->update($data);
-
- session()->flash('message', 'Empresa actualizada correctamente.');
- $this->resetForm();
- }
-
- public function createCompany()
- {
- $this->validate();
-
- $data = [
- 'name' => $this->name,
- 'tax_id' => $this->tax_id,
- 'address' => $this->address,
- 'phone' => $this->phone,
- 'email' => $this->email,
- 'website' => $this->website,
- 'type' => $this->type,
- 'notes' => $this->notes,
- ];
-
- if ($this->logo) {
- $logoPath = $this->logo->store('company-logos', 'public');
- $data['logo_path'] = $logoPath;
- }
-
- Company::create($data);
-
- session()->flash('message', 'Empresa creada correctamente.');
- $this->resetForm();
- }
-
- public function deleteCompany(Company $company)
- {
- $company->delete(); // Soft delete
- session()->flash('message', 'Empresa eliminada correctamente.');
- }
-
+ public string $search = '';
+ public string $filterType = '';
+ public string $filterEstado = '';
+
public function getCompaniesProperty()
{
- return Company::when($this->search, function ($query) {
- $query->where('name', 'like', '%' . $this->search . '%')
- ->orWhere('apodo', 'like', '%' . $this->search . '%')
- ->orWhere('tax_id', 'like', '%' . $this->search . '%');
- })
- ->when($this->filterType, function ($query) {
- $query->where('type', $this->filterType);
- })
- ->when($this->filterEstado, function ($query) {
- $query->where('estado', $this->filterEstado);
- })
- ->withCount('projects') // Eager load project count
- ->orderBy('name')
- ->get();
+ return Company::when($this->search, function ($q) {
+ $s = '%' . $this->search . '%';
+ $q->where(fn($q2) => $q2
+ ->where('name', 'like', $s)
+ ->orWhere('apodo', 'like', $s)
+ ->orWhere('tax_id', 'like', $s));
+ })
+ ->when($this->filterType, fn($q) => $q->where('type', $this->filterType))
+ ->when($this->filterEstado, fn($q) => $q->where('estado', $this->filterEstado))
+ ->withCount('projects')
+ ->orderBy('name')
+ ->get();
}
-
+
+ public function deleteCompany(Company $company): void
+ {
+ if ($company->logo_path) {
+ Storage::disk('public')->delete($company->logo_path);
+ }
+ $company->delete();
+ $this->dispatch('notify', 'Empresa eliminada.');
+ }
+
public function exportCsv()
{
$companies = $this->getCompaniesProperty();
-
- // Create CSV content
- $headers = [
- "Content-type: text/csv",
- "Content-Disposition: attachment; filename=empresas.csv",
- "Pragma: no-cache",
- "Cache-Control: must-revalidate, post-check=0, pre-check=0",
- "Expires: 0"
- ];
-
- $callback = function() use ($companies) {
+
+ return response()->streamDownload(function () use ($companies) {
$handle = fopen('php://output', 'w');
- // Add BOM for UTF-8 in Excel
- fprintf($handle, chr(0xEF).chr(0xBB).chr(0xBF));
-
- // Header row
- fputcsv($handle, ['Nombre', 'Apodo', 'NIF/Tax ID', 'Tipo', 'Estado', 'Dirección', 'Teléfono', 'Email', 'Website', 'Proyectos Asociados', 'Fecha Creación']);
-
- foreach ($companies as $company) {
+ fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
+ fputcsv($handle, ['Nombre', 'Apodo', 'NIF/Tax ID', 'Tipo', 'Estado', 'Dirección', 'Teléfono', 'Email', 'Website', 'Proyectos', 'Creación']);
+ foreach ($companies as $c) {
fputcsv($handle, [
- $company->name,
- $company->apodo ?? '',
- $company->tax_id ?? '',
- $company->type,
- $company->estado,
- $company->address ?? '',
- $company->phone ?? '',
- $company->email ?? '',
- $company->website ?? '',
- $company->projects_count ?? 0,
- $company->created_at ? $company->created_at->format('d/m/Y H:i') : ''
+ $c->name, $c->apodo ?? '', $c->tax_id ?? '',
+ $c->type, $c->estado, $c->address ?? '',
+ $c->phone ?? '', $c->email ?? '', $c->website ?? '',
+ $c->projects_count ?? 0,
+ $c->created_at?->format('d/m/Y'),
]);
}
-
fclose($handle);
- };
-
- return response()->stream($callback, 200, $headers);
+ }, 'empresas.csv', ['Content-Type' => 'text/csv; charset=UTF-8']);
}
-
+
public function render()
{
return view('livewire.company-management');
}
-}
\ No newline at end of file
+}
diff --git a/app/Livewire/CompanyTable.php b/app/Livewire/CompanyTable.php
new file mode 100644
index 0000000..ebada74
--- /dev/null
+++ b/app/Livewire/CompanyTable.php
@@ -0,0 +1,173 @@
+setPrimaryKey('id')
+ ->setDefaultSort('name', 'asc')
+ ->setSortingPillsEnabled(false)
+ ->setAdditionalSelects([
+ 'companies.id as id',
+ 'companies.apodo as apodo',
+ 'companies.tax_id as tax_id',
+ 'companies.phone as phone',
+ 'companies.email as email',
+ 'companies.logo_path as logo_path',
+ 'companies.created_at as created_at',
+ ]);
+ }
+
+ public function builder(): Builder
+ {
+ return Company::withCount('projects');
+ }
+
+ public function columns(): array
+ {
+ return [
+ Column::make('Empresa', 'name')
+ ->sortable()
+ ->searchable()
+ ->format(function ($value, $row) {
+ $logoHtml = '';
+ if ($row->logo_path && Storage::disk('public')->exists($row->logo_path)) {
+ $url = Storage::disk('public')->url($row->logo_path);
+ $logoHtml = '';
+ } else {
+ $logoHtml = '
'.e($value).'
'; + if ($row->apodo) $html .= ''.e($row->apodo).'
'; + if ($row->tax_id) $html .= 'NIF: '.e($row->tax_id).'
'; + $html .= ''.e($value).'
'; + $html .= ''.e($row->email).'
'; + $html .= '{{ $stats['global_progress'] }}%
-| {{ __('Name') }} | -{{ __('Status') }} | -{{ __('Phases') }} | -{{ __('Progress') }} | -- |
|---|---|---|---|---|
| {{ $project->name }} | -- @php - $badgeClass = match($project->status) { - 'planning' => 'badge-ghost', - 'in_progress' => 'badge-primary', - 'paused' => 'badge-warning', - 'completed' => 'badge-success', - default => 'badge-ghost' - }; - @endphp - {{ __(ucfirst(str_replace('_', ' ', $project->status))) }} - | -{{ $project->phases_count }} | -
- @php $avg = $project->phases->avg('progress_percent'); @endphp
-
-
-
-
-
- {{ round($avg) }}%
- |
- - {{ __('Map') }} - | -
| {{ __('No results') }} | ||||
Proyectos activos
++ {{ $stats['active_projects'] }} + / {{ $stats['total_projects'] }} +
Avance global
+{{ $stats['global_progress'] }}%
+Fases con retraso
++ {{ $stats['delayed_phases'] }} +
+ @if($stats['delayed_phases'] > 0) +Requiere atención
+ @else +Sin retrasos
+ @endif +Elementos totales
+{{ $stats['total_features'] }}
+{{ $stats['total_phases'] }} fases
+Issues abiertos
+{{ $stats['open_issues'] }}
+ @if($stats['critical_issues'] > 0) +{{ $stats['critical_issues'] }} críticos
+ @else +0 críticos
+ @endif +Insp. pendientes
+{{ $stats['pending_inspections'] }}
+Por realizar
+Insp. completadas
+{{ $stats['completed_inspections'] }}
+Aprobadas
+Insp. rechazadas
++ {{ $stats['rejected_inspections'] }} +
+Requieren revisión
+No hay proyectos disponibles
+{{ $issue->title }}
+
+ @if($issue->feature)
+
+
Sin issues abiertos
++ {{ $inspection->template?->name ?? 'Inspección' }} +
+ {{ $inspStatusConfig['label'] }} +
+
+
{{ $inspection->created_at->diffForHumans() }}
+Sin inspecciones recientes
+
| {{ __('Name') }} | -{{ __('Email') }} | -{{ __('Role') }} | -{{ __('Language') }} | -{{ __('Actions') }} | +Usuario | +Rol | +Verificado | +|
|---|---|---|---|---|---|---|---|---|
| {{ $user->name }} | -{{ $user->email }} | + @forelse($this->users as $u) +|||||||
|
-
- @foreach($user->roles as $role)
-
- {{ __($role->name) }}
-
- @endforeach
+
+
+
+
+ {{ strtoupper(substr($u->name, 0, 1)) }}
+
+
+
{{ $u->name }} +{{ $u->email }} + |
- {{ strtoupper($user->locale ?? 'en') }} |
- @can('assign users')
-
- @endcan
+
+ @foreach($u->roles as $role)
+
+ {{ $role->name }}
+
+ @endforeach
+ @if($u->roles->isEmpty())
+ Sin rol
+ @endif
+
+ |
+
+ @if($u->email_verified_at)
+ |
+ + | ||||
|
+ No se encontraron usuarios + |
+ ||||||||
- {{ $project['description'] ?? 'Sin descripción disponible' }} + {{ $project['description'] ?? __('No description available') }}
- @php - $statuses = [ - 'planning' => 'Planificación', - 'in_progress' => 'En progreso', - 'on_hold' => 'En espera', - 'completed' => 'Completado', - 'cancelled' => 'Cancelado' - ]; - echo $statuses[$projectDetails['status']] ?? ucfirst($projectDetails['status']); - @endphp + {{ __(ucfirst(str_replace('_', ' ', $projectDetails['status'] ?? ''))) }}
- {{ $projectDetails['start_date'] ?? 'No definida' }} + {{ $projectDetails['start_date'] ?? __('Not defined') }}
- {{ $projectDetails['end_date'] ?? 'No definida' }} + {{ $projectDetails['end_date'] ?? __('Not defined') }}
- {{ $projectDetails['description'] ?? 'No hay descripción disponible' }} + {{ $projectDetails['description'] ?? __('No description available') }}
No hay fases definidas para este proyecto
+{{ __('No phases defined for this project') }}
{{ $order['description'] }}
- +No hay órdenes de cambio pendientes
+{{ __('No pending change orders') }}
Gestione las empresas que participan en los proyectos
+{{ __('Manage the companies that participate in projects') }}
- Complete la información de la empresa. Los campos marcados con * son obligatorios. + {{ __('Complete the company information. Fields marked with * are required.') }}
No hay empresas registradas. Cree su primera empresa usando el botón de arriba.
+{{ __('No companies registered. Create your first company using the button above.') }}
NIF/CIF: {{ $company->tax_id }}
+ @endif + + {{-- Contacto inline --}} +{{ $usersCount }}
+Personas
+{{ $projectsCount }}
+Proyectos
+{{ $avgProgress }}%
+Progreso medio
+ @if($projectsCount > 0) + + @endif +{{ $openIssues }}
+Issues abiertos
+{{ $message }}
+ @enderror +Ninguna persona asociada a esta empresa.
+| Persona | +Rol | +Estado | +Contacto | ++ |
|---|---|---|---|---|
|
+
+
+
+
+
+
+ {{ strtoupper(substr($u->first_name ?: $u->name, 0, 1)) }}{{ strtoupper(substr($u->last_name ?: '', 0, 1)) }}
+
+
+
+
+ + @if($u->title) {{ $u->title }} @endif + {{ $u->first_name && $u->last_name + ? $u->first_name . ' ' . $u->last_name + : $u->name }} + +{{ $u->email }} + |
+
+
+ @foreach($u->roles as $role)
+
+ {{ $role->name }}
+
+ @endforeach
+ @if($u->roles->isEmpty())
+ Sin rol
+ @endif
+
+ |
+ + {{ $uStatusBadge[1] }} + | +
+ @if($u->phone) {{ $u->phone }} @endif
+ |
+
+
+
+
+ |
+
La empresa ya está vinculada a todos los proyectos.
+ @else +{{ $message }}
@enderror +{{ $message }}
@enderror +Sin proyectos vinculados.
+| Proyecto | +Rol de la empresa | +Estado | +Progreso | ++ |
|---|---|---|---|---|
|
+
+ {{ $project->name }}
+
+ @if($project->address)
+ {{ $project->address }} + @endif + |
+ + + {{ $project->pivot->role_in_project }} + + | ++ {{ $psCfg[1] }} + | +
+
+
+ {{ round($avg) }}%
+
+ |
+ + + | +
Sin notas.
+ +{{ $issue->title }}
+ @if($issue->description) +{{ $issue->description }}
+ @endif +No hay issues registrados
+ @endforelse +Gestión de incidencias y problemas
+Sin issues registrados
+Crea el primer issue con el botón "Nuevo Issue".
+| Prioridad | +Título | +Feature | +Estado | +Asignado a | +Fecha | +Acciones | +
|---|---|---|---|---|---|---|
| + @php + $pClass = match($issue->priority) { + 'critical' => 'badge-purple', + 'high' => 'badge-error', + 'medium' => 'badge-warning', + 'low' => 'badge-ghost', + default => 'badge-ghost', + }; + $pLabel = match($issue->priority) { + 'critical' => 'Crítico', + 'high' => 'Alto', + 'medium' => 'Medio', + 'low' => 'Bajo', + default => ucfirst($issue->priority), + }; + @endphp + + {{ $pLabel }} + + | + + {{-- Título + descripción breve --}} +
+ {{ $issue->title }}
+ @if($issue->description)
+ {{ Str::limit($issue->description, 60) }}
+ @endif
+ @if($issue->reporter)
+
+ Reportado por {{ $issue->reporter->name }}
+
+ @endif
+ |
+
+ {{-- Feature --}}
+ + @if($issue->feature) + {{ $issue->feature->name }} + @else + — + @endif + | + + {{-- Estado --}} ++ @php + $sLabel = match($issue->status) { + 'open' => 'Abierto', + 'in_review' => 'En revisión', + 'resolved' => 'Resuelto', + 'closed' => 'Cerrado', + default => ucfirst($issue->status), + }; + @endphp + + {{ $sLabel }} + + | + + {{-- Asignado a --}} +
+ @if($issue->assignee)
+
+
+ {{ strtoupper(substr($issue->assignee->name, 0, 1)) }}
+
+ {{ $issue->assignee->name }}
+
+ @else
+ Sin asignar
+ @endif
+ |
+
+ {{-- Fecha --}}
+
+ {{ $issue->created_at->format('d/m/Y') }}
+ @if($issue->resolved_at)
+ Res. {{ $issue->resolved_at->format('d/m/Y') }}
+ @endif
+ |
+
+ {{-- Acciones --}}
+
+
+ {{-- Editar --}}
+
+
+ {{-- Resolver --}}
+ @if(in_array($issue->status, ['open', 'in_review']))
+
+ @endif
+
+ {{-- Cerrar --}}
+ @if($issue->status !== 'closed')
+
+ @endif
+
+ {{-- Eliminar --}}
+
+
+ |
+
{{ __("No results") }}. Crea una o importa.
+{{ __("No layers. Create or import one.") }}
@endif📁
-{{ __("No files yet") }}. Sube imágenes o documentos.
+{{ __("No files yet") }}
| Nombre | Progreso | Color | Acciones |
|---|---|---|---|
| {{ __('Name') }} | {{ __('Progress') }} | {{ __('Color') }} | {{ __('Actions') }} | - Actualizar - + {{ __('Update') }} + | @endforeach
| + {{ __('Fase') }} + | + + {{-- Month header row --}} +
+ @php
+ $projectStart = $project->start_date ?? now()->startOfMonth();
+ $projectEnd = $project->end_date_estimated ?? now()->addMonths(6);
+ $totalDays = max(1, $projectStart->diffInDays($projectEnd));
+
+ // Build month segments
+ $months = [];
+ $cursor = $projectStart->copy()->startOfMonth();
+ while ($cursor->lte($projectEnd)) {
+ $mStart = $cursor->copy()->max($projectStart);
+ $mEnd = $cursor->copy()->endOfMonth()->min($projectEnd);
+ $days = max(1, $mStart->diffInDays($mEnd) + 1);
+ $widthPct = round(($days / $totalDays) * 100, 2);
+ $months[] = [
+ 'label' => $cursor->translatedFormat('M Y'),
+ 'width_pct' => $widthPct,
+ ];
+ $cursor->addMonthNoOverflow();
+ }
+ @endphp
+
+ @foreach($months as $month)
+
+
+ {{ $month['label'] }}
+
+ @endforeach
+ |
+
+ {{-- Dates column --}}
+ + {{ __('Fechas') }} + | + + {{-- Status column --}} ++ {{ __('Estado') }} + | +
|---|---|---|---|
|
+
+
+
+ {{ $phase['name'] }}
+
+
+ @if($phase['features_count'] > 0)
+
+ {{ $phase['features_count'] }} {{ __('elementos') }}
+
+ @endif
+ |
+
+ {{-- Gantt bar cell --}}
+
+
+
+ {{-- Month grid lines --}}
+ @php $offset = 0; @endphp
+ @foreach($months as $i => $month)
+ @if($i > 0)
+
+ @endif
+ @php $offset += $month['width_pct']; @endphp
+ @endforeach
+
+ {{-- Planned bar --}}
+
+
+
+
+ {{-- Actual bar (if exists) --}}
+ @if($phase['a_start_pct'] !== null && $phase['a_width_pct'] !== null)
+
+
+ @endif
+
+ {{-- Progress label --}}
+
+
+ {{ $phase['progress'] }}%
+
+
+
+ |
+
+ {{-- Dates column --}}
+
+
+
+
+
+ {{ $phase['planned_start'] }} – {{ $phase['planned_end'] }}
+
+ @if($phase['actual_start'])
+
+
+
+ {{ $phase['actual_start'] }} – {{ $phase['actual_end'] ?? __('En curso') }}
+
+
+ @endif
+ |
+
+ {{-- Status badge --}}
+
+ @if($phase['is_delayed'])
+
+ |
+
+
{{ Str::limit($project->description, 80) }}
+ @endif +Avance global
+{{ $stats['global_progress'] }}%
+Fases
+{{ $stats['total_phases'] }}
+ @if($stats['delayed_phases'] > 0) +{{ $stats['delayed_phases'] }} con retraso
+ @else +Sin retrasos
+ @endif +Elementos
+{{ $stats['total_features'] }}
++ {{ $stats['completed_features'] }} completados + · {{ $stats['verified_features'] }} verificados +
+Issues abiertos
++ {{ $stats['open_issues'] }} +
+ @if($stats['critical_issues'] > 0) +{{ $stats['critical_issues'] }} críticos
+ @else +0 críticos
+ @endif +Total inspecciones
+{{ $stats['total_inspections'] }}
+Aprobadas
+{{ $stats['passed_inspections'] }}
+Rechazadas
++ {{ $stats['failed_inspections'] }} +
+Sin fases aún.
+ @else +{{ $phase->name }}
+{{ $company->apodo ?: $company->name }}
+ @if($company->pivot->role_in_project) +{{ $company->pivot->role_in_project }}
+ @endif +{{ $member->name }}
+ @if($member->pivot->role_in_project) +{{ $member->pivot->role_in_project }}
+ @endif +Sin issues abiertos
+{{ $issue->title }}
+
+
Sin inspecciones
++ {{ $ins->template?->name ?? 'Inspección' }} +
+ {{ $iCfg[1] }} +
+
{{ $ins->created_at->diffForHumans() }}
+- {{ __('Click on the map to set the project location. The address and country will be filled automatically.') }} + {{ __('Click on the map or drag the marker to update the location') }}
@@ -47,7 +47,7 @@Fase: {{ $selectedFeature->layer?->phase?->name ?? '—' }}
-Capa: {{ $selectedFeature->layer?->name ?? '—' }}
+{{ __('Phase') }}: {{ $selectedFeature->layer?->phase?->name ?? '—' }}
+{{ __('Layer') }}: {{ $selectedFeature->layer?->name ?? '—' }}