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:
@@ -1,53 +1,101 @@
|
||||
<div>
|
||||
@if(session()->has('message'))
|
||||
<div class="alert alert-success mb-2 text-sm">{{ session('message') }}</div>
|
||||
@endif
|
||||
@if(session()->has('error'))
|
||||
<div class="alert alert-error mb-2 text-sm">{{ session('error') }}</div>
|
||||
|
||||
@if(session('notify'))
|
||||
<div class="alert alert-success mb-4">
|
||||
<x-heroicon-o-check-circle class="w-5 h-5" />
|
||||
{{ session('notify') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ── Cabecera ─────────────────────────────────────────────────────────── --}}
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-3 mb-4">
|
||||
<label class="input input-bordered input-sm flex items-center gap-2 flex-1 max-w-sm">
|
||||
<x-heroicon-o-magnifying-glass class="w-4 h-4 opacity-50" />
|
||||
<input type="text" wire:model.live.debounce.300ms="search"
|
||||
class="grow" placeholder="Buscar por nombre o email…" />
|
||||
</label>
|
||||
<a href="{{ route('admin.users.create') }}" class="btn btn-primary btn-sm gap-1 shrink-0" wire:navigate>
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
Nuevo usuario
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{-- ── Tabla ────────────────────────────────────────────────────────────── --}}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __('Name') }}</th>
|
||||
<th>{{ __('Email') }}</th>
|
||||
<th>{{ __('Role') }}</th>
|
||||
<th>{{ __('Language') }}</th>
|
||||
<th>{{ __('Actions') }}</th>
|
||||
<th>Usuario</th>
|
||||
<th>Rol</th>
|
||||
<th>Verificado</th>
|
||||
<th class="w-24"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($users as $user)
|
||||
<tr>
|
||||
<td class="font-medium">{{ $user->name }}</td>
|
||||
<td class="text-sm">{{ $user->email }}</td>
|
||||
@forelse($this->users as $u)
|
||||
<tr wire:key="user-{{ $u->id }}">
|
||||
<td>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($user->roles as $role)
|
||||
<span class="badge badge-sm {{ $role->name === 'Admin' ? 'badge-error' : 'badge-primary' }}">
|
||||
{{ __($role->name) }}
|
||||
</span>
|
||||
@endforeach
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-8">
|
||||
<span class="text-xs">{{ strtoupper(substr($u->name, 0, 1)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-sm">{{ $u->name }}</p>
|
||||
<p class="text-xs text-gray-500">{{ $u->email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-sm">{{ strtoupper($user->locale ?? 'en') }}</td>
|
||||
<td>
|
||||
@can('assign users')
|
||||
<select wire:change="updateRole({{ $user->id }}, $event.target.value)"
|
||||
class="select select-bordered select-xs"
|
||||
@if(Auth::id() === $user->id && $user->hasRole('Admin')) disabled @endif>
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->name }}" @selected($user->hasRole($role->name))>
|
||||
{{ __($role->name) }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@endcan
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($u->roles as $role)
|
||||
<span class="badge badge-sm {{ $role->name === 'Admin' ? 'badge-error' : 'badge-primary' }}">
|
||||
{{ $role->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
@if($u->roles->isEmpty())
|
||||
<span class="badge badge-sm badge-ghost">Sin rol</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@if($u->email_verified_at)
|
||||
<x-heroicon-o-check-circle class="w-5 h-5 text-success" />
|
||||
@else
|
||||
<x-heroicon-o-clock class="w-5 h-5 text-warning" />
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end gap-1">
|
||||
<a href="{{ route('admin.users.show', $u) }}"
|
||||
class="btn btn-xs btn-outline" title="Ver" wire:navigate>
|
||||
<x-heroicon-o-eye class="w-3.5 h-3.5" />
|
||||
</a>
|
||||
<a href="{{ route('admin.users.edit', $u) }}"
|
||||
class="btn btn-xs btn-outline btn-info" title="Editar" wire:navigate>
|
||||
<x-heroicon-o-pencil class="w-3.5 h-3.5" />
|
||||
</a>
|
||||
@if($u->id !== auth()->id())
|
||||
<button wire:click="deleteUser({{ $u->id }})"
|
||||
wire:confirm="¿Eliminar a '{{ $u->name }}'? Se perderán todos sus datos."
|
||||
class="btn btn-xs btn-outline btn-error" title="Eliminar">
|
||||
<x-heroicon-o-trash class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="text-center text-gray-400 py-8">
|
||||
<x-heroicon-o-users class="w-10 h-10 mx-auto mb-1 opacity-25" />
|
||||
<p class="text-sm">No se encontraron usuarios</p>
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
@if(!$selectedProject)
|
||||
<!-- Project Selection -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Seleccione un proyecto para ver detalles</h2>
|
||||
|
||||
<h2 class="text-xl font-bold mb-4">{{ __('Select a project to view details') }}</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
@foreach($projects as $project)
|
||||
<div class="project-card cursor-pointer hover:shadow-lg transition-shadow"
|
||||
<div class="project-card cursor-pointer hover:shadow-lg transition-shadow"
|
||||
wire:click="selectProject({{ $project['id'] }})">
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-2">{{ $project['name'] }}</h3>
|
||||
<p class="text-sm text-gray-500 mb-2">
|
||||
{{ $project['description'] ?? 'Sin descripción disponible' }}
|
||||
{{ $project['description'] ?? __('No description available') }}
|
||||
</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-full">
|
||||
@@ -21,7 +21,7 @@
|
||||
@php
|
||||
$progress = collect($project['phases'])->avg('progress_percent') ?? 0;
|
||||
@endphp
|
||||
{{ number_format($progress, 1) }}% completado
|
||||
{{ number_format($progress, 1) }}% {{ __('completed') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,84 +34,75 @@
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-bold">{{ $projectDetails['name'] }}</h2>
|
||||
<button wire:click="selectedProject = null"
|
||||
<button wire:click="selectedProject = null"
|
||||
class="px-3 py-1 bg-gray-200 text-gray-700 text-sm rounded hover:bg-gray-300">
|
||||
← Volver a proyectos
|
||||
← {{ __('Back to projects') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-500 mb-2">Estado</h3>
|
||||
<h3 class="text-sm font-medium text-gray-500 mb-2">{{ __('Status') }}</h3>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
@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'] ?? ''))) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-500 mb-2">Fecha de inicio</h3>
|
||||
<h3 class="text-sm font-medium text-gray-500 mb-2">{{ __('Start date') }}</h3>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{ $projectDetails['start_date'] ?? 'No definida' }}
|
||||
{{ $projectDetails['start_date'] ?? __('Not defined') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 class="text-sm font-medium text-gray-500 mb-2">Fecha estimada</h3>
|
||||
<h3 class="text-sm font-medium text-gray-500 mb-2">{{ __('Estimated end date') }}</h3>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{ $projectDetails['end_date'] ?? 'No definida' }}
|
||||
{{ $projectDetails['end_date'] ?? __('Not defined') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg mb-6">
|
||||
<h3 class="text-sm font-medium text-gray-500 mb-2">Descripción</h3>
|
||||
<h3 class="text-sm font-medium text-gray-500 mb-2">{{ __('Description') }}</h3>
|
||||
<p class="text-gray-700">
|
||||
{{ $projectDetails['description'] ?? 'No hay descripción disponible' }}
|
||||
{{ $projectDetails['description'] ?? __('No description available') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Progress Overview -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Resumen de Progreso</h2>
|
||||
|
||||
<h2 class="text-xl font-bold mb-4">{{ __('Progress overview') }}</h2>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium">Progreso General</h3>
|
||||
<h3 class="text-lg font-medium">{{ __('General progress') }}</h3>
|
||||
<div class="text-2xl font-bold text-green-600">
|
||||
{{ number_format($projectDetails['progress'] ?? 0, 1) }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 mb-4">
|
||||
<div class="bg-green-600 h-2.5 rounded-full"
|
||||
<div class="bg-green-600 h-2.5 rounded-full"
|
||||
style="width: {{ min(max($projectDetails['progress'] ?? 0, 0), 100) }}%"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $projectDetails['progress'] ?? 0 }}% completado
|
||||
{{ $projectDetails['progress'] ?? 0 }}% {{ __('completed') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Phases Progress -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Progreso por Fase</h2>
|
||||
|
||||
<h2 class="text-xl font-bold mb-4">{{ __('Progress by phase') }}</h2>
|
||||
|
||||
@php
|
||||
$project = \App\Models\Project::find($selectedProject);
|
||||
$phases = $project->phases ?? collect();
|
||||
@endphp
|
||||
|
||||
|
||||
@if($phases->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($phases as $phase)
|
||||
@@ -119,29 +110,29 @@
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-lg font-medium">{{ $phase->name }}</h3>
|
||||
<span class="px-2 py-1 bg-indigo-100 text-indigo-800 text-xs rounded-full">
|
||||
Fase {{ $phase->id }}
|
||||
{{ __('Phase') }} {{ $phase->id }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div class="bg-indigo-600 h-2.5 rounded-full"
|
||||
<div class="bg-indigo-600 h-2.5 rounded-full"
|
||||
style="width: {{ min(max($phase->progress_percent ?? 0, 0), 100) }}%"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-sm text-gray-500 mt-1">
|
||||
{{ $phase->progress_percent ?? 0 }}% completado
|
||||
{{ $phase->progress_percent ?? 0 }}% {{ __('completed') }}
|
||||
</div>
|
||||
|
||||
|
||||
@if($phase->features->isNotEmpty())
|
||||
<div class="mt-3 pt-2 border-t border-gray-200">
|
||||
<h4 class="text-sm font-medium text-gray-600 mb-2">Características:</h4>
|
||||
<h4 class="text-sm font-medium text-gray-600 mb-2">{{ __('Features') }}:</h4>
|
||||
<div class="space-y-1 text-sm">
|
||||
@foreach($phase->features as $feature)
|
||||
<div class="flex items-start">
|
||||
<span class="flex-shrink-0">•</span>
|
||||
<span class="ml-2">
|
||||
{{ $feature->name }}:
|
||||
<span class="font-medium">{{ $feature->completion_status ?? 'Pendiente' }}</span>
|
||||
{{ $feature->name }}:
|
||||
<span class="font-medium">{{ $feature->completion_status ?? __('Pending') }}</span>
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
@@ -153,20 +144,20 @@
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-gray-50 p-6 text-center rounded-lg">
|
||||
<p class="text-gray-500">No hay fases definidas para este proyecto</p>
|
||||
<p class="text-gray-500">{{ __('No phases defined for this project') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Gallery -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Galería de Progreso</h2>
|
||||
|
||||
<h2 class="text-xl font-bold mb-4">{{ __('Progress gallery') }}</h2>
|
||||
|
||||
<div class="gallery-grid">
|
||||
@foreach($galleryImages as $image)
|
||||
<div class="gallery-item">
|
||||
<img src="{{ $image['url'] }}"
|
||||
alt="{{ $image['title'] }}"
|
||||
<img src="{{ $image['url'] }}"
|
||||
alt="{{ $image['title'] }}"
|
||||
class="w-full h-48 object-cover">
|
||||
<div class="p-3">
|
||||
<h4 class="text-sm font-medium">{{ $image['title'] }}</h4>
|
||||
@@ -176,18 +167,18 @@
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Change Orders -->
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Órdenes de Cambio</h2>
|
||||
|
||||
<h2 class="text-xl font-bold mb-4">{{ __('Change orders') }}</h2>
|
||||
|
||||
@if($changeOrders->isNotEmpty())
|
||||
<div class="space-y-4">
|
||||
@foreach($changeOrders as $order)
|
||||
<div class="change-order-card change-order-{{ strtolower($order['status']) }} p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-lg font-medium">{{ $order['title'] }}</h3>
|
||||
<span class="px-2 py-1 text-xs rounded-full
|
||||
<span class="px-2 py-1 text-xs rounded-full
|
||||
@if($order['status'] == 'pending') bg-yellow-100 text-yellow-800
|
||||
@elseif($order['status'] == 'approved') bg-green-100 text-green-800
|
||||
@elseif($order['status'] == 'rejected') bg-red-100 text-red-800
|
||||
@@ -195,28 +186,28 @@
|
||||
{{ ucfirst($order['status']) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<p class="text-gray-600 mb-2">{{ $order['description'] }}</p>
|
||||
|
||||
|
||||
<div class="flex items-center space-x-3 text-sm">
|
||||
<span class="mr-4">
|
||||
<span class="font-medium">Solicitado:</span> {{ $order['requested_at'] }}
|
||||
<span class="font-medium">{{ __('Requested') }}:</span> {{ $order['requested_at'] }}
|
||||
</span>
|
||||
<span class="mr-4">
|
||||
<span class="font-medium">Monto:</span> ${{ number_format($order['amount'], 2) }}
|
||||
<span class="font-medium">{{ __('Amount') }}:</span> ${{ number_format($order['amount'], 2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
@if($order['status'] == 'pending')
|
||||
<div class="mt-3 pt-2 border-t border-gray-200">
|
||||
<div class="flex items-center space-x-3">
|
||||
<button wire:click="approveChangeOrder({{ $order['id'] }})"
|
||||
class="flex-1 px-3 py-2 bg-green-600 text-white text-sm rounded hover:bg-green-700 disabled:opacity-50">
|
||||
Aprobar
|
||||
{{ __('Approve') }}
|
||||
</button>
|
||||
<button wire:click="rejectChangeOrder({{ $order['id'] }})"
|
||||
class="flex-1 px-3 py-2 bg-red-600 text-white text-sm rounded hover:bg-red-700 disabled:opacity-50">
|
||||
Rechazar
|
||||
{{ __('Reject') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,7 +217,7 @@
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-gray-50 p-6 text-center rounded-lg">
|
||||
<p class="text-gray-500">No hay órdenes de cambio pendientes</p>
|
||||
<p class="text-gray-500">{{ __('No pending change orders') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<div>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('companies.manage') }}" class="btn btn-ghost btn-sm px-2" wire:navigate>
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||||
</a>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ $company ? 'Editar empresa: ' . $company->name : 'Nueva empresa' }}
|
||||
</h2>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8">
|
||||
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
|
||||
|
||||
@if(session('notify'))
|
||||
<div class="alert alert-success mb-4">{{ session('notify') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-8">
|
||||
|
||||
<form wire:submit.prevent="save" class="space-y-0">
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-error text-sm mb-6">
|
||||
<x-heroicon-o-exclamation-circle class="w-5 h-5 shrink-0" />
|
||||
<ul class="list-disc pl-3 space-y-0.5">
|
||||
@foreach($errors->all() as $e) <li>{{ $e }}</li> @endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ── Macro para fila de formulario horizontal ──────────── --}}
|
||||
{{-- Patrón: flex row, label w-48 shrink-0, campo flex-1 --}}
|
||||
|
||||
{{-- ── Sección: Identificación ──────────────────────────── --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Identificación
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Nombre registrado <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="name"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Constructora Ejemplo, S.L." />
|
||||
@error('name') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Apodo / comercial
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="apodo"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Ejemplo Constr." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
NIF / CIF / Tax ID
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="tax_id"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="B12345678" />
|
||||
@error('tax_id') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Tipo de empresa <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="type" class="select select-bordered w-full">
|
||||
<option value="owner">Promotor / Propietario</option>
|
||||
<option value="constructor">Constructor principal</option>
|
||||
<option value="subcontractor">Subcontratista</option>
|
||||
<option value="consultant">Consultor / Ingeniería</option>
|
||||
<option value="supplier">Proveedor</option>
|
||||
<option value="other">Otro</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Estado <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="estado" class="select select-bordered w-full max-w-xs">
|
||||
<option value="activo">Activo</option>
|
||||
<option value="inactivo">Inactivo</option>
|
||||
<option value="suspendido">Suspendido</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Sección: Contacto ─────────────────────────────────── --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Contacto
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Dirección
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<textarea wire:model="address" rows="2"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Calle, número, ciudad, CP, país"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Teléfono
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<x-heroicon-o-phone class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input type="tel" wire:model="phone" class="grow"
|
||||
placeholder="+34 600 123 456" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<x-heroicon-o-envelope class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input type="email" wire:model="email" class="grow"
|
||||
placeholder="contacto@empresa.com" />
|
||||
</label>
|
||||
@error('email') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Sitio web
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<x-heroicon-o-globe-alt class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input type="url" wire:model="website" class="grow"
|
||||
placeholder="https://www.empresa.com" />
|
||||
</label>
|
||||
@error('website') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Sección: Logo ─────────────────────────────────────── --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Logo
|
||||
</h3>
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
{{ $company?->logo_path ? 'Reemplazar logo' : 'Subir logo' }}
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">PNG / JPG, máx. 2 MB</p>
|
||||
</label>
|
||||
<div class="flex-1 flex items-start gap-4">
|
||||
{{-- Preview --}}
|
||||
@if($logo)
|
||||
<img src="{{ $logo->temporaryUrl() }}" alt="Preview"
|
||||
class="w-16 h-16 object-contain border border-base-300 rounded-lg shrink-0" />
|
||||
@elseif($company?->logo_path && \Illuminate\Support\Facades\Storage::disk('public')->exists($company->logo_path))
|
||||
<img src="{{ \Illuminate\Support\Facades\Storage::disk('public')->url($company->logo_path) }}"
|
||||
alt="Logo actual"
|
||||
class="w-16 h-16 object-contain border border-base-300 rounded-lg shrink-0" />
|
||||
@else
|
||||
<div class="w-16 h-16 bg-base-200 rounded-lg flex items-center justify-center shrink-0">
|
||||
<x-heroicon-o-building-office class="w-7 h-7 opacity-30" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex-1">
|
||||
<input type="file" wire:model="logo" accept="image/*"
|
||||
class="file-input file-input-bordered w-full" />
|
||||
<div wire:loading wire:target="logo" class="text-xs text-info mt-1">Subiendo…</div>
|
||||
@error('logo') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Sección: Notas ────────────────────────────────────── --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Notas internas
|
||||
</h3>
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Observaciones
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Información interna</p>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<textarea wire:model="notes" rows="3"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Condiciones especiales, observaciones…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Botones ───────────────────────────────────────────── --}}
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<a href="{{ route('companies.manage') }}" class="btn btn-outline gap-1" wire:navigate>
|
||||
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2"
|
||||
wire:loading.attr="disabled" wire:target="save">
|
||||
<span wire:loading wire:target="save" class="loading loading-spinner loading-sm"></span>
|
||||
<x-heroicon-o-check class="w-4 h-4" />
|
||||
{{ $company ? 'Guardar cambios' : 'Crear empresa' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,9 +4,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-500 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h10m-9-3h8m-7 0h7M8 13v2a2 2 0 002 2h5a2 2 0 002-2v-2m0 0V9a2 2 0 00-2-2H5a2 2 0 00-2 2v2Z" />
|
||||
</svg>
|
||||
Gestión de Empresas
|
||||
{{ __('Company Management') }}
|
||||
</h2>
|
||||
<p class="text-gray-600 mt-2">Gestione las empresas que participan en los proyectos</p>
|
||||
<p class="text-gray-600 mt-2">{{ __('Manage the companies that participate in projects') }}</p>
|
||||
</div>
|
||||
|
||||
@if(session('message'))
|
||||
@@ -22,7 +22,7 @@
|
||||
<div class="w-full md:w-1/2">
|
||||
<input type="text"
|
||||
wire:model.live="search"
|
||||
placeholder="Buscar empresas por nombre o NIF..."
|
||||
placeholder="{{ __('Search companies by name or tax ID...') }}"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors">
|
||||
</div>
|
||||
<div class="w-full md:w-1/3 mt-4 md:mt-0">
|
||||
@@ -31,7 +31,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Nueva Empresa
|
||||
{{ __('New Company') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -47,16 +47,16 @@
|
||||
class="bg-white rounded-lg shadow-md p-6">
|
||||
<div class="mb-4">
|
||||
<h3 class="text-xl font-semibold text-gray-800 flex items-center">
|
||||
{{ $editingCompanyId ? 'Editar Empresa' : 'Crear Nueva Empresa' }}
|
||||
{{ $editingCompanyId ? __('Edit Company') : __('New Company') }}
|
||||
</h3>
|
||||
<p class="text-gray-600 mt-1">
|
||||
Complete la información de la empresa. Los campos marcados con * son obligatorios.
|
||||
{{ __('Complete the company information. Fields marked with * are required.') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<strong>Errores de validación:</strong>
|
||||
<strong>{{ __('Validation errors') }}:</strong>
|
||||
<ul class="list-disc pl-5 mt-2 text-sm text-red-600">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@@ -71,17 +71,17 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Nombre *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Name') }} *</label>
|
||||
<input type="text"
|
||||
wire:model="name"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">NIF/NIE/CIF *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Tax ID') }} *</label>
|
||||
<input type="text"
|
||||
wire:model="tax_id"
|
||||
placeholder="Ej: B12345678"
|
||||
placeholder="{{ __('E.g.: B12345678') }}"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors">
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,20 +89,20 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Apodo</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Nickname') }}</label>
|
||||
<input type="text"
|
||||
wire:model="apodo"
|
||||
placeholder="Ej: Acme Construct"
|
||||
placeholder="{{ __('E.g.: Acme Construct') }}"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Estado *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Status') }} *</label>
|
||||
<select wire:model="estado"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors">
|
||||
<option value="">Seleccione un estado</option>
|
||||
<option value="activo">Activo</option>
|
||||
<option value="inactivo">Inactivo</option>
|
||||
<option value="suspendido">Suspendido</option>
|
||||
<option value="">{{ __('Select a status') }}</option>
|
||||
<option value="activo">{{ __('Active') }}</option>
|
||||
<option value="inactivo">{{ __('Inactive') }}</option>
|
||||
<option value="suspendido">{{ __('Suspended') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,30 +110,30 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Dirección</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Address') }}</label>
|
||||
<textarea wire:model="address"
|
||||
rows="3"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Tipo de Empresa *</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Company Type') }} *</label>
|
||||
<select wire:model="type"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors">
|
||||
<option value="">Seleccione un tipo</option>
|
||||
<option value="owner">Promotor/Propietario</option>
|
||||
<option value="constructor">Constructor Principal</option>
|
||||
<option value="subcontractor">Subcontratista</option>
|
||||
<option value="consultant">Consultor/Ingeniería</option>
|
||||
<option value="supplier">Proveedor</option>
|
||||
<option value="other">Otro</option>
|
||||
<option value="">{{ __('Select a type') }}</option>
|
||||
<option value="owner">{{ __('Owner') }}</option>
|
||||
<option value="constructor">{{ __('Constructor') }}</option>
|
||||
<option value="subcontractor">{{ __('Subcontractor') }}</option>
|
||||
<option value="consultant">{{ __('Consultant') }}</option>
|
||||
<option value="supplier">{{ __('Supplier') }}</option>
|
||||
<option value="other">{{ __('Other') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Teléfono</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Phone') }}</label>
|
||||
<input type="tel"
|
||||
wire:model="phone"
|
||||
placeholder="+34 600 123 456"
|
||||
@@ -141,31 +141,31 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Email') }}</label>
|
||||
<input type="email"
|
||||
wire:model="email"
|
||||
placeholder="contacto@empresa.com"
|
||||
placeholder="{{ __('contact@company.com') }}"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Sitio Web</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Website') }}</label>
|
||||
<input type="url"
|
||||
wire:model="website"
|
||||
placeholder="https://www.empresa.com"
|
||||
placeholder="https://www.company.com"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Logo de la Empresa</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Company Logo') }}</label>
|
||||
<div class="flex flex-col">
|
||||
<label class="cursor-pointer text-blue-600 hover:text-blue-800">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16v-2a2 2 0 012-2h2a2 2 0 012 2v2m-4 0h.01M12 12a2 2 0 100-4 2 2 0 000 4zm4.5-6.75a.75.75 0 100-1.5.75.75 0 000 1.5zm0 0h.01M7 10h.01M14 10h.01M10.363 5.636a.75.75 0 10-1.06-1.06l-.47 1.242A12.038 12.038 0 0112 9.042c1.373 0 2.702.28 3.901.784l1.242-.47a.75.75 0 10-1.06-1.06l-.469-1.241a9.038 9.038 0 00-2.342-.348z" />
|
||||
</svg>
|
||||
Seleccionar archivo...
|
||||
{{ __('Select file...') }}
|
||||
</label>
|
||||
<input type="file"
|
||||
wire:model="logo"
|
||||
@@ -173,13 +173,13 @@
|
||||
class="mt-2 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||
@if($logo)
|
||||
<div class="mt-3 flex items-center">
|
||||
<img src="{{ $logo->temporaryUrl() }}"
|
||||
alt="Vista previa del logo"
|
||||
<img src="{{ $logo->temporaryUrl() }}"
|
||||
alt="{{ __('Logo preview') }}"
|
||||
class="h-12 w-12 object-contain border border-gray-200 rounded-lg">
|
||||
<button type="button"
|
||||
wire:click="logo = null"
|
||||
class="ml-3 text-xs text-red-600 hover:text-red-800">
|
||||
Eliminar
|
||||
{{ __('Remove') }}
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
@@ -188,7 +188,7 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Notas Adicionales</label>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ __('Additional notes') }}</label>
|
||||
<textarea wire:model="notes"
|
||||
rows="4"
|
||||
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"></textarea>
|
||||
@@ -198,11 +198,11 @@
|
||||
<button type="button"
|
||||
wire:click="resetForm"
|
||||
class="px-5 py-3 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors">
|
||||
Cancelar
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-bold rounded-lg transition-colors">
|
||||
{{$editingCompanyId ? 'Actualizar Empresa' : 'Crear Empresa'}}
|
||||
{{ $editingCompanyId ? __('Update') : __('Create') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -216,7 +216,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2z" />
|
||||
</svg>
|
||||
Lista de Empresas ({{ $companies->count() }})
|
||||
{{ __('Company list') }} ({{ $companies->count() }})
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-gray-300 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2z" />
|
||||
</svg>
|
||||
<p class="mt-2">No hay empresas registradas. Cree su primera empresa usando el botón de arriba.</p>
|
||||
<p class="mt-2">{{ __('No companies registered. Create your first company using the button above.') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="divide-y divide-gray-200">
|
||||
@@ -235,7 +235,7 @@
|
||||
<div class="flex items-start space-x-3">
|
||||
@if($company->logo_path && Storage::disk('public')->exists($company->logo_path))
|
||||
<img src="{{ Storage::disk('public')->url($company->logo_path) }}"
|
||||
alt="Logo de {{ $company->name }}"
|
||||
alt="{{ __('Logo of') }} {{ $company->name }}"
|
||||
class="h-12 w-12 object-contain border border-gray-200 rounded-lg flex-shrink-0">
|
||||
@else
|
||||
<div class="h-12 w-12 flex items-center justify-center bg-gray-100 rounded-lg text-gray-400 flex-shrink-0">
|
||||
@@ -250,11 +250,11 @@
|
||||
@if($company->tax_id)
|
||||
{{ $company->tax_id }}
|
||||
@else
|
||||
Sin NIF/CIF
|
||||
{{ __('No tax ID') }}
|
||||
@endif
|
||||
</p>
|
||||
@if($company->type)
|
||||
<span class="inline-block mt-1 px-2 py-0.5 text-xs font-medium
|
||||
<span class="inline-block mt-1 px-2 py-0.5 text-xs font-medium
|
||||
@if($company->type === 'owner') bg-green-100 text-green-800
|
||||
@elseif($company->type === 'constructor') bg-blue-100 text-blue-800
|
||||
@elseif($company->type === 'subcontractor') bg-purple-100 text-purple-800
|
||||
@@ -304,15 +304,15 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
||||
</svg>
|
||||
Editar
|
||||
{{ __('Edit') }}
|
||||
</button>
|
||||
<button wire:click="deleteCompany({{ $company->id }})"
|
||||
class="text-sm text-red-600 hover:text-red-800 font-medium flex items-center"
|
||||
onclick="return confirm('¿Está seguro de que desea eliminar esta empresa? Esta acción no se puede deshacer.')">
|
||||
wire:confirm="{{ __('Delete company confirmation') }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10h-3a2 2 0 00-2 2v2a2 2 0 002 2h3zm-3-4h1a2 2 0 012 2v2a2 2 0 01-2 2h-1V9a2 2 0 012-2z" />
|
||||
</svg>
|
||||
Eliminar
|
||||
{{ __('Delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,4 +324,4 @@
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,592 @@
|
||||
<div>
|
||||
<x-slot name="header">
|
||||
|
||||
{{-- ── Header de la empresa ─────────────────────────────────────────────── --}}
|
||||
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||||
|
||||
{{-- Izquierda: logo + datos --}}
|
||||
<div class="flex items-start gap-4">
|
||||
|
||||
{{-- Logo --}}
|
||||
@if($company->logo_path && \Illuminate\Support\Facades\Storage::disk('public')->exists($company->logo_path))
|
||||
<img src="{{ \Illuminate\Support\Facades\Storage::disk('public')->url($company->logo_path) }}"
|
||||
class="w-16 h-16 rounded-xl object-contain border border-base-300 bg-white shadow shrink-0"
|
||||
alt="Logo {{ $company->name }}" />
|
||||
@else
|
||||
<div class="w-16 h-16 rounded-xl bg-base-200 flex items-center justify-center shadow shrink-0">
|
||||
<x-heroicon-o-building-office-2 class="w-8 h-8 opacity-30" />
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Datos --}}
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h2 class="font-bold text-xl leading-tight">{{ $company->name }}</h2>
|
||||
@if($company->apodo)
|
||||
<span class="text-gray-400 font-normal text-base">"{{ $company->apodo }}"</span>
|
||||
@endif
|
||||
{{-- Tipo --}}
|
||||
@php
|
||||
$typeBadge = match($company->type) {
|
||||
'owner' => ['badge-success', 'Promotor'],
|
||||
'constructor' => ['badge-primary', 'Constructor'],
|
||||
'subcontractor' => ['badge-secondary','Subcontratista'],
|
||||
'consultant' => ['badge-info', 'Consultor'],
|
||||
'supplier' => ['badge-warning', 'Proveedor'],
|
||||
default => ['badge-ghost', 'Otro'],
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $typeBadge[0] }}">{{ $typeBadge[1] }}</span>
|
||||
</div>
|
||||
|
||||
{{-- NIF --}}
|
||||
@if($company->tax_id)
|
||||
<p class="text-xs text-gray-400 mt-0.5">NIF/CIF: {{ $company->tax_id }}</p>
|
||||
@endif
|
||||
|
||||
{{-- Contacto inline --}}
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-0.5 mt-1.5 text-sm text-gray-500">
|
||||
@if($company->email)
|
||||
<span class="flex items-center gap-1">
|
||||
<x-heroicon-o-envelope class="w-3.5 h-3.5 opacity-60 shrink-0" />
|
||||
{{ $company->email }}
|
||||
</span>
|
||||
@endif
|
||||
@if($company->phone)
|
||||
<span class="flex items-center gap-1">
|
||||
<x-heroicon-o-phone class="w-3.5 h-3.5 opacity-60 shrink-0" />
|
||||
{{ $company->phone }}
|
||||
</span>
|
||||
@endif
|
||||
@if($company->address)
|
||||
<span class="flex items-center gap-1 max-w-xs">
|
||||
<x-heroicon-o-map-pin class="w-3.5 h-3.5 opacity-60 shrink-0" />
|
||||
<span class="truncate">{{ $company->address }}</span>
|
||||
</span>
|
||||
@endif
|
||||
@if($company->website)
|
||||
<a href="{{ $company->website }}" target="_blank" rel="noopener"
|
||||
class="flex items-center gap-1 text-primary hover:underline">
|
||||
<x-heroicon-o-globe-alt class="w-3.5 h-3.5 shrink-0" />
|
||||
{{ parse_url($company->website, PHP_URL_HOST) ?? $company->website }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Derecha: estado + botones --}}
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
{{-- Estado --}}
|
||||
@php
|
||||
$estadoBadge = match($company->estado ?? 'activo') {
|
||||
'activo' => ['badge-success', 'Activo'],
|
||||
'inactivo' => ['badge-ghost', 'Inactivo'],
|
||||
'suspendido' => ['badge-error', 'Suspendido'],
|
||||
default => ['badge-ghost', ucfirst($company->estado ?? '')],
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $estadoBadge[0] }} badge-md">{{ $estadoBadge[1] }}</span>
|
||||
|
||||
{{-- Botones --}}
|
||||
<div class="flex gap-2 mt-1">
|
||||
<a href="{{ route('companies.edit', $company) }}"
|
||||
class="btn btn-outline btn-sm gap-1" wire:navigate>
|
||||
<x-heroicon-o-pencil class="w-4 h-4" />
|
||||
Editar
|
||||
</a>
|
||||
<a href="{{ route('companies.manage') }}"
|
||||
class="btn btn-ghost btn-sm gap-1" wire:navigate>
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||||
Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</x-slot>
|
||||
|
||||
{{-- ── Contenido principal ──────────────────────────────────────────────── --}}
|
||||
<div class="py-6">
|
||||
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8 space-y-4">
|
||||
|
||||
{{-- Tabs --}}
|
||||
<div role="tablist" class="tabs tabs-bordered">
|
||||
<button role="tab" wire:click="setTab('summary')"
|
||||
class="tab gap-2 {{ $activeTab === 'summary' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-chart-bar class="w-4 h-4" />
|
||||
Resumen
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('people')"
|
||||
class="tab gap-2 {{ $activeTab === 'people' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-users class="w-4 h-4" />
|
||||
Personas
|
||||
<span class="badge badge-sm badge-outline">{{ $usersCount }}</span>
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('projects')"
|
||||
class="tab gap-2 {{ $activeTab === 'projects' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-folder-open class="w-4 h-4" />
|
||||
Proyectos
|
||||
<span class="badge badge-sm badge-outline">{{ $projectsCount }}</span>
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('notes')"
|
||||
class="tab gap-2 {{ $activeTab === 'notes' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-document-text class="w-4 h-4" />
|
||||
Notas
|
||||
@if($company->notes)
|
||||
<span class="badge badge-sm badge-primary">•</span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: RESUMEN
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'summary')
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- KPIs --}}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-users class="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ $usersCount }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Personas</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-folder-open class="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ $projectsCount }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Proyectos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-green-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-arrow-trending-up class="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold">{{ $avgProgress }}<span class="text-lg font-normal text-gray-400">%</span></p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Progreso medio</p>
|
||||
@if($projectsCount > 0)
|
||||
<progress class="progress progress-success w-full h-1 mt-1"
|
||||
value="{{ $avgProgress }}" max="100"></progress>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 items-center text-center">
|
||||
<div class="w-10 h-10 rounded-full bg-orange-100 flex items-center justify-center mb-1">
|
||||
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-orange-500" />
|
||||
</div>
|
||||
<p class="text-3xl font-bold {{ $openIssues > 0 ? 'text-warning' : '' }}">{{ $openIssues }}</p>
|
||||
<p class="text-xs text-gray-500 mt-0.5">Issues abiertos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- Proyectos con progreso --}}
|
||||
@if($company->projects->isNotEmpty())
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-sm mb-3 flex items-center gap-2">
|
||||
<x-heroicon-o-chart-bar-square class="w-4 h-4 text-primary" />
|
||||
Estado de proyectos
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($company->projects as $p)
|
||||
@php
|
||||
$avg = $p->phases->avg('progress_percent') ?? 0;
|
||||
$pStatusBadge = match($p->status) {
|
||||
'in_progress' => ['badge-primary', 'En progreso'],
|
||||
'completed' => ['badge-success', 'Completado'],
|
||||
'paused' => ['badge-warning', 'Pausado'],
|
||||
'planning' => ['badge-ghost', 'Planificación'],
|
||||
default => ['badge-ghost', ucfirst($p->status)],
|
||||
};
|
||||
@endphp
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-0.5">
|
||||
<a href="{{ route('projects.dashboard', $p) }}"
|
||||
class="font-medium text-sm hover:text-primary transition-colors truncate" wire:navigate>
|
||||
{{ $p->name }}
|
||||
</a>
|
||||
<span class="badge badge-xs {{ $pStatusBadge[0] }} shrink-0">{{ $pStatusBadge[1] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<progress class="progress progress-primary flex-1 h-1.5"
|
||||
value="{{ round($avg) }}" max="100"></progress>
|
||||
<span class="text-xs text-gray-400 shrink-0 w-8 text-right">{{ round($avg) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@if($p->pivot->role_in_project)
|
||||
<span class="badge badge-sm badge-outline shrink-0">{{ $p->pivot->role_in_project }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Ficha empresa --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-sm mb-3 flex items-center gap-2">
|
||||
<x-heroicon-o-identification class="w-4 h-4 text-primary" />
|
||||
Ficha
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-2 text-sm">
|
||||
@foreach([
|
||||
['NIF/CIF', $company->tax_id],
|
||||
['Tipo', $typeBadge[1]],
|
||||
['Estado', $estadoBadge[1]],
|
||||
['Teléfono', $company->phone],
|
||||
['Email', $company->email],
|
||||
['Dirección', $company->address],
|
||||
['Web', $company->website],
|
||||
] as [$label, $val])
|
||||
@if($val)
|
||||
<div class="flex gap-2 py-1.5 border-b border-base-200">
|
||||
<span class="text-gray-400 w-24 shrink-0">{{ $label }}</span>
|
||||
@if($label === 'Web')
|
||||
<a href="{{ $val }}" target="_blank" rel="noopener"
|
||||
class="text-primary hover:underline truncate">{{ $val }}</a>
|
||||
@else
|
||||
<span class="font-medium truncate">{{ $val }}</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: PERSONAS
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'people')
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Acciones --}}
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{{-- Crear nuevo usuario --}}
|
||||
<a href="{{ route('admin.users.create') }}"
|
||||
class="btn btn-primary btn-sm gap-1" wire:navigate>
|
||||
<x-heroicon-o-user-plus class="w-4 h-4" />
|
||||
Crear nuevo usuario
|
||||
</a>
|
||||
|
||||
{{-- Asignar existente --}}
|
||||
@if($assignableUsers->isNotEmpty())
|
||||
<div class="flex items-center gap-2"
|
||||
x-data="{ open: false }">
|
||||
<button @click="open = !open" class="btn btn-outline btn-sm gap-1">
|
||||
<x-heroicon-o-link class="w-4 h-4" />
|
||||
Asignar usuario existente
|
||||
</button>
|
||||
<div x-show="open" x-cloak class="flex items-center gap-2 flex-wrap">
|
||||
<select wire:model="assignUserId" class="select select-bordered select-sm min-w-[200px]">
|
||||
<option value="">— Seleccionar —</option>
|
||||
@foreach($assignableUsers as $u)
|
||||
<option value="{{ $u->id }}">
|
||||
{{ $u->name }}
|
||||
@if($u->company) (actual: {{ $u->company->apodo ?: $u->company->name }}) @endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button wire:click="assignUser" class="btn btn-primary btn-sm gap-1">
|
||||
<x-heroicon-o-check class="w-4 h-4" />
|
||||
Asignar
|
||||
</button>
|
||||
@error('assignUserId')
|
||||
<p class="text-error text-xs">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Lista personas --}}
|
||||
@if($company->users->isEmpty())
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center py-10 text-gray-400">
|
||||
<x-heroicon-o-users class="w-10 h-10 opacity-25 mb-2" />
|
||||
<p class="text-sm">Ninguna persona asociada a esta empresa.</p>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="card bg-base-100 shadow overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Persona</th>
|
||||
<th>Rol</th>
|
||||
<th>Estado</th>
|
||||
<th>Contacto</th>
|
||||
<th class="w-20"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($company->users as $u)
|
||||
@php
|
||||
$uStatusBadge = match($u->status ?? 'active') {
|
||||
'active' => ['badge-success', 'Activo'],
|
||||
'inactive' => ['badge-ghost', 'Inactivo'],
|
||||
'suspended' => ['badge-error', 'Suspendido'],
|
||||
default => ['badge-ghost', ucfirst($u->status ?? '')],
|
||||
};
|
||||
@endphp
|
||||
<tr wire:key="user-{{ $u->id }}">
|
||||
<td>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="avatar placeholder shrink-0">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-8">
|
||||
<span class="text-xs">
|
||||
{{ strtoupper(substr($u->first_name ?: $u->name, 0, 1)) }}{{ strtoupper(substr($u->last_name ?: '', 0, 1)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-sm">
|
||||
@if($u->title) <span class="text-gray-400 font-normal">{{ $u->title }}</span> @endif
|
||||
{{ $u->first_name && $u->last_name
|
||||
? $u->first_name . ' ' . $u->last_name
|
||||
: $u->name }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-400">{{ $u->email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
@foreach($u->roles as $role)
|
||||
<span class="badge badge-xs {{ $role->name === 'Admin' ? 'badge-error' : 'badge-primary' }}">
|
||||
{{ $role->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
@if($u->roles->isEmpty())
|
||||
<span class="badge badge-xs badge-ghost">Sin rol</span>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-sm {{ $uStatusBadge[0] }}">{{ $uStatusBadge[1] }}</span>
|
||||
</td>
|
||||
<td class="text-xs text-gray-500">
|
||||
@if($u->phone) <div>{{ $u->phone }}</div> @endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end gap-1">
|
||||
<a href="{{ route('admin.users.show', $u) }}"
|
||||
class="btn btn-xs btn-outline" title="Ver" wire:navigate>
|
||||
<x-heroicon-o-eye class="w-3.5 h-3.5" />
|
||||
</a>
|
||||
<button wire:click="removeUser({{ $u->id }})"
|
||||
wire:confirm="¿Desvincular a {{ $u->name }} de esta empresa?"
|
||||
class="btn btn-xs btn-outline btn-warning" title="Desvincular">
|
||||
<x-heroicon-o-link-slash class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: PROYECTOS
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'projects')
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Formulario asignar --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-sm mb-3 flex items-center gap-2">
|
||||
<x-heroicon-o-plus-circle class="w-4 h-4 text-primary" />
|
||||
Vincular a proyecto
|
||||
</h3>
|
||||
@if($availableProjects->isEmpty())
|
||||
<p class="text-sm text-gray-400">La empresa ya está vinculada a todos los proyectos.</p>
|
||||
@else
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<div class="form-control flex-1 min-w-[200px]">
|
||||
<label class="label-text text-xs mb-1">Proyecto</label>
|
||||
<select wire:model="addProjectId" class="select select-bordered select-sm w-full">
|
||||
<option value="">— Seleccionar —</option>
|
||||
@foreach($availableProjects as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('addProjectId') <p class="text-error text-xs mt-0.5">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div class="form-control flex-1 min-w-[180px]">
|
||||
<label class="label-text text-xs mb-1">
|
||||
Rol en el proyecto <span class="text-error">*</span>
|
||||
</label>
|
||||
<input type="text" wire:model="addProjectRole"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="ej: Constructor principal" />
|
||||
@error('addProjectRole') <p class="text-error text-xs mt-0.5">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<button wire:click="assignProject" class="btn btn-primary btn-sm gap-1 shrink-0">
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
Vincular
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Lista proyectos --}}
|
||||
@if($company->projects->isEmpty())
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center py-10 text-gray-400">
|
||||
<x-heroicon-o-folder-open class="w-10 h-10 opacity-25 mb-2" />
|
||||
<p class="text-sm">Sin proyectos vinculados.</p>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="card bg-base-100 shadow overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Proyecto</th>
|
||||
<th>Rol de la empresa</th>
|
||||
<th>Estado</th>
|
||||
<th>Progreso</th>
|
||||
<th class="w-16"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($company->projects as $project)
|
||||
@php
|
||||
$avg = $project->phases->avg('progress_percent') ?? 0;
|
||||
$psCfg = match($project->status) {
|
||||
'in_progress' => ['badge-primary', 'En progreso'],
|
||||
'completed' => ['badge-success', 'Completado'],
|
||||
'paused' => ['badge-warning', 'Pausado'],
|
||||
'planning' => ['badge-ghost', 'Planificación'],
|
||||
default => ['badge-ghost', ucfirst($project->status)],
|
||||
};
|
||||
@endphp
|
||||
<tr wire:key="proj-{{ $project->id }}">
|
||||
<td>
|
||||
<a href="{{ route('projects.dashboard', $project) }}"
|
||||
class="font-medium hover:text-primary transition-colors" wire:navigate>
|
||||
{{ $project->name }}
|
||||
</a>
|
||||
@if($project->address)
|
||||
<p class="text-xs text-gray-400 truncate max-w-[200px]">{{ $project->address }}</p>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-sm badge-outline">
|
||||
{{ $project->pivot->role_in_project }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-sm {{ $psCfg[0] }}">{{ $psCfg[1] }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<progress class="progress progress-primary w-20 h-1.5"
|
||||
value="{{ round($avg) }}" max="100"></progress>
|
||||
<span class="text-xs text-gray-500">{{ round($avg) }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button wire:click="removeProject({{ $project->id }})"
|
||||
wire:confirm="¿Desvincular a '{{ $company->name }}' del proyecto '{{ $project->name }}'?"
|
||||
class="btn btn-xs btn-outline btn-error" title="Desvincular">
|
||||
<x-heroicon-o-x-mark class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: NOTAS
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'notes')
|
||||
<div class="card bg-base-100 shadow max-w-2xl">
|
||||
<div class="card-body p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2">
|
||||
<x-heroicon-o-document-text class="w-5 h-5 text-primary" />
|
||||
Notas internas
|
||||
</h3>
|
||||
@if(!$editingNotes)
|
||||
<button wire:click="$set('editingNotes', true)"
|
||||
class="btn btn-sm btn-outline gap-1">
|
||||
<x-heroicon-o-pencil class="w-3.5 h-3.5" />
|
||||
Editar
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($editingNotes)
|
||||
<textarea wire:model="notes" rows="10"
|
||||
class="textarea textarea-bordered w-full text-sm"
|
||||
placeholder="Añade notas, historial de relación, condiciones contractuales, observaciones…"
|
||||
autofocus></textarea>
|
||||
<div class="flex justify-end gap-2 mt-3">
|
||||
<button wire:click="$set('editingNotes', false)"
|
||||
class="btn btn-outline btn-sm">Cancelar</button>
|
||||
<button wire:click="saveNotes"
|
||||
class="btn btn-primary btn-sm gap-1">
|
||||
<x-heroicon-o-check class="w-4 h-4" />
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
@if($company->notes)
|
||||
<div class="prose prose-sm max-w-none text-gray-700 whitespace-pre-line bg-base-200 rounded-lg p-4 min-h-[120px]">
|
||||
{{ $company->notes }}
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-12 text-gray-400">
|
||||
<x-heroicon-o-document-text class="w-10 h-10 mx-auto opacity-25 mb-2" />
|
||||
<p class="text-sm">Sin notas.</p>
|
||||
<button wire:click="$set('editingNotes', true)"
|
||||
class="btn btn-sm btn-outline mt-3 gap-1">
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
Añadir nota
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,89 @@
|
||||
<div>
|
||||
{{-- Issue form --}}
|
||||
@if($editing)
|
||||
<div class="card bg-base-100 shadow mb-4">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-base">
|
||||
{{ $editingId ? 'Editar Issue' : 'Nuevo Issue' }}
|
||||
</h3>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label"><span class="label-text">Título *</span></label>
|
||||
<input type="text" wire:model="title" class="input input-bordered" placeholder="Título del issue" />
|
||||
@error('title') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label"><span class="label-text">Descripción</span></label>
|
||||
<textarea wire:model="description" class="textarea textarea-bordered" rows="3" placeholder="Descripción..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Estado</span></label>
|
||||
<select wire:model="status" class="select select-bordered">
|
||||
<option value="open">Abierto</option>
|
||||
<option value="in_review">En revisión</option>
|
||||
<option value="resolved">Resuelto</option>
|
||||
<option value="closed">Cerrado</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Prioridad</span></label>
|
||||
<select wire:model="priority" class="select select-bordered">
|
||||
<option value="low">Baja</option>
|
||||
<option value="medium">Media</option>
|
||||
<option value="high">Alta</option>
|
||||
<option value="critical">Crítica</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 justify-end mt-2">
|
||||
<button wire:click="cancel" class="btn btn-ghost btn-sm">Cancelar</button>
|
||||
<button wire:click="save" class="btn btn-primary btn-sm">Guardar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex justify-end mb-3">
|
||||
<button wire:click="create" class="btn btn-primary btn-sm">
|
||||
+ Nuevo Issue
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Issue list --}}
|
||||
<div class="space-y-2">
|
||||
@forelse($issues as $issue)
|
||||
<div class="card bg-base-100 shadow-sm border border-base-200">
|
||||
<div class="card-body py-3 px-4">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex-1">
|
||||
<p class="font-medium text-sm">{{ $issue->title }}</p>
|
||||
@if($issue->description)
|
||||
<p class="text-xs text-base-content/60 mt-0.5 line-clamp-2">{{ $issue->description }}</p>
|
||||
@endif
|
||||
<div class="flex gap-2 mt-1">
|
||||
<span class="badge badge-xs" style="background-color: {{ $issue->priority_color }}; color: #fff;">
|
||||
{{ ucfirst($issue->priority) }}
|
||||
</span>
|
||||
<span class="badge badge-xs badge-outline">
|
||||
{{ ucfirst(str_replace('_', ' ', $issue->status)) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-1 shrink-0">
|
||||
<button wire:click="edit({{ $issue->id }})" class="btn btn-ghost btn-xs">Editar</button>
|
||||
<button wire:click="delete({{ $issue->id }})"
|
||||
wire:confirm="¿Eliminar este issue?"
|
||||
class="btn btn-ghost btn-xs text-error">Eliminar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-center text-sm text-base-content/50 py-6">No hay issues registrados</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,403 @@
|
||||
<div>
|
||||
{{-- ================================================================
|
||||
HEADER
|
||||
================================================================ --}}
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 mb-5">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold">Issues del proyecto</h2>
|
||||
<p class="text-sm text-base-content/60">Gestión de incidencias y problemas</p>
|
||||
</div>
|
||||
<button
|
||||
wire:click="openForm()"
|
||||
class="btn btn-primary btn-sm gap-2"
|
||||
>
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
Nuevo Issue
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- ================================================================
|
||||
STATS BAR
|
||||
================================================================ --}}
|
||||
@php
|
||||
$countOpen = $issues->where('status', 'open')->count();
|
||||
$countInReview = $issues->where('status', 'in_review')->count();
|
||||
$countResolved = $issues->where('status', 'resolved')->count();
|
||||
$countClosed = $issues->where('status', 'closed')->count();
|
||||
@endphp
|
||||
|
||||
<div class="flex flex-wrap gap-2 mb-5">
|
||||
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||||
<div class="stat-title text-xs">Abiertos</div>
|
||||
<div class="stat-value text-error text-2xl">{{ $countOpen }}</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||||
<div class="stat-title text-xs">En revisión</div>
|
||||
<div class="stat-value text-warning text-2xl">{{ $countInReview }}</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||||
<div class="stat-title text-xs">Resueltos</div>
|
||||
<div class="stat-value text-success text-2xl">{{ $countResolved }}</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||||
<div class="stat-title text-xs">Cerrados</div>
|
||||
<div class="stat-value text-base-content/50 text-2xl">{{ $countClosed }}</div>
|
||||
</div>
|
||||
<div class="stat bg-base-200 rounded-box py-2 px-4 flex-1 min-w-[120px]">
|
||||
<div class="stat-title text-xs">Total</div>
|
||||
<div class="stat-value text-2xl">{{ $issues->count() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ================================================================
|
||||
ISSUES TABLE
|
||||
================================================================ --}}
|
||||
@if($issues->isEmpty())
|
||||
<div class="flex flex-col items-center justify-center py-16 text-base-content/40">
|
||||
<x-heroicon-o-bug-ant class="w-16 h-16 mb-3" />
|
||||
<p class="text-lg font-semibold">Sin issues registrados</p>
|
||||
<p class="text-sm">Crea el primer issue con el botón "Nuevo Issue".</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-x-auto rounded-box border border-base-300">
|
||||
<table class="table table-sm w-full">
|
||||
<thead class="bg-base-200">
|
||||
<tr>
|
||||
<th class="w-28">Prioridad</th>
|
||||
<th>Título</th>
|
||||
<th class="hidden md:table-cell">Feature</th>
|
||||
<th class="w-28">Estado</th>
|
||||
<th class="hidden lg:table-cell w-36">Asignado a</th>
|
||||
<th class="hidden lg:table-cell w-28">Fecha</th>
|
||||
<th class="w-36 text-right">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($issues as $issue)
|
||||
<tr wire:key="issue-{{ $issue->id }}" class="hover">
|
||||
{{-- Prioridad --}}
|
||||
<td>
|
||||
@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
|
||||
<span
|
||||
class="badge badge-sm font-semibold
|
||||
{{ $issue->priority === 'critical' ? 'text-white' : '' }}"
|
||||
style="background-color: {{ $issue->priority_color }}; color: {{ $issue->priority === 'critical' || $issue->priority === 'high' ? '#fff' : '#1f2937' }}; border-color: transparent;"
|
||||
>
|
||||
{{ $pLabel }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{{-- Título + descripción breve --}}
|
||||
<td>
|
||||
<div class="font-medium text-sm leading-tight">{{ $issue->title }}</div>
|
||||
@if($issue->description)
|
||||
<div class="text-xs text-base-content/50 truncate max-w-xs">{{ Str::limit($issue->description, 60) }}</div>
|
||||
@endif
|
||||
@if($issue->reporter)
|
||||
<div class="text-xs text-base-content/40 mt-0.5">
|
||||
Reportado por {{ $issue->reporter->name }}
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Feature --}}
|
||||
<td class="hidden md:table-cell">
|
||||
@if($issue->feature)
|
||||
<span class="badge badge-outline badge-sm">{{ $issue->feature->name }}</span>
|
||||
@else
|
||||
<span class="text-base-content/30 text-xs">—</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Estado --}}
|
||||
<td>
|
||||
@php
|
||||
$sLabel = match($issue->status) {
|
||||
'open' => 'Abierto',
|
||||
'in_review' => 'En revisión',
|
||||
'resolved' => 'Resuelto',
|
||||
'closed' => 'Cerrado',
|
||||
default => ucfirst($issue->status),
|
||||
};
|
||||
@endphp
|
||||
<span
|
||||
class="badge badge-sm"
|
||||
style="background-color: {{ $issue->status_color }}; color: #fff; border-color: transparent;"
|
||||
>
|
||||
{{ $sLabel }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{{-- Asignado a --}}
|
||||
<td class="hidden lg:table-cell">
|
||||
@if($issue->assignee)
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span class="w-6 h-6 rounded-full bg-primary text-primary-content flex items-center justify-center text-xs font-bold flex-shrink-0">
|
||||
{{ strtoupper(substr($issue->assignee->name, 0, 1)) }}
|
||||
</span>
|
||||
<span class="text-sm truncate">{{ $issue->assignee->name }}</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-base-content/30 text-xs">Sin asignar</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Fecha --}}
|
||||
<td class="hidden lg:table-cell text-xs text-base-content/50">
|
||||
{{ $issue->created_at->format('d/m/Y') }}
|
||||
@if($issue->resolved_at)
|
||||
<div class="text-success">Res. {{ $issue->resolved_at->format('d/m/Y') }}</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Acciones --}}
|
||||
<td class="text-right">
|
||||
<div class="flex items-center justify-end gap-1 flex-wrap">
|
||||
{{-- Editar --}}
|
||||
<button
|
||||
wire:click="openForm({{ $issue->id }})"
|
||||
class="btn btn-xs btn-ghost tooltip"
|
||||
data-tip="Editar"
|
||||
>
|
||||
<x-heroicon-o-pencil class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
{{-- Resolver --}}
|
||||
@if(in_array($issue->status, ['open', 'in_review']))
|
||||
<button
|
||||
wire:click="resolve({{ $issue->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="resolve({{ $issue->id }})"
|
||||
class="btn btn-xs btn-success tooltip"
|
||||
data-tip="Marcar como resuelto"
|
||||
>
|
||||
<x-heroicon-o-check class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
{{-- Cerrar --}}
|
||||
@if($issue->status !== 'closed')
|
||||
<button
|
||||
wire:click="close({{ $issue->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="close({{ $issue->id }})"
|
||||
class="btn btn-xs btn-neutral tooltip"
|
||||
data-tip="Cerrar issue"
|
||||
>
|
||||
<x-heroicon-o-x-mark class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@endif
|
||||
|
||||
{{-- Eliminar --}}
|
||||
<button
|
||||
wire:click="delete({{ $issue->id }})"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="delete({{ $issue->id }})"
|
||||
wire:confirm="¿Eliminar este issue? Esta acción no se puede deshacer."
|
||||
class="btn btn-xs btn-error btn-outline tooltip"
|
||||
data-tip="Eliminar"
|
||||
>
|
||||
<x-heroicon-o-trash class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ================================================================
|
||||
MODAL FORM (create / edit)
|
||||
================================================================ --}}
|
||||
@if($showForm)
|
||||
{{-- Overlay --}}
|
||||
<div
|
||||
class="fixed inset-0 z-40 bg-black/50"
|
||||
wire:click="closeForm()"
|
||||
></div>
|
||||
|
||||
{{-- Modal panel --}}
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="bg-base-100 rounded-box shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
|
||||
{{-- Modal header --}}
|
||||
<div class="flex items-center justify-between p-5 border-b border-base-300">
|
||||
<h3 class="text-lg font-bold">
|
||||
{{ $editingIssue ? 'Editar Issue' : 'Nuevo Issue' }}
|
||||
</h3>
|
||||
<button wire:click="closeForm()" class="btn btn-sm btn-ghost btn-circle">
|
||||
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Modal body --}}
|
||||
<form wire:submit.prevent="save" class="p-5 space-y-4">
|
||||
|
||||
{{-- Título --}}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Título <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
wire:model="title"
|
||||
class="input input-bordered w-full @error('title') input-error @enderror"
|
||||
placeholder="Describe brevemente el problema..."
|
||||
autofocus
|
||||
/>
|
||||
@error('title')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Descripción --}}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Descripción</span>
|
||||
</label>
|
||||
<textarea
|
||||
wire:model="description"
|
||||
class="textarea textarea-bordered w-full h-24 resize-y @error('description') textarea-error @enderror"
|
||||
placeholder="Detalla el problema, contexto, pasos para reproducirlo..."
|
||||
></textarea>
|
||||
@error('description')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Prioridad + Estado --}}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Prioridad <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<select
|
||||
wire:model="priority"
|
||||
class="select select-bordered w-full @error('priority') select-error @enderror"
|
||||
>
|
||||
<option value="low">Baja</option>
|
||||
<option value="medium">Media</option>
|
||||
<option value="high">Alta</option>
|
||||
<option value="critical">Crítica</option>
|
||||
</select>
|
||||
@error('priority')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Estado <span class="text-error">*</span></span>
|
||||
</label>
|
||||
<select
|
||||
wire:model.live="status"
|
||||
class="select select-bordered w-full @error('status') select-error @enderror"
|
||||
>
|
||||
<option value="open">Abierto</option>
|
||||
<option value="in_review">En revisión</option>
|
||||
<option value="resolved">Resuelto</option>
|
||||
<option value="closed">Cerrado</option>
|
||||
</select>
|
||||
@error('status')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Asignado a --}}
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Asignado a</span>
|
||||
</label>
|
||||
<select
|
||||
wire:model="assignedTo"
|
||||
class="select select-bordered w-full @error('assignedTo') select-error @enderror"
|
||||
>
|
||||
<option value="">Sin asignar</option>
|
||||
@foreach($projectUsers as $user)
|
||||
<option value="{{ $user->id }}">{{ $user->name }} – {{ $user->email }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('assignedTo')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
{{-- Notas de resolución (visible when status = resolved or closed) --}}
|
||||
@if(in_array($status, ['resolved', 'closed']))
|
||||
<div class="form-control">
|
||||
<label class="label">
|
||||
<span class="label-text font-medium">Notas de resolución</span>
|
||||
<span class="label-text-alt text-base-content/50">Opcional</span>
|
||||
</label>
|
||||
<textarea
|
||||
wire:model="resolutionNotes"
|
||||
class="textarea textarea-bordered w-full h-20 resize-y @error('resolutionNotes') textarea-error @enderror"
|
||||
placeholder="Describe cómo se resolvió el problema..."
|
||||
></textarea>
|
||||
@error('resolutionNotes')
|
||||
<label class="label">
|
||||
<span class="label-text-alt text-error">{{ $message }}</span>
|
||||
</label>
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Modal footer --}}
|
||||
<div class="flex items-center justify-end gap-3 pt-2 border-t border-base-300">
|
||||
<button
|
||||
type="button"
|
||||
wire:click="closeForm()"
|
||||
class="btn btn-ghost"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
wire:loading.attr="disabled"
|
||||
wire:target="save"
|
||||
class="btn btn-primary gap-2"
|
||||
>
|
||||
<span wire:loading.remove wire:target="save">
|
||||
<x-heroicon-o-check class="w-4 h-4" />
|
||||
</span>
|
||||
<span wire:loading wire:target="save" class="loading loading-spinner loading-sm"></span>
|
||||
{{ $editingIssue ? 'Actualizar Issue' : 'Crear Issue' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="flex items-center gap-1"
|
||||
x-on:locale-changed.window="window.location.reload()">
|
||||
@foreach(['en' => '🇬🇧 EN', 'es' => '🇪🇸 ES'] as $code => $label)
|
||||
<button wire:click="switchLanguage('{{ $code }}')"
|
||||
class="btn btn-xs {{ $currentLocale === $code ? 'btn-primary' : 'btn-ghost' }}"
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label">{{ __("Color") }}</label>
|
||||
<input type="color" wire:model="layer{{ __("Color") }}" class="input input-bordered w-20" />
|
||||
<input type="color" wire:model="layerColor" class="input input-bordered w-20" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<input type="color" wire:model="layerColor" class="input input-bordered">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label">Archivo (GeoJSON, KML, Shapefile.zip, DWG)</label></br>
|
||||
<label class="label">{{ __('File') }} (GeoJSON, KML, Shapefile.zip, DWG)</label></br>
|
||||
<input type="file" wire:model="uploadFile" class="file-input file-input-bordered">
|
||||
@error('uploadFile') <span class="text-error">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
@@ -49,13 +49,13 @@
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button wire:click="selectLayer({{ $layer->id }})" class="btn btn-xs btn-info">✏️ Editar</button>
|
||||
<button wire:click="deleteLayer({{ $layer->id }})" class="btn btn-xs btn-error" onclick="return confirm('¿{{ __("Delete layer") }}?')">🗑️</button>
|
||||
<button wire:click="selectLayer({{ $layer->id }})" class="btn btn-xs btn-info">✏️ {{ __('Edit') }}</button>
|
||||
<button wire:click="deleteLayer({{ $layer->id }})" wire:confirm="{{ __('Delete layer') }}?" class="btn btn-xs btn-error">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@if($layers->isEmpty())
|
||||
<p class="text-center">{{ __("No results") }}. Crea una o importa.</p>
|
||||
<p class="text-center">{{ __("No layers. Create or import one.") }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,8 +69,8 @@
|
||||
<h2 class="card-title">{{ __("Edit") }}</h2>
|
||||
@if($selectedLayer)
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button id="saveDrawingBtn" class="btn btn-primary">💾 Guardar cambios</button>
|
||||
<button wire:click="cancelEditing" class="btn btn-outline">Cancelar edición</button>
|
||||
<button id="saveDrawingBtn" class="btn btn-primary">💾 {{ __('Save changes') }}</button>
|
||||
<button wire:click="cancelEditing" class="btn btn-outline">{{ __('Cancel') }}</button>
|
||||
</div>
|
||||
@endif
|
||||
<div id="permanentMap" style="flex: 1; min-height: 500px; width: 100%; background: #e2e8f0;" wire:ignore></div>
|
||||
@@ -158,10 +158,10 @@
|
||||
onEachFeature: (f, l) => {
|
||||
l.feature = f;
|
||||
const props = f.properties;
|
||||
const content = `<b>${props.name || 'Elemento'}</b><br>
|
||||
Progreso: ${props.progress || 0}%<br>
|
||||
Responsable: ${props.responsible || '-'}<br>
|
||||
<em>Editable</em>`;
|
||||
const content = `<b>${props.name || @js(__('Feature'))}</b><br>
|
||||
@js(__('Progress')): ${props.progress || 0}%<br>
|
||||
@js(__('Responsible')): ${props.responsible || '-'}<br>
|
||||
<em>@js(__('Editable'))</em>`;
|
||||
l.bindPopup(content);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,6 +43,12 @@ new class extends Component
|
||||
</x-nav-link>
|
||||
</div>
|
||||
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link :href="route('companies.manage')" :active="request()->routeIs('companies.manage')" wire:navigate>
|
||||
{{ __('Companies') }}
|
||||
</x-nav-link>
|
||||
</div>
|
||||
|
||||
@can('manage all')
|
||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||
<x-nav-link :href="route('admin.users')" :active="request()->routeIs('admin.users')" wire:navigate>
|
||||
@@ -57,6 +63,11 @@ new class extends Component
|
||||
@livewire('language-switcher')
|
||||
</div>
|
||||
|
||||
<!-- Notification Bell -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-2">
|
||||
@livewire('notification-bell')
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-2">
|
||||
<x-dropdown align="right" width="48">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label-text">{{ __("Description") }}</label>
|
||||
<input type="text" wire:model="uploadDescription" class="input input-bordered input-sm" placeholder="Opcional" />
|
||||
<input type="text" wire:model="uploadDescription" class="input input-bordered input-sm" placeholder="{{ __('Optional') }}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
<button wire:click.stop="deleteMedia({{ $media->id }})"
|
||||
class="absolute top-0 right-0 bg-error text-white text-xs w-4 h-4 rounded-full opacity-0 group-hover:opacity-100 transition-opacity leading-none"
|
||||
onclick="return confirm('¿Borrar {{ $media->name }}?')">×</button>
|
||||
wire:confirm="{{ __('Delete file confirmation') }}">×</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@
|
||||
<span class="text-xs text-gray-400">{{ $media->formatted_size }}</span>
|
||||
<button wire:click="deleteMedia({{ $media->id }})"
|
||||
class="btn btn-xs btn-ghost text-error"
|
||||
onclick="return confirm('¿Borrar {{ $media->name }}?')">×</button>
|
||||
wire:confirm="{{ __('Delete file confirmation') }}">×</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@
|
||||
@if($mediaItems->isEmpty())
|
||||
<div class="text-center text-gray-400 py-6 text-sm">
|
||||
<p class="text-2xl mb-2">📁</p>
|
||||
<p>{{ __("No files yet") }}. Sube imágenes o documentos.</p>
|
||||
<p>{{ __("No files yet") }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<div class="relative" wire:poll.30s="loadNotifications">
|
||||
<!-- Bell button -->
|
||||
<div class="dropdown dropdown-end">
|
||||
<label tabindex="0" class="btn btn-ghost btn-circle" role="button">
|
||||
<div class="indicator">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
|
||||
</svg>
|
||||
@if($unreadCount > 0)
|
||||
<span class="badge badge-xs badge-error indicator-item">
|
||||
{{ $unreadCount > 99 ? '99+' : $unreadCount }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div tabindex="0" class="dropdown-content z-[50] menu p-0 shadow-lg bg-base-100 rounded-box w-80 mt-1 border border-base-200">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-base-200">
|
||||
<span class="font-semibold text-base-content">Notificaciones</span>
|
||||
@if($unreadCount > 0)
|
||||
<button wire:click="markAllAsRead"
|
||||
class="text-xs text-primary hover:underline focus:outline-none">
|
||||
Marcar todas
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Notification list -->
|
||||
<ul class="max-h-80 overflow-y-auto divide-y divide-base-200">
|
||||
@forelse($notifications as $notification)
|
||||
@php
|
||||
$data = is_array($notification['data']) ? $notification['data'] : json_decode($notification['data'], true);
|
||||
$isUnread = is_null($notification['read_at']);
|
||||
$createdAt = \Carbon\Carbon::parse($notification['created_at']);
|
||||
@endphp
|
||||
<li class="flex items-start gap-3 px-4 py-3 {{ $isUnread ? 'bg-primary/5' : '' }} hover:bg-base-200 transition-colors">
|
||||
<!-- Dot indicator -->
|
||||
<div class="mt-1 shrink-0">
|
||||
@if($isUnread)
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-primary"></span>
|
||||
@else
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-base-300"></span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-base-content leading-snug">
|
||||
{{ $data['message'] ?? 'Notificación' }}
|
||||
</p>
|
||||
<p class="text-xs text-base-content/50 mt-1">
|
||||
{{ $createdAt->diffForHumans() }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Mark as read -->
|
||||
@if($isUnread)
|
||||
<button wire:click="markAsRead('{{ $notification['id'] }}')"
|
||||
class="shrink-0 text-base-content/40 hover:text-primary focus:outline-none"
|
||||
title="Marcar como leída">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
@endif
|
||||
</li>
|
||||
@empty
|
||||
<li class="px-4 py-8 text-center text-sm text-base-content/50">
|
||||
No hay notificaciones
|
||||
</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
|
||||
<!-- Footer -->
|
||||
@if(count($notifications) > 0 && $unreadCount > 0)
|
||||
<div class="border-t border-base-200 px-4 py-2 text-center">
|
||||
<button wire:click="markAllAsRead"
|
||||
class="btn btn-ghost btn-xs w-full text-primary">
|
||||
Marcar todas como leídas
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@
|
||||
@endif
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr><th>Nombre</th><th>Progreso</th><th>Color</th><th>Acciones</th></tr>
|
||||
<tr><th>{{ __('Name') }}</th><th>{{ __('Progress') }}</th><th>{{ __('Color') }}</th><th>{{ __('Actions') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($phases as $phase)
|
||||
@@ -18,12 +18,12 @@
|
||||
</td>
|
||||
<td><div class="w-6 h-6 rounded" style="background: {{ $phase->color }}"></div></td>
|
||||
<td>
|
||||
<a href="{{ route('phases.progress', $phase) }}" class="btn btn-xs btn-info">Actualizar</a>
|
||||
<button wire:click="deletePhase({{ $phase->id }})" class="btn btn-xs btn-error">Eliminar</button>
|
||||
<a href="{{ route('phases.progress', $phase) }}" class="btn btn-xs btn-info">{{ __('Update') }}</a>
|
||||
<button wire:click="deletePhase({{ $phase->id }})" class="btn btn-xs btn-error">{{ __('Delete') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<button wire:click="addPhase" class="btn btn-sm btn-secondary mt-2">+ Agregar Fase</button>
|
||||
<button wire:click="addPhase" class="btn btn-sm btn-secondary mt-2">+ {{ __('Add Phase') }}</button>
|
||||
</div>
|
||||
@@ -0,0 +1,270 @@
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-8">
|
||||
|
||||
<form wire:submit.prevent="save">
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-error text-sm mb-6">
|
||||
<x-heroicon-o-exclamation-circle class="w-5 h-5 shrink-0" />
|
||||
<ul class="list-disc pl-3 space-y-0.5">
|
||||
@foreach($errors->all() as $e) <li>{{ $e }}</li> @endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════════════
|
||||
1. IDENTIFICACIÓN
|
||||
══════════════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">Identificación</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Nombre <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="name"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Edificio Residencial Las Palmas"
|
||||
autofocus />
|
||||
@error('name') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Referencia
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Código interno o expediente</p>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="reference"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
placeholder="OBR-2026-001" />
|
||||
@error('reference') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($project)
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">Estado</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="status" class="select select-bordered w-full max-w-xs">
|
||||
<option value="planning">Planificación</option>
|
||||
<option value="in_progress">En progreso</option>
|
||||
<option value="paused">Pausado</option>
|
||||
<option value="completed">Completado</option>
|
||||
</select>
|
||||
@error('status') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════════════
|
||||
2. UBICACIÓN
|
||||
══════════════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">Ubicación</h3>
|
||||
|
||||
{{-- Search box --}}
|
||||
<div class="flex gap-2 mb-3">
|
||||
<label class="input input-bordered input-sm flex items-center gap-2 flex-1">
|
||||
<x-heroicon-o-magnifying-glass class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input type="text" id="map-search-input" class="grow"
|
||||
placeholder="Buscar dirección, ciudad, lugar…"
|
||||
autocomplete="off" />
|
||||
</label>
|
||||
<button type="button" id="map-search-btn"
|
||||
class="btn btn-outline btn-sm gap-1 shrink-0">
|
||||
<x-heroicon-o-magnifying-glass class="w-4 h-4" />
|
||||
Buscar
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Geocode status message --}}
|
||||
<p id="geocode-status" class="text-xs text-gray-400 mb-2 min-h-[1rem]"></p>
|
||||
|
||||
{{-- Map (wire:ignore prevents Livewire morphing from destroying Leaflet) --}}
|
||||
<div wire:ignore
|
||||
id="project-location-map"
|
||||
data-lat="{{ $lat }}"
|
||||
data-lng="{{ $lng }}"
|
||||
style="height: 380px; border-radius: 0.5rem; overflow: hidden; z-index: 1;"
|
||||
class="border border-base-300 shadow-sm mb-4">
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-400 mb-4 flex items-center gap-1">
|
||||
<x-heroicon-o-cursor-arrow-rays class="w-3.5 h-3.5 opacity-60" />
|
||||
Pulsa en el mapa o arrastra el marcador para actualizar la ubicación.
|
||||
</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Lat/Lng (read-only, filled by map) --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Coordenadas
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Auto al pulsar el mapa</p>
|
||||
</label>
|
||||
<div class="flex-1 flex items-center gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="label-text text-xs mb-0.5">Latitud</label>
|
||||
<input type="text" wire:model="lat" readonly
|
||||
id="input-lat"
|
||||
class="input input-bordered input-sm w-full bg-base-200 font-mono"
|
||||
placeholder="40.41680000" />
|
||||
@error('lat') <p class="text-error text-xs mt-0.5">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<span class="text-gray-300 mt-5">/</span>
|
||||
<div class="flex-1">
|
||||
<label class="label-text text-xs mb-0.5">Longitud</label>
|
||||
<input type="text" wire:model="lng" readonly
|
||||
id="input-lng"
|
||||
class="input input-bordered input-sm w-full bg-base-200 font-mono"
|
||||
placeholder="-3.70380000" />
|
||||
@error('lng') <p class="text-error text-xs mt-0.5">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Dirección --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Dirección <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<textarea wire:model="address" rows="2"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Calle Gran Vía 28, 28013 Madrid, España"></textarea>
|
||||
@error('address') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- País — custom dropdown with flag images (native <select> can't render emoji on Windows) --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">País</label>
|
||||
<div class="flex-1 max-w-xs">
|
||||
<div x-data="{ open: false, q: '' }"
|
||||
@click.outside="open = false; q = ''"
|
||||
class="relative">
|
||||
|
||||
{{-- Trigger button --}}
|
||||
<button type="button"
|
||||
@click="open = !open; if(open) $nextTick(() => $refs.qs?.focus())"
|
||||
class="btn btn-outline w-full justify-start gap-2 font-normal h-12">
|
||||
@if($country && isset($countryList[$country]))
|
||||
<img src="https://flagcdn.com/w20/{{ $country }}.png"
|
||||
class="w-6 h-4 object-cover rounded-sm shrink-0"
|
||||
onerror="this.style.display='none'" />
|
||||
<span>{{ $countryList[$country] }}</span>
|
||||
@else
|
||||
<span class="text-gray-400">— Sin especificar —</span>
|
||||
@endif
|
||||
<x-heroicon-o-chevron-up-down class="w-4 h-4 ml-auto opacity-40 shrink-0" />
|
||||
</button>
|
||||
|
||||
{{-- Dropdown panel --}}
|
||||
<div x-show="open"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 scale-95"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
class="absolute z-50 mt-1 w-full bg-base-100 border border-base-300 rounded-xl shadow-xl overflow-hidden"
|
||||
style="display:none">
|
||||
|
||||
{{-- Search --}}
|
||||
<div class="p-2 border-b border-base-200">
|
||||
<input x-ref="qs" x-model="q" type="text"
|
||||
placeholder="Buscar país…"
|
||||
class="input input-sm input-bordered w-full"
|
||||
@keydown.escape="open = false; q = ''" />
|
||||
</div>
|
||||
|
||||
{{-- Clear option --}}
|
||||
<button type="button"
|
||||
@click="$wire.set('country', ''); open = false; q = ''"
|
||||
class="flex items-center gap-2 w-full px-3 py-2 hover:bg-base-200 text-sm text-gray-400 border-b border-base-200">
|
||||
— Sin especificar —
|
||||
</button>
|
||||
|
||||
{{-- Country list --}}
|
||||
<ul class="overflow-y-auto max-h-52 py-1">
|
||||
@foreach($countryList as $code => $cName)
|
||||
<li>
|
||||
<button type="button"
|
||||
x-show="q === '' || '{{ strtolower(addslashes($cName)) }}'.includes(q.toLowerCase())"
|
||||
@click="$wire.set('country', '{{ $code }}'); open = false; q = ''"
|
||||
class="flex items-center gap-2.5 w-full px-3 py-1.5 hover:bg-base-200 text-sm text-left {{ $country === $code ? 'bg-primary/10 font-semibold text-primary' : '' }}">
|
||||
<img src="https://flagcdn.com/w20/{{ $code }}.png"
|
||||
class="w-6 h-4 object-cover rounded-sm shrink-0"
|
||||
loading="lazy"
|
||||
onerror="this.style.display='none'" />
|
||||
{{ $cName }}
|
||||
@if($country === $code)
|
||||
<x-heroicon-o-check class="w-3.5 h-3.5 ml-auto shrink-0" />
|
||||
@endif
|
||||
</button>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@error('country') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════════════
|
||||
3. PLANIFICACIÓN
|
||||
══════════════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">Planificación</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Fecha inicio <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="date" wire:model="startDate"
|
||||
class="input input-bordered w-full max-w-xs" />
|
||||
@error('startDate') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Fecha fin estimada
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Vacío = sin fecha límite</p>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="date" wire:model="endDateEstimated"
|
||||
class="input input-bordered w-full max-w-xs" />
|
||||
@error('endDateEstimated') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Botones ─────────────────────────────────────────────────────── --}}
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<a href="{{ route('projects.index') }}" class="btn btn-outline gap-1" wire:navigate>
|
||||
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2"
|
||||
wire:loading.attr="disabled" wire:target="save">
|
||||
<span wire:loading wire:target="save" class="loading loading-spinner loading-sm"></span>
|
||||
<x-heroicon-o-check class="w-4 h-4" />
|
||||
{{ $project ? 'Guardar cambios' : 'Crear proyecto' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,275 @@
|
||||
<div class="p-4 space-y-4">
|
||||
|
||||
{{-- Page header --}}
|
||||
<div class="flex items-center justify-between flex-wrap gap-2">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('projects.map', $project) }}"
|
||||
class="btn btn-sm btn-ghost gap-1">
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||||
{{ __('Back to Map') }}
|
||||
</a>
|
||||
<h1 class="text-xl font-bold">{{ __('Cronograma') }}: {{ $project->name }}</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('projects.report', $project) }}"
|
||||
target="_blank"
|
||||
class="btn btn-sm btn-outline gap-1">
|
||||
<x-heroicon-o-document-text class="w-4 h-4" />
|
||||
{{ __('Report PDF') }}
|
||||
</a>
|
||||
<span class="text-sm text-base-content/60">
|
||||
{{ $project->start_date?->format('d/m/Y') ?? __('N/A') }}
|
||||
—
|
||||
{{ $project->end_date_estimated?->format('d/m/Y') ?? __('N/A') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Legend --}}
|
||||
<div class="flex items-center gap-4 text-sm flex-wrap">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-5 h-3 rounded" style="background:#3b82f6"></span>
|
||||
{{ __('Planificado') }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-5 h-3 rounded" style="background:#22c55e"></span>
|
||||
{{ __('Real') }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block w-5 h-3 rounded border-2" style="background:#fee2e2;border-color:#ef4444"></span>
|
||||
{{ __('Retrasado') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- Editor de fechas por fase (siempre visible) --}}
|
||||
<div class="bg-base-100 rounded-box border border-base-300 p-4 mb-4">
|
||||
<h3 class="font-semibold text-sm mb-3">Fechas planificadas y reales por fase</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($phases as $phase)
|
||||
<div x-data="{
|
||||
ps: '{{ $phase->planned_start?->format('Y-m-d') ?? '' }}',
|
||||
pe: '{{ $phase->planned_end?->format('Y-m-d') ?? '' }}',
|
||||
as_: '{{ $phase->actual_start?->format('Y-m-d') ?? '' }}',
|
||||
ae: '{{ $phase->actual_end?->format('Y-m-d') ?? '' }}'
|
||||
}" class="grid grid-cols-2 md:grid-cols-5 gap-2 items-center text-sm border-b pb-3 last:border-0">
|
||||
<div class="font-medium truncate flex items-center gap-1">
|
||||
<span class="w-2 h-2 rounded-full inline-block flex-shrink-0" style="background:{{ $phase->color ?? '#3b82f6' }}"></span>
|
||||
{{ $phase->name }}
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label-text text-xs text-gray-500">Plan. inicio</label>
|
||||
<input type="date" x-model="ps" class="input input-xs input-bordered" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label-text text-xs text-gray-500">Plan. fin</label>
|
||||
<input type="date" x-model="pe" class="input input-xs input-bordered" />
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label-text text-xs text-gray-500">Real inicio</label>
|
||||
<input type="date" x-model="as_" class="input input-xs input-bordered" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="label-text text-xs text-gray-500">Real fin</label>
|
||||
<div class="flex gap-1">
|
||||
<input type="date" x-model="ae" class="input input-xs input-bordered flex-1" />
|
||||
<button @click="$wire.updatePhaseDates({{ $phase->id }}, ps, pe, as_, ae)"
|
||||
class="btn btn-xs btn-primary">
|
||||
✓
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(empty($ganttData))
|
||||
<div class="alert alert-info">
|
||||
<x-heroicon-o-information-circle class="w-5 h-5" />
|
||||
<span>Define fechas planificadas arriba para ver el diagrama.</span>
|
||||
</div>
|
||||
@else
|
||||
{{-- Gantt table --}}
|
||||
<div class="bg-base-100 rounded-box border border-base-300 overflow-x-auto">
|
||||
<table class="w-full text-sm" style="min-width:900px;">
|
||||
<thead>
|
||||
<tr class="border-b border-base-300">
|
||||
{{-- Phase name column --}}
|
||||
<th class="text-left px-3 py-2 font-semibold bg-base-200" style="width:200px;min-width:200px;">
|
||||
{{ __('Fase') }}
|
||||
</th>
|
||||
|
||||
{{-- Month header row --}}
|
||||
<th class="px-0 py-0 bg-base-200" style="min-width:400px;">
|
||||
@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
|
||||
<div class="flex w-full border-b border-base-300">
|
||||
@foreach($months as $month)
|
||||
<div class="text-center text-xs py-1 font-medium border-r border-base-300 last:border-r-0 truncate"
|
||||
style="width:{{ $month['width_pct'] }}%;flex-shrink:0;">
|
||||
{{ $month['label'] }}
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</th>
|
||||
|
||||
{{-- Dates column --}}
|
||||
<th class="text-left px-3 py-2 font-semibold bg-base-200 whitespace-nowrap" style="width:160px;min-width:160px;">
|
||||
{{ __('Fechas') }}
|
||||
</th>
|
||||
|
||||
{{-- Status column --}}
|
||||
<th class="text-center px-3 py-2 font-semibold bg-base-200" style="width:110px;min-width:110px;">
|
||||
{{ __('Estado') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($ganttData as $phase)
|
||||
<tr class="border-b border-base-300 hover:bg-base-50 transition-colors {{ $phase['is_delayed'] ? 'bg-red-50' : '' }}">
|
||||
|
||||
{{-- Phase name --}}
|
||||
<td class="px-3 py-3" style="width:200px;min-width:200px;vertical-align:middle;">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block w-3 h-3 rounded-full flex-shrink-0"
|
||||
style="background:{{ $phase['color'] }}"></span>
|
||||
<span class="font-medium truncate" title="{{ $phase['name'] }}">
|
||||
{{ $phase['name'] }}
|
||||
</span>
|
||||
</div>
|
||||
@if($phase['features_count'] > 0)
|
||||
<div class="ml-5 text-xs text-base-content/50 mt-0.5">
|
||||
{{ $phase['features_count'] }} {{ __('elementos') }}
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
{{-- Gantt bar cell --}}
|
||||
<td class="px-0 py-3" style="vertical-align:middle;">
|
||||
<div class="relative w-full" style="height:36px;">
|
||||
|
||||
{{-- Month grid lines --}}
|
||||
@php $offset = 0; @endphp
|
||||
@foreach($months as $i => $month)
|
||||
@if($i > 0)
|
||||
<div class="absolute top-0 bottom-0 border-l border-base-300/50"
|
||||
style="left:{{ $offset }}%;"></div>
|
||||
@endif
|
||||
@php $offset += $month['width_pct']; @endphp
|
||||
@endforeach
|
||||
|
||||
{{-- Planned bar --}}
|
||||
<div class="absolute rounded"
|
||||
style="
|
||||
top: 4px;
|
||||
height: 13px;
|
||||
left: {{ $phase['p_start_pct'] }}%;
|
||||
width: {{ max(0.5, $phase['p_width_pct']) }}%;
|
||||
background: {{ $phase['is_delayed'] ? '#fca5a5' : $phase['color'] }};
|
||||
border: {{ $phase['is_delayed'] ? '2px solid #ef4444' : 'none' }};
|
||||
opacity: 0.85;
|
||||
"
|
||||
title="{{ __('Planificado') }}: {{ $phase['planned_start'] }} - {{ $phase['planned_end'] }}">
|
||||
</div>
|
||||
|
||||
{{-- Actual bar (if exists) --}}
|
||||
@if($phase['a_start_pct'] !== null && $phase['a_width_pct'] !== null)
|
||||
<div class="absolute rounded"
|
||||
style="
|
||||
top: 19px;
|
||||
height: 13px;
|
||||
left: {{ $phase['a_start_pct'] }}%;
|
||||
width: {{ max(0.5, $phase['a_width_pct']) }}%;
|
||||
background: #22c55e;
|
||||
opacity: 0.85;
|
||||
"
|
||||
title="{{ __('Real') }}: {{ $phase['actual_start'] }} - {{ $phase['actual_end'] ?? __('En curso') }}">
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Progress label --}}
|
||||
<div class="absolute inset-0 flex items-center"
|
||||
style="left: {{ $phase['p_start_pct'] }}%; width: {{ max(0.5, $phase['p_width_pct']) }}%;">
|
||||
<span class="text-xs font-bold text-white drop-shadow px-1 truncate"
|
||||
style="font-size:10px; line-height:13px; position:absolute; top:4px; left:2px;">
|
||||
{{ $phase['progress'] }}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{{-- Dates column --}}
|
||||
<td class="px-3 py-3 text-xs" style="width:160px;min-width:160px;vertical-align:middle;">
|
||||
<div class="space-y-0.5">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="inline-block w-2 h-2 rounded-full flex-shrink-0" style="background:{{ $phase['color'] }}"></span>
|
||||
<span class="text-base-content/70">{{ $phase['planned_start'] }} – {{ $phase['planned_end'] }}</span>
|
||||
</div>
|
||||
@if($phase['actual_start'])
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="inline-block w-2 h-2 rounded-full flex-shrink-0" style="background:#22c55e"></span>
|
||||
<span class="text-base-content/70">
|
||||
{{ $phase['actual_start'] }} – {{ $phase['actual_end'] ?? __('En curso') }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{{-- Status badge --}}
|
||||
<td class="px-3 py-3 text-center" style="width:110px;min-width:110px;vertical-align:middle;">
|
||||
@if($phase['is_delayed'])
|
||||
<span class="badge badge-error badge-sm gap-1">
|
||||
<x-heroicon-o-exclamation-triangle class="w-3 h-3" />
|
||||
{{ __('En retraso') }}
|
||||
</span>
|
||||
@elseif($phase['progress'] >= 100)
|
||||
<span class="badge badge-success badge-sm gap-1">
|
||||
<x-heroicon-o-check-circle class="w-3 h-3" />
|
||||
{{ __('Completado') }}
|
||||
</span>
|
||||
@elseif($phase['progress'] > 0)
|
||||
<span class="badge badge-info badge-sm">
|
||||
{{ $phase['progress'] }}%
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-ghost badge-sm">
|
||||
{{ __('Pendiente') }}
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Summary footer --}}
|
||||
<div class="text-xs text-base-content/50 text-right">
|
||||
{{ count($ganttData) }} {{ __('fases') }}
|
||||
•
|
||||
{{ __('Actualizado') }}: {{ now()->format('d/m/Y H:i') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,396 @@
|
||||
<div>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('projects.list') }}" class="btn btn-ghost btn-sm px-2">
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||||
</a>
|
||||
<div>
|
||||
<h2 class="font-bold text-xl leading-tight">{{ $project->name }}</h2>
|
||||
@if($project->description)
|
||||
<p class="text-sm text-gray-500 leading-tight mt-0.5">{{ Str::limit($project->description, 80) }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@php
|
||||
$statusCfg = match($project->status) {
|
||||
'in_progress' => ['badge-primary', 'En progreso'],
|
||||
'completed' => ['badge-success', 'Completado'],
|
||||
'paused' => ['badge-warning', 'Pausado'],
|
||||
'planning' => ['badge-ghost', 'Planificación'],
|
||||
default => ['badge-ghost', ucfirst(str_replace('_',' ',$project->status))],
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $statusCfg[0] }} badge-sm">{{ $statusCfg[1] }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-outline btn-sm gap-1">
|
||||
<x-heroicon-o-map class="w-4 h-4" />
|
||||
Mapa
|
||||
</a>
|
||||
<a href="{{ route('projects.gantt', $project) }}" class="btn btn-outline btn-sm gap-1">
|
||||
<x-heroicon-o-calendar-days class="w-4 h-4" />
|
||||
Gantt
|
||||
</a>
|
||||
<a href="{{ route('projects.issues', $project) }}" class="btn btn-outline btn-sm gap-1">
|
||||
<x-heroicon-o-exclamation-triangle class="w-4 h-4" />
|
||||
Issues
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
{{-- ── KPIs fila 1 ────────────────────────────────────────────────────── --}}
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
|
||||
{{-- Avance global --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Avance global</p>
|
||||
<p class="mt-1 text-3xl font-bold text-green-600">{{ $stats['global_progress'] }}%</p>
|
||||
<div class="mt-2 w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div class="bg-green-500 h-1.5 rounded-full transition-all" style="width: {{ $stats['global_progress'] }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-3 bg-green-100 rounded-full ml-3 shrink-0">
|
||||
<x-heroicon-o-chart-bar class="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Fases --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Fases</p>
|
||||
<p class="mt-1 text-3xl font-bold text-blue-600">{{ $stats['total_phases'] }}</p>
|
||||
@if($stats['delayed_phases'] > 0)
|
||||
<p class="text-xs text-red-500 mt-0.5">{{ $stats['delayed_phases'] }} con retraso</p>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 mt-0.5">Sin retrasos</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-3 {{ $stats['delayed_phases'] > 0 ? 'bg-red-100' : 'bg-blue-100' }} rounded-full">
|
||||
<x-heroicon-o-rectangle-stack class="w-5 h-5 {{ $stats['delayed_phases'] > 0 ? 'text-red-500' : 'text-blue-600' }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Elementos --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Elementos</p>
|
||||
<p class="mt-1 text-3xl font-bold text-indigo-600">{{ $stats['total_features'] }}</p>
|
||||
<p class="text-xs text-gray-400 mt-0.5">
|
||||
{{ $stats['completed_features'] }} completados
|
||||
· {{ $stats['verified_features'] }} verificados
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-3 bg-indigo-100 rounded-full">
|
||||
<x-heroicon-o-map-pin class="w-5 h-5 text-indigo-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Issues --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Issues abiertos</p>
|
||||
<p class="mt-1 text-3xl font-bold {{ $stats['open_issues'] > 0 ? 'text-orange-600' : 'text-gray-400' }}">
|
||||
{{ $stats['open_issues'] }}
|
||||
</p>
|
||||
@if($stats['critical_issues'] > 0)
|
||||
<p class="text-xs text-red-600 font-semibold mt-0.5">{{ $stats['critical_issues'] }} críticos</p>
|
||||
@else
|
||||
<p class="text-xs text-gray-400 mt-0.5">0 críticos</p>
|
||||
@endif
|
||||
</div>
|
||||
<div class="p-3 {{ $stats['open_issues'] > 0 ? 'bg-orange-100' : 'bg-gray-100' }} rounded-full">
|
||||
<x-heroicon-o-exclamation-triangle class="w-5 h-5 {{ $stats['open_issues'] > 0 ? 'text-orange-600' : 'text-gray-400' }}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{{-- ── KPIs fila 2: Inspecciones ────────────────────────────────────────── --}}
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 flex-row items-center gap-4">
|
||||
<div class="p-3 bg-gray-100 rounded-full shrink-0">
|
||||
<x-heroicon-o-clipboard-document-list class="w-5 h-5 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Total inspecciones</p>
|
||||
<p class="text-2xl font-bold">{{ $stats['total_inspections'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 flex-row items-center gap-4">
|
||||
<div class="p-3 bg-green-100 rounded-full shrink-0">
|
||||
<x-heroicon-o-check-circle class="w-5 h-5 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Aprobadas</p>
|
||||
<p class="text-2xl font-bold text-green-600">{{ $stats['passed_inspections'] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-4 flex-row items-center gap-4">
|
||||
<div class="p-3 {{ $stats['failed_inspections'] > 0 ? 'bg-red-100' : 'bg-gray-100' }} rounded-full shrink-0">
|
||||
<x-heroicon-o-x-circle class="w-5 h-5 {{ $stats['failed_inspections'] > 0 ? 'text-red-600' : 'text-gray-400' }}" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wider">Rechazadas</p>
|
||||
<p class="text-2xl font-bold {{ $stats['failed_inspections'] > 0 ? 'text-red-600' : 'text-gray-400' }}">
|
||||
{{ $stats['failed_inspections'] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Main grid: fases + actividad reciente ───────────────────────────── --}}
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
{{-- LEFT 2/3: Fases con progreso --}}
|
||||
<div class="lg:col-span-2 space-y-4">
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2">
|
||||
<x-heroicon-o-rectangle-stack class="w-4 h-4" />
|
||||
Fases del proyecto
|
||||
</h3>
|
||||
<a href="{{ route('projects.gantt', $project) }}" class="btn btn-xs btn-outline gap-1">
|
||||
<x-heroicon-o-calendar-days class="w-3.5 h-3.5" />
|
||||
Gantt
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($phases->isEmpty())
|
||||
<p class="text-sm text-gray-400 text-center py-6">Sin fases aún.</p>
|
||||
@else
|
||||
<div class="space-y-3">
|
||||
@foreach($phases as $phase)
|
||||
@php
|
||||
$pct = round($phase->progress_percent ?? 0);
|
||||
$isDelayed = $phase->planned_end && $phase->planned_end < now() && $pct < 100;
|
||||
$barColor = $isDelayed ? 'bg-red-500' : ($pct >= 100 ? 'bg-green-500' : 'bg-blue-500');
|
||||
$featureCount = $phase->layers->sum('features_count');
|
||||
@endphp
|
||||
<div class="border border-base-200 rounded-lg p-3 hover:border-primary/40 transition-colors">
|
||||
<div class="flex items-start justify-between gap-2 mb-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-semibold text-sm truncate">{{ $phase->name }}</p>
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-gray-500 mt-0.5">
|
||||
<span>{{ $phase->layers_count }} capa(s)</span>
|
||||
<span>·</span>
|
||||
<span>{{ $featureCount }} elementos</span>
|
||||
@if($phase->planned_start && $phase->planned_end)
|
||||
<span>·</span>
|
||||
<span>{{ $phase->planned_start->format('d/m/y') }} – {{ $phase->planned_end->format('d/m/y') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
@if($isDelayed)
|
||||
<span class="badge badge-error badge-xs">Retraso</span>
|
||||
@elseif($pct >= 100)
|
||||
<span class="badge badge-success badge-xs">Completada</span>
|
||||
@endif
|
||||
<span class="text-sm font-bold {{ $isDelayed ? 'text-red-600' : ($pct >= 100 ? 'text-green-600' : 'text-blue-600') }}">
|
||||
{{ $pct }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div class="{{ $barColor }} h-2 rounded-full transition-all" style="width: {{ $pct }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Empresas participantes --}}
|
||||
@if($companies->isNotEmpty())
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2 mb-3">
|
||||
<x-heroicon-o-building-office-2 class="w-4 h-4" />
|
||||
Empresas participantes
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
@foreach($companies as $company)
|
||||
<div class="flex items-center gap-2 border border-base-200 rounded-lg px-3 py-2">
|
||||
@if($company->logo_path && \Illuminate\Support\Facades\Storage::disk('public')->exists($company->logo_path))
|
||||
<img src="{{ \Illuminate\Support\Facades\Storage::disk('public')->url($company->logo_path) }}"
|
||||
alt="" class="w-7 h-7 object-contain rounded" />
|
||||
@else
|
||||
<x-heroicon-o-building-office class="w-5 h-5 opacity-40" />
|
||||
@endif
|
||||
<div>
|
||||
<p class="text-xs font-semibold leading-tight">{{ $company->apodo ?: $company->name }}</p>
|
||||
@if($company->pivot->role_in_project)
|
||||
<p class="text-xs text-gray-400">{{ $company->pivot->role_in_project }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- RIGHT 1/3: Actividad reciente --}}
|
||||
<div class="space-y-5">
|
||||
|
||||
{{-- Equipo --}}
|
||||
@if($teamMembers->isNotEmpty())
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2 mb-3">
|
||||
<x-heroicon-o-users class="w-4 h-4" />
|
||||
Equipo ({{ $teamMembers->count() }})
|
||||
</h3>
|
||||
<div class="space-y-2">
|
||||
@foreach($teamMembers->take(8) as $member)
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="avatar placeholder shrink-0">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-7">
|
||||
<span class="text-xs">{{ strtoupper(substr($member->name, 0, 1)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium truncate">{{ $member->name }}</p>
|
||||
@if($member->pivot->role_in_project)
|
||||
<p class="text-xs text-gray-400 truncate">{{ $member->pivot->role_in_project }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@foreach($member->roles->take(1) as $role)
|
||||
<span class="badge badge-xs {{ $role->name === 'Admin' ? 'badge-error' : 'badge-ghost' }}">{{ $role->name }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Issues recientes --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2">
|
||||
<x-heroicon-o-exclamation-triangle class="w-4 h-4 text-orange-500" />
|
||||
Issues abiertos
|
||||
</h3>
|
||||
<a href="{{ route('projects.issues', $project) }}" class="btn btn-xs btn-outline">Ver todos</a>
|
||||
</div>
|
||||
@if($recentIssues->isEmpty())
|
||||
<div class="text-center py-4 text-gray-400">
|
||||
<x-heroicon-o-check-circle class="w-7 h-7 mx-auto mb-1 opacity-25" />
|
||||
<p class="text-xs">Sin issues abiertos</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
@foreach($recentIssues as $issue)
|
||||
@php
|
||||
$pCfg = match($issue->priority ?? 'medium') {
|
||||
'critical' => 'badge-error',
|
||||
'high' => 'badge-warning',
|
||||
'medium' => 'badge-info',
|
||||
default => 'badge-ghost',
|
||||
};
|
||||
@endphp
|
||||
<div class="p-2.5 rounded-lg bg-base-200">
|
||||
<div class="flex items-start gap-1.5">
|
||||
<span class="badge badge-xs {{ $pCfg }} shrink-0 mt-0.5">{{ ucfirst($issue->priority ?? 'medium') }}</span>
|
||||
<p class="text-xs font-medium truncate">{{ $issue->title }}</p>
|
||||
</div>
|
||||
@if($issue->feature)
|
||||
<p class="text-xs text-gray-400 mt-0.5 truncate">
|
||||
<x-heroicon-o-map-pin class="w-2.5 h-2.5 inline" /> {{ $issue->feature->name }}
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Inspecciones recientes --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2">
|
||||
<x-heroicon-o-clipboard-document-list class="w-4 h-4 text-yellow-500" />
|
||||
Inspecciones recientes
|
||||
</h3>
|
||||
<a href="{{ route('projects.map', $project) }}?tab=inspections" class="btn btn-xs btn-outline">Ver en mapa</a>
|
||||
</div>
|
||||
@if($recentInspections->isEmpty())
|
||||
<div class="text-center py-4 text-gray-400">
|
||||
<x-heroicon-o-clipboard-document-list class="w-7 h-7 mx-auto mb-1 opacity-25" />
|
||||
<p class="text-xs">Sin inspecciones</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
@foreach($recentInspections as $ins)
|
||||
@php
|
||||
$iCfg = match($ins->result ?? '') {
|
||||
'pass' => ['badge-success', 'OK'],
|
||||
'fail' => ['badge-error', 'Fallo'],
|
||||
default => ['badge-ghost', 'Pendiente'],
|
||||
};
|
||||
@endphp
|
||||
<div class="p-2.5 rounded-lg bg-base-200">
|
||||
<div class="flex items-start justify-between gap-1">
|
||||
<p class="text-xs font-medium truncate flex-1">
|
||||
{{ $ins->template?->name ?? 'Inspección' }}
|
||||
</p>
|
||||
<span class="badge badge-xs {{ $iCfg[0] }} shrink-0">{{ $iCfg[1] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-0.5">
|
||||
@if($ins->feature)
|
||||
<p class="text-xs text-gray-400 truncate">
|
||||
<x-heroicon-o-map-pin class="w-2.5 h-2.5 inline" /> {{ $ins->feature->name }}
|
||||
</p>
|
||||
@endif
|
||||
<p class="text-xs text-gray-400 shrink-0 ml-1">{{ $ins->created_at->diffForHumans() }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{-- end right --}}
|
||||
|
||||
</div>
|
||||
{{-- end main grid --}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>{{-- end root --}}
|
||||
@@ -10,18 +10,18 @@
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">{{ __('Address') }}</label>
|
||||
<input type="text" wire:model="address" class="input input-bordered w-full" placeholder="{{ __('Street address, city, etc.') }}">
|
||||
<input type="text" wire:model="address" class="input input-bordered w-full" placeholder="{{ __('Address') }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">{{ __('Country') }}</label>
|
||||
<input type="text" wire:model="country" class="input input-bordered w-full" placeholder="{{ __('Country (auto-filled)') }}" readonly>
|
||||
<label class="block text-sm font-medium mb-2">{{ __('Coordinates') }}</label>
|
||||
<input type="text" wire:model="country" class="input input-bordered w-full" placeholder="{{ __('No country') }}" readonly>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium mb-2">{{ __('Start Date') }}</label>
|
||||
<label class="block text-sm font-medium mb-2">{{ __('Start date') }}</label>
|
||||
<input type="date" wire:model="start_date" class="input input-bordered w-full" required>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium mb-2">{{ __('End Date (estimated)') }}</label>
|
||||
<label class="block text-sm font-medium mb-2">{{ __('Estimated end date') }}</label>
|
||||
<input type="date" wire:model="end_date_estimated" class="input input-bordered w-full">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
@@ -36,9 +36,9 @@
|
||||
</div>
|
||||
|
||||
<div class="border rounded-lg p-4">
|
||||
<h2 class="text-xl font-bold mb-4">{{ __('Project Location') }}</h2>
|
||||
<h2 class="text-xl font-bold mb-4">{{ __('Location') }}</h2>
|
||||
<p class="text-sm text-gray-500 mb-2">
|
||||
{{ __('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') }}
|
||||
</p>
|
||||
<div id="projectMap" style="height: 400px; width: 100%; background: #e2e8f0; border-radius: 0.5rem;"></div>
|
||||
<input type="hidden" wire:model="lat">
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<div class="flex justify-end space-x-4">
|
||||
<button type="button" wire:click="resetForm" class="btn btn-outline">
|
||||
{{ __('Reset') }}
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ $projectId ? __('Update') : __('Create') }}
|
||||
|
||||
@@ -30,7 +30,14 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-sm btn-outline">{{ __('Map') }}</a>
|
||||
<a href="{{ route('projects.dashboard', $project) }}" class="btn btn-sm btn-outline gap-1">
|
||||
<x-heroicon-o-squares-2x2 class="w-3.5 h-3.5" />
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="{{ route('projects.map', $project) }}" class="btn btn-sm btn-outline gap-1">
|
||||
<x-heroicon-o-map class="w-3.5 h-3.5" />
|
||||
{{ __('Map') }}
|
||||
</a>
|
||||
@can('edit projects')
|
||||
<a href="{{ route('projects.edit', $project) }}" class="btn btn-sm btn-warning">{{ __('Edit') }}</a>
|
||||
@endcan
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{{-- Feature seleccionado --}}
|
||||
@if($selectedFeature)
|
||||
<div class="border rounded-lg p-3 mb-3 bg-base-200">
|
||||
<h3 class="font-bold text-sm">{{ $selectedFeature->name ?? 'Elemento' }}</h3>
|
||||
<p class="text-xs text-gray-500">Fase: {{ $selectedFeature->layer?->phase?->name ?? '—' }}</p>
|
||||
<p class="text-xs text-gray-500">Capa: {{ $selectedFeature->layer?->name ?? '—' }}</p>
|
||||
<h3 class="font-bold text-sm">{{ $selectedFeature->name ?? __('Feature') }}</h3>
|
||||
<p class="text-xs text-gray-500">{{ __('Phase') }}: {{ $selectedFeature->layer?->phase?->name ?? '—' }}</p>
|
||||
<p class="text-xs text-gray-500">{{ __('Layer') }}: {{ $selectedFeature->layer?->name ?? '—' }}</p>
|
||||
</div>
|
||||
|
||||
{{-- {{ __("Progress") }} --}}
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label-text">{{ __("Responsible") }}</label>
|
||||
<input type="text" wire:model="editResponsible" class="input input-bordered input-sm" placeholder="Nombre" />
|
||||
<input type="text" wire:model="editResponsible" class="input input-bordered input-sm" placeholder="{{ __('Name of responsible') }}" />
|
||||
</div>
|
||||
|
||||
<button wire:click="saveFeatureProgress" class="btn btn-primary btn-sm w-full mb-3">
|
||||
@@ -41,9 +41,9 @@
|
||||
@if($templates->isNotEmpty())
|
||||
<div class="divider text-xs">{{ __("Inspection") }}</div>
|
||||
<div class="form-control mb-2">
|
||||
<label class="label-text">Plantilla</label>
|
||||
<label class="label-text">{{ __('Template') }}</label>
|
||||
<select wire:model.live="selectedTemplateId" wire:change="onTemplateChange" class="select select-bordered select-sm">
|
||||
<option value="">Seleccionar plantilla...</option>
|
||||
<option value="">{{ __('Select template...') }}</option>
|
||||
@foreach($templates as $t)
|
||||
<option value="{{ $t->id }}">{{ $t->name }}</option>
|
||||
@endforeach
|
||||
@@ -69,7 +69,7 @@
|
||||
@break
|
||||
@case('select')
|
||||
<select wire:model="inspectionFormData.{{ $field['name'] }}" class="select select-bordered select-sm w-full">
|
||||
<option value="">Seleccionar</option>
|
||||
<option value="">{{ __('Select') }}</option>
|
||||
@foreach(explode(',', $field['options'] ?? '') as $opt)
|
||||
<option value="{{ trim($opt) }}">{{ trim($opt) }}</option>
|
||||
@endforeach
|
||||
@@ -97,7 +97,7 @@
|
||||
<span class="font-medium">{{ $ins->template?->name ?? '{{ __("Inspection") }}' }}</span>
|
||||
<span class="text-gray-400">{{ $ins->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
@if($ins->user)<span class="text-gray-500">por {{ $ins->user->name }}</span>@endif
|
||||
@if($ins->user)<span class="text-gray-500">{{ __('by') }} {{ $ins->user->name }}</span>@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -117,6 +117,6 @@
|
||||
@else
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<p class="text-lg">👆</p>
|
||||
<p>Haz clic en un elemento del mapa para editarlo</p>
|
||||
<p>{{ __('Click on a map element or search above to edit it') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@@ -1,4 +1,4 @@
|
||||
<div x-data="{ formFullscreen: $wire.entangle('formFullscreen') }"
|
||||
<div x-data="{ formFullscreen: $wire.entangle('formFullscreen') }"
|
||||
class="flex flex-col lg:flex-row gap-0 h-screen p-1">
|
||||
<!-- Columna izquierda: Mapa -->
|
||||
<div x-show="!formFullscreen" x-cloak class="w-full lg:w-2/3 flex-1 relative">
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<!-- Panel lateral de capas -->
|
||||
<div class="absolute top-2 right-2 z-[1000] bg-base-100 rounded-box shadow-xl p-4 w-72 border border-base-300 text-sm max-h-[calc(100vh-4rem)] overflow-y-auto">
|
||||
<h3 class="font-semibold text-base mb-2">{{ __("Fases and layers") }}</h3>
|
||||
<h3 class="font-semibold text-base mb-2">{{ __('Phases and layers') }}</h3>
|
||||
<div class="space-y-3">
|
||||
@foreach($phases as $phase)
|
||||
<div class="border rounded-lg p-2 {{ in_array($phase->id, $activeLayers) ? 'bg-base-200' : '' }}">
|
||||
@@ -17,7 +17,7 @@
|
||||
class="toggle toggle-xs toggle-primary">
|
||||
<span style="color: {{ $phase->color }};" class="text-lg">⬤</span>
|
||||
<span class="flex-1 font-medium text-sm truncate">{{ $phase->name }}</span>
|
||||
<span class="badge badge-sm {{ $phase->progress_percent >= 100 ? 'badge-success' : 'badge-ghost' }}">{{ $phase->progress_percent }}%</span>
|
||||
<span class="badge badge-sm {{ $phase->progress_percent >= 100 ? 'badge-success' : 'badge-ghost' }}">{{ $phase->progress_percent }}</span>
|
||||
</div>
|
||||
|
||||
{{-- Capas de esta fase --}}
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="flex items-center gap-1 text-xs text-gray-600">
|
||||
<span class="w-2 h-2 rounded-full inline-block" style="background: {{ $layer->color ?? '#ccc' }}"></span>
|
||||
<span class="flex-1 truncate">{{ $layer->name }}</span>
|
||||
<span class="badge badge-xs">{{ $layer->features_count ?? $layer->features->count() }} elem.</span>
|
||||
<span class="badge badge-xs">{{ $layer->features_count ?? $layer->features->count() }} {{ __('elem.') }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -36,10 +36,10 @@
|
||||
{{-- Botón para ir a gestión de capas de esta fase --}}
|
||||
<div class="mt-1 ml-7">
|
||||
<a href="{{ route('layers.manage', [$project, $phase]) }}" class="btn btn-xs btn-outline btn-primary">
|
||||
✏️ {{ __("Manage Layers") }}
|
||||
✏️ {{ __('Manage Layers') }}
|
||||
</a>
|
||||
<a href="{{ route('phases.progress', $phase) }}" class="btn btn-xs btn-outline">
|
||||
📊 {{ __("Progress") }}
|
||||
📊 {{ __('Progress') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -50,7 +50,7 @@
|
||||
<div class="mt-3">
|
||||
<label class="flex items-center gap-2 text-xs cursor-pointer">
|
||||
<input type="checkbox" wire:change="toggleFeatureImages" @if($showFeatureImages) checked @endif class="checkbox checkbox-xs checkbox-primary" />
|
||||
🖼️ {{ __("Show images on map") }}
|
||||
🖼️ {{ __('Show images on map') }}
|
||||
@if($featureImageMarkers)
|
||||
<span class="badge badge-xs">{{ count($featureImageMarkers) }}</span>
|
||||
@endif
|
||||
@@ -60,34 +60,34 @@
|
||||
{{-- Botones generales --}}
|
||||
<div class="mt-2 space-y-1">
|
||||
<a href="{{ route('projects.media', $project) }}" class="btn btn-sm btn-outline w-full">
|
||||
📁 {{ __("Project files") }}
|
||||
📁 {{ __('Project files') }}
|
||||
</a>
|
||||
<button wire:click="$dispatch('centerMap')" class="btn btn-sm btn-outline w-full">
|
||||
📍 {{ __("Centered in project") }}
|
||||
📍 {{ __('Centered in project') }}
|
||||
</button>
|
||||
<button onclick="getUserLocation()" class="btn btn-sm btn-outline w-full">
|
||||
🧭 {{ __("My location") }}
|
||||
🧭 {{ __('My location') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna derecha: {{ __("Edit") }} de progreso / inspecciones -->
|
||||
<!-- Columna derecha: {{ __('Edit') }} de progreso / inspecciones -->
|
||||
<div class="w-full lg:w-1/3 transition-all duration-300" :class="{'lg:w-full': formFullscreen}">
|
||||
<div class="card bg-base-100 shadow-xl h-full flex flex-col">
|
||||
<div class="card-body overflow-y-auto flex-1">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h2 class="card-title">{{ __("Project Map") }}</h2>
|
||||
<button wire:click="toggleFullscreen" class="btn btn-circle btn-sm" title="Pantalla completa">
|
||||
<h2 class="card-title">{{ __('Map') }}</h2>
|
||||
<button wire:click="toggleFullscreen" class="btn btn-circle btn-sm" title="{{ __('Fullscreen') }}">
|
||||
<span x-text="formFullscreen ? '✕' : '⤢'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="tabs box mb-4">
|
||||
<button wire:click="setActiveTab('edit')" class="tab {{ $activeTab === 'edit' ? 'tab-active' : '' }}">{{ __("Edit") }}</button>
|
||||
<button wire:click="setActiveTab('features')" class="tab {{ $activeTab === 'features' ? 'tab-active' : '' }}">{{ __("Features") }}</button>
|
||||
<button wire:click="setActiveTab('inspections')" class="tab {{ $activeTab === 'inspections' ? 'tab-active' : '' }}">{{ __("Inspections") }}</button>
|
||||
<button wire:click="setActiveTab('edit')" class="tab {{ $activeTab === 'edit' ? 'tab-active' : '' }}">{{ __('Edit') }}</button>
|
||||
<button wire:click="setActiveTab('features')" class="tab {{ $activeTab === 'features' ? 'tab-active' : '' }}">{{ __('Features') }}</button>
|
||||
<button wire:click="setActiveTab('inspections')" class="tab {{ $activeTab === 'inspections' ? 'tab-active' : '' }}">{{ __('Inspections') }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
@@ -96,14 +96,14 @@
|
||||
@if($selectedFeature)
|
||||
<!-- Feature seleccionado -->
|
||||
<div class="border rounded-lg p-3 mb-3 bg-base-200">
|
||||
<h3 class="font-bold text-sm">{{ $selectedFeature->name ?? 'Elemento' }}</h3>
|
||||
<p class="text-xs text-gray-500">Fase: {{ $selectedFeature->layer?->phase?->name ?? '—' }}</p>
|
||||
<p class="text-xs text-gray-500">Capa: {{ $selectedFeature->layer?->name ?? '—' }}</p>
|
||||
<h3 class="font-bold text-sm">{{ $selectedFeature->name ?? __('Feature') }}</h3>
|
||||
<p class="text-xs text-gray-500">{{ __('Phase') }}: {{ $selectedFeature->layer?->phase?->name ?? '—' }}</p>
|
||||
<p class="text-xs text-gray-500">{{ __('Layer') }}: {{ $selectedFeature->layer?->name ?? '—' }}</p>
|
||||
</div>
|
||||
|
||||
{{-- {{ __("Progress") }} --}}
|
||||
{{-- Progreso --}}
|
||||
<div class="form-control mb-3">
|
||||
<label class="label-text">{{ __("Progress") }}: {{ $editProgress }}%</label>
|
||||
<label class="label-text">{{ __('Progress') }}: {{ $editProgress }}%</label>
|
||||
<input type="range" min="0" max="100" wire:model.live="editProgress" class="range range-primary range-sm" />
|
||||
<div class="flex justify-between text-xs">
|
||||
<span>0%</span><span>50%</span><span>100%</span>
|
||||
@@ -111,18 +111,18 @@
|
||||
</div>
|
||||
|
||||
<div class="form-control mb-3">
|
||||
<label class="label-text">{{ __("Responsible") }}</label>
|
||||
<input type="text" wire:model="editResponsible" class="input input-bordered input-sm" placeholder="Nombre" />
|
||||
<label class="label-text">{{ __('Responsible') }}</label>
|
||||
<input type="text" wire:model="editResponsible" class="input input-bordered input-sm" placeholder="{{ __('Name of responsible') }}" />
|
||||
</div>
|
||||
|
||||
<button wire:click="saveFeatureProgress" class="btn btn-primary btn-sm w-full mb-3">
|
||||
💾 {{ __("Save progress") }}
|
||||
💾 {{ __('Save progress') }}
|
||||
</button>
|
||||
|
||||
{{-- Gestor de archivos del feature --}}
|
||||
<details class="mb-3 border rounded-lg">
|
||||
<summary class="text-xs font-semibold cursor-pointer p-2 bg-base-200 rounded-t-lg">
|
||||
📎 {{ __("Files of element") }}
|
||||
📎 {{ __('Files of element') }}
|
||||
</summary>
|
||||
<div class="p-2">
|
||||
@livewire('media-manager', [
|
||||
@@ -134,11 +134,11 @@
|
||||
|
||||
{{-- Templates / Inspecciones --}}
|
||||
@if($templates->isNotEmpty())
|
||||
<div class="divider text-xs">{{ __("Inspection") }}</div>
|
||||
<div class="divider text-xs">{{ __('Inspection') }}</div>
|
||||
<div class="form-control mb-2">
|
||||
<label class="label-text">Plantilla</label>
|
||||
<label class="label-text">{{ __('Template') }}</label>
|
||||
<select wire:model.live="selectedTemplateId" wire:change="onTemplateChange" class="select select-bordered select-sm">
|
||||
<option value="">Seleccionar plantilla...</option>
|
||||
<option value="">{{ __('Select template...') }}</option>
|
||||
@foreach($templates as $t)
|
||||
<option value="{{ $t->id }}">{{ $t->name }}</option>
|
||||
@endforeach
|
||||
@@ -164,7 +164,7 @@
|
||||
@break
|
||||
@case('select')
|
||||
<select wire:model="inspectionFormData.{{ $field['name'] }}" class="select select-bordered select-sm w-full">
|
||||
<option value="">Seleccionar</option>
|
||||
<option value="">{{ __('Select') }}</option>
|
||||
@foreach(explode(',', $field['options'] ?? '') as $opt)
|
||||
<option value="{{ trim($opt) }}">{{ trim($opt) }}</option>
|
||||
@endforeach
|
||||
@@ -178,21 +178,21 @@
|
||||
@endswitch
|
||||
</div>
|
||||
@endforeach
|
||||
<button wire:click="saveInspection" class="btn btn-primary btn-xs w-full mt-1">{{ __("Register inspection") }}</button>
|
||||
<button wire:click="saveInspection" class="btn btn-primary btn-xs w-full mt-1">{{ __('Register inspection') }}</button>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
{{-- {{ __("History") }} de inspecciones --}}
|
||||
{{-- Historial de inspecciones --}}
|
||||
@if($inspectionHistory->isNotEmpty())
|
||||
<div class="divider text-xs">{{ __("History") }}</div>
|
||||
<div class="divider text-xs">{{ __('History') }}</div>
|
||||
<div class="space-y-1 max-h-40 overflow-y-auto">
|
||||
@foreach($inspectionHistory as $ins)
|
||||
<div class="border rounded p-2 text-xs">
|
||||
<div class="border rounded p-2 text-xs" wire:key="hist-{{ $ins->id }}">
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">{{ $ins->template?->name ?? {{ __("Inspection") }} }}</span>
|
||||
<span class="font-medium">{{ $ins->template?->name ?? __('Inspection') }}</span>
|
||||
<span class="text-gray-400">{{ $ins->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
@if($ins->user)<span class="text-gray-500">por {{ $ins->user->name }}</span>@endif
|
||||
@if($ins->user)<span class="text-gray-500">{{ __('by') }} {{ $ins->user->name }}</span>@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@@ -203,16 +203,16 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<div>
|
||||
<h3 class="font-bold">{{ __("No templates yet") }}</h3>
|
||||
<div class="text-xs">{{ __("Create an inspection template") }}.</div>
|
||||
<h3 class="font-bold">{{ __('No templates yet') }}</h3>
|
||||
<div class="text-xs">{{ __('Create an inspection template') }}.</div>
|
||||
</div>
|
||||
<a href="{{ route('projects.templates', $project) }}" class="btn btn-primary btn-sm">{{ __("Create") }}</a>
|
||||
<a href="{{ route('projects.templates', $project) }}" class="btn btn-primary btn-sm">{{ __('Create') }}</a>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<p class="text-lg">👆</p>
|
||||
<p>Haz clic en un elemento del mapa para editarlo</p>
|
||||
<p>{{ __('Click on a map element or search above to edit it') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@elseif($activeTab === 'features')
|
||||
@@ -222,12 +222,12 @@
|
||||
<table class="table table-sm table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __("Feature") }}</th>
|
||||
<th>{{ __("Layer") }}</th>
|
||||
<th>{{ __("Phase") }}</th>
|
||||
<th>{{ __("Progress") }}</th>
|
||||
<th>{{ __("Responsible") }}</th>
|
||||
<th>{{ __("Template") }}</th>
|
||||
<th>{{ __('Feature') }}</th>
|
||||
<th>{{ __('Layer') }}</th>
|
||||
<th>{{ __('Phase') }}</th>
|
||||
<th>{{ __('Progress') }}</th>
|
||||
<th>{{ __('Responsible') }}</th>
|
||||
<th>{{ __('Template') }}</th>
|
||||
<th class="w-16"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -251,7 +251,7 @@
|
||||
@else
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<p class="text-lg">📋</p>
|
||||
<p>{{ __("No features found") }}</p>
|
||||
<p>{{ __('No elements in this project') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@elseif($activeTab === 'inspections')
|
||||
@@ -261,10 +261,10 @@
|
||||
<table class="table table-sm table-compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ __("Date") }}</th>
|
||||
<th>{{ __("Feature") }}</th>
|
||||
<th>{{ __("Template") }}</th>
|
||||
<th>{{ __("User") }}</th>
|
||||
<th>{{ __('Date') }}</th>
|
||||
<th>{{ __('Feature') }}</th>
|
||||
<th>{{ __('Template') }}</th>
|
||||
<th>{{ __('User') }}</th>
|
||||
<th class="w-16"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -286,174 +286,249 @@
|
||||
@else
|
||||
<div class="text-center text-gray-400 py-8">
|
||||
<p class="text-lg">📋</p>
|
||||
<p>{{ __("No inspections found") }}</p>
|
||||
<p>{{ __('No inspections registered') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@push('scripts')
|
||||
<style>
|
||||
.leaflet-container { z-index: 0 !important; }
|
||||
</style>
|
||||
<script>
|
||||
</div>
|
||||
let map;
|
||||
const layers = {};
|
||||
let imageMarkersLayer = null;
|
||||
let imageViewerModal = null;
|
||||
|
||||
function initMap() {
|
||||
if (map) return;
|
||||
const center = [{{ $project->lat }}, {{ $project->lng }}];
|
||||
map = L.map('map').setView(center, 16);
|
||||
@push('styles')
|
||||
<style>
|
||||
.leaflet-container { z-index: 0 !important; }
|
||||
</style>
|
||||
@endpush
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> & CartoDB'
|
||||
}).addTo(map);
|
||||
@push('scripts')
|
||||
<script>
|
||||
let map;
|
||||
const layers = {};
|
||||
let imageMarkersLayer = null;
|
||||
let imageViewerModal = null;
|
||||
let mapInitialized = false;
|
||||
let combinedBounds = null;
|
||||
|
||||
// Cargar fases y sus features
|
||||
@foreach($phases as $phase)
|
||||
@php
|
||||
$phaseFeatures = $phase->features()->with('layer.phase')->get();
|
||||
$fc = [
|
||||
'type' => 'FeatureCollection',
|
||||
'features' => $phaseFeatures->map(function($f) {
|
||||
return [
|
||||
'type' => 'Feature',
|
||||
'id' => $f->id,
|
||||
'geometry' => $f->geometry,
|
||||
'properties' => array_merge($f->properties ?? [], [
|
||||
'name' => $f->name,
|
||||
'progress' => $f->progress,
|
||||
'responsible' => $f->responsible,
|
||||
'template_id' => $f->template_id,
|
||||
'_feature_id' => $f->id,
|
||||
])
|
||||
];
|
||||
})->values()->toArray()
|
||||
];
|
||||
@endphp
|
||||
(function() {
|
||||
const data = @json($fc);
|
||||
if (data && data.features && data.features.length > 0) {
|
||||
const phaseLayer = L.geoJSON(data, {
|
||||
style: { color: '{{ $phase->color }}', weight: 3, opacity: 0.8, fillOpacity: 0.3 },
|
||||
onEachFeature: function(feature, layer) {
|
||||
const props = feature.properties || {};
|
||||
const featId = props._feature_id || feature.id;
|
||||
let content = `<b>${props.name || 'Elemento'}</b><br>
|
||||
{{ __("Progress") }}: ${props.progress || 0}%<br>
|
||||
{{ __("Responsible") }}: ${props.responsible || '-'}<br>
|
||||
<button class="btn btn-xs btn-primary mt-1" onclick="selectFeature('${featId}')">✏️ Editar</button>`;
|
||||
layer.bindPopup(content);
|
||||
layer.on('click', function() { selectFeature(' + featId + '); });
|
||||
// Utility function to escape HTML
|
||||
function escapeHtml(text) {
|
||||
if (text === null || text === undefined) return '';
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Utility function to validate URL
|
||||
function isValidUrl(string) {
|
||||
try {
|
||||
new URL(string);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initMap() {
|
||||
// Prevent multiple initializations
|
||||
if (mapInitialized || map) return;
|
||||
mapInitialized = true;
|
||||
|
||||
const center = [{{ $project->lat }}, {{ $project->lng }}];
|
||||
map = L.map('map').setView(center, 16);
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OSM</a> & CartoDB'
|
||||
}).addTo(map);
|
||||
|
||||
// Cargar fases y sus features
|
||||
@foreach($phases as $phase)
|
||||
@php
|
||||
$phaseFeatures = $phase->features()->with('layer.phase')->get();
|
||||
$fc = [
|
||||
'type' => 'FeatureCollection',
|
||||
'features' => $phaseFeatures->map(function($f) {
|
||||
return [
|
||||
'type' => 'Feature',
|
||||
'id' => $f->id,
|
||||
'geometry' => $f->geometry,
|
||||
'properties' => array_merge($f->properties ?? [], [
|
||||
'name' => $f->name,
|
||||
'progress' => $f->progress,
|
||||
'responsible' => $f->responsible,
|
||||
'template_id' => $f->template_id,
|
||||
'_feature_id' => $f->id,
|
||||
])
|
||||
];
|
||||
})->values()->toArray()
|
||||
];
|
||||
@endphp
|
||||
(function() {
|
||||
const data = @json($fc);
|
||||
if (data && data.features && data.features.length > 0) {
|
||||
const phaseLayer = L.geoJSON(data, {
|
||||
style: { color: '{{ $phase->color }}', weight: 3, opacity: 0.8, fillOpacity: 0.3 },
|
||||
onEachFeature: function(feature, layer) {
|
||||
const props = feature.properties || {};
|
||||
const featId = props._feature_id || feature.id;
|
||||
const safeName = escapeHtml(props.name || '{{ __('Feature') }}');
|
||||
const safeProgress = escapeHtml(props.progress || 0);
|
||||
const safeResponsible = escapeHtml(props.responsible || '-');
|
||||
let content = `<b>${safeName}</b><br>
|
||||
{{ __('Progress') }}: ${safeProgress}%<br>
|
||||
{{ __('Responsible') }}: ${safeResponsible}<br>
|
||||
<button class="btn btn-xs btn-primary mt-1" onclick="selectFeature('${featId}')">✏️ {{ __('Edit') }}</button>`;
|
||||
layer.bindPopup(content);
|
||||
layer.on('click', function() { selectFeature(featId); });
|
||||
}
|
||||
});
|
||||
layers[{{ $phase->id }}] = phaseLayer;
|
||||
@if(in_array($phase->id, $activeLayers))
|
||||
phaseLayer.addTo(map);
|
||||
@endif
|
||||
}
|
||||
})()
|
||||
@endforeach
|
||||
|
||||
// Initialize combined bounds
|
||||
updateCombinedBounds();
|
||||
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
zoomToAllFeatures();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function updateCombinedBounds() {
|
||||
if (!map) return;
|
||||
combinedBounds = L.latLngBounds();
|
||||
let hasBounds = false;
|
||||
for (let id in layers) {
|
||||
const layer = layers[id];
|
||||
if (map.hasLayer(layer) && typeof layer.getBounds === 'function') {
|
||||
const b = layer.getBounds();
|
||||
if (b.isValid()) {
|
||||
combinedBounds.extend(b);
|
||||
hasBounds = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasBounds;
|
||||
}
|
||||
|
||||
function zoomToAllFeatures() {
|
||||
if (!map) return;
|
||||
updateCombinedBounds();
|
||||
if (combinedBounds && combinedBounds.isValid()) {
|
||||
map.fitBounds(combinedBounds, { padding: [20, 20] });
|
||||
} else {
|
||||
map.setView([{{ $project->lat }}, {{ $project->lng }}], 16);
|
||||
}
|
||||
}
|
||||
|
||||
function selectFeature(featureId) {
|
||||
@this.selectFeature(featureId);
|
||||
}
|
||||
|
||||
function getUserLocation() {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition((pos) => {
|
||||
const latlng = [pos.coords.latitude, pos.coords.longitude];
|
||||
L.marker(latlng).addTo(map).bindPopup('{{ __('My location') }}').openPopup();
|
||||
map.setView(latlng, 16);
|
||||
}, () => alert('{{ __('No results') }}'));
|
||||
} else {
|
||||
alert('{{ __('No results') }}');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('livewire:init', function () {
|
||||
setTimeout(initMap, 50);
|
||||
|
||||
Livewire.on('layersUpdated', (activeIds) => {
|
||||
const ids = Array.isArray(activeIds) ? activeIds[0] : activeIds;
|
||||
for (let id in layers) {
|
||||
const lid = parseInt(id);
|
||||
if (ids.includes(lid)) {
|
||||
if (!map.hasLayer(layers[id])) {
|
||||
layers[id].addTo(map);
|
||||
updateCombinedBounds();
|
||||
}
|
||||
} else {
|
||||
if (map.hasLayer(layers[id])) {
|
||||
map.removeLayer(layers[id]);
|
||||
updateCombinedBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
zoomToAllFeatures();
|
||||
});
|
||||
|
||||
Livewire.on('centerMap', zoomToAllFeatures);
|
||||
Livewire.on('mapResize', () => {
|
||||
if (map) {
|
||||
if (!this.resizeTimeout) {
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
this.resizeTimeout = null;
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Livewire.on('featureImagesToggled', (show, markers) => {
|
||||
const m = Array.isArray(markers) ? markers : markers[1];
|
||||
const s = Array.isArray(show) ? show[0] : show;
|
||||
if (imageMarkersLayer) {
|
||||
map.removeLayer(imageMarkersLayer);
|
||||
imageMarkersLayer = null;
|
||||
updateCombinedBounds();
|
||||
}
|
||||
if (s && m && m.length > 0) {
|
||||
imageMarkersLayer = L.layerGroup().addTo(map);
|
||||
const photoIcon = L.divIcon({
|
||||
html: '<span style="font-size: 20px; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.5));">📼</span>',
|
||||
className: '',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
m.forEach(marker => {
|
||||
const safeUrl = isValidUrl(marker.image_url) ? marker.image_url : '';
|
||||
const safeName = escapeHtml(marker.image_name || '');
|
||||
if (safeUrl) {
|
||||
const popupContent = `<b>${safeName}</b><br>
|
||||
<img src="${safeUrl}" class="max-w-[200px] max-h-[150px] rounded cursor-pointer"
|
||||
onclick="window.openViewer('${safeUrl}', '${safeName}')" />`;
|
||||
L.marker([marker.lat, marker.lng], { icon: photoIcon })
|
||||
.bindPopup(popupContent)
|
||||
.addTo(imageMarkersLayer);
|
||||
}
|
||||
});
|
||||
layers[{{ $phase->id }}] = phaseLayer;
|
||||
@if(in_array($phase->id, $activeLayers))
|
||||
phaseLayer.addTo(map);
|
||||
@endif
|
||||
updateCombinedBounds();
|
||||
}
|
||||
})();
|
||||
@endforeach
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
zoomToAllFeatures();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
function zoomToAllFeatures() {
|
||||
if (!map) return;
|
||||
const bounds = L.latLngBounds();
|
||||
let hasBounds = false;
|
||||
for (let id in layers) {
|
||||
const layer = layers[id];
|
||||
if (map.hasLayer(layer) && typeof layer.getBounds === 'function') {
|
||||
const b = layer.getBounds();
|
||||
if (b.isValid()) { bounds.extend(b); hasBounds = true; }
|
||||
}
|
||||
}
|
||||
if (hasBounds) map.fitBounds(bounds, { padding: [20, 20] });
|
||||
else map.setView([{{ $project->lat }}, {{ $project->lng }}], 16);
|
||||
}
|
||||
|
||||
function selectFeature(featureId) {
|
||||
@this.selectFeature(featureId);
|
||||
}
|
||||
|
||||
function getUserLocation() {
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition((pos) => {
|
||||
const latlng = [pos.coords.latitude, pos.coords.longitude];
|
||||
L.marker(latlng).addTo(map).bindPopup('Tu ubicación').openPopup();
|
||||
map.setView(latlng, 16);
|
||||
}, () => alert('No se pudo obtener la ubicación'));
|
||||
} else {
|
||||
alert('Geolocalización no soportada');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('livewire:init', function () {
|
||||
setTimeout(initMap, 100);
|
||||
|
||||
Livewire.on('layersUpdated', (activeIds) => {
|
||||
// Livewire wraps single parameters in an array, so we need to extract the actual data
|
||||
const ids = Array.isArray(activeIds) ? activeIds[0] : activeIds;
|
||||
for (let id in layers) {
|
||||
const lid = parseInt(id);
|
||||
if (ids.includes(lid)) {
|
||||
if (!map.hasLayer(layers[id])) layers[id].addTo(map);
|
||||
} else {
|
||||
if (map.hasLayer(layers[id])) map.removeLayer(layers[id]);
|
||||
window.openViewer = function(url, name) {
|
||||
if (!isValidUrl(url)) {
|
||||
console.error('Invalid URL provided to openViewer:', url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
zoomToAllFeatures();
|
||||
);
|
||||
|
||||
Livewire.on('centerMap', zoomToAllFeatures);
|
||||
Livewire.on('mapResize', () => { if (map) setTimeout(() => map.invalidateSize(), 100); });
|
||||
|
||||
// Toggle imágenes en mapa
|
||||
Livewire.on('featureImagesToggled', (show, markers) => {
|
||||
const m = Array.isArray(markers) ? markers : markers[1];
|
||||
const s = Array.isArray(show) ? show[0] : show;
|
||||
if (imageMarkersLayer) { map.removeLayer(imageMarkersLayer); imageMarkersLayer = null; }
|
||||
if (s && m && m.length > 0) {
|
||||
imageMarkersLayer = L.layerGroup().addTo(map);
|
||||
const photoIcon = L.divIcon({
|
||||
html: '<span style="font-size: 20px; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.5));">🖼️</span>',
|
||||
className: '',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
m.forEach(marker => {
|
||||
const popupContent = `<b>${marker.name}</b><br>
|
||||
<img src="${marker.image_url}" class="max-w-[200px] max-h-[150px] rounded cursor-pointer"
|
||||
onclick="window.openViewer('${marker.image_url}', '${marker.image_name}')" />`;
|
||||
L.marker([marker.lat, marker.lng], { icon: photoIcon })
|
||||
.bindPopup(popupContent)
|
||||
.addTo(imageMarkersLayer);
|
||||
});
|
||||
}
|
||||
const safeName = escapeHtml(name);
|
||||
if (imageViewerModal) imageViewerModal.remove();
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'imageViewerModal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:5000;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;padding:20px;cursor:pointer';
|
||||
overlay.innerHTML = `<div style="position:relative;max-width:90vw;max-height:90vh">
|
||||
<button onclick="this.closest('#imageViewerModal').remove()" style="position:absolute;top:-30px;right:0;color:white;font-size:24px;background:none;border:none;cursor:pointer">✕</button>
|
||||
<img src="${url}" alt="${safeName}" style="max-width:100%;max-height:85vh;object-fit:contain;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5)" />
|
||||
<p style="color:white;text-align:center;margin-top:8px;font-size:14px">${safeName}</p>
|
||||
</div>`;
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
document.body.appendChild(overlay);
|
||||
imageViewerModal = overlay;
|
||||
};
|
||||
});
|
||||
|
||||
// Modal para ver imagen al hacer clic
|
||||
window.openViewer = function(url, name) {
|
||||
if (imageViewerModal) imageViewerModal.remove();
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'imageViewerModal';
|
||||
overlay.style.cssText = 'position:fixed;inset:0;z-index:5000;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;padding:20px;cursor:pointer';
|
||||
overlay.innerHTML = `<div style="position:relative;max-width:90vw;max-height:90vh">
|
||||
<button onclick="this.closest('#imageViewerModal').remove()" style="position:absolute;top:-30px;right:0;color:white;font-size:24px;background:none;border:none;cursor:pointer">✕</button>
|
||||
<img src="${url}" alt="${name}" style="max-width:100%;max-height:85vh;object-fit:contain;border-radius:8px;box-shadow:0 4px 20px rgba(0,0,0,0.5)" />
|
||||
<p style="color:white;text-align:center;margin-top:8px;font-size:14px">${name}</p>
|
||||
</div>`;
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
|
||||
document.body.appendChild(overlay);
|
||||
imageViewerModal = overlay;
|
||||
};
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
@endpush
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl font-bold mb-4">Reportes y Analítica</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> Exportar Proyectos\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> Exportar Fases\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> Exportar Inspecciones\n </a>\n </div>\n </div>
|
||||
|
||||
<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">Rango de tiempo:</span>
|
||||
<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">Esta semana</option>
|
||||
<option value="month" selected>Este mes</option>
|
||||
<option value="quarter">Este trimestre</option>
|
||||
<option value="year">Este año</option>
|
||||
<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"
|
||||
|
||||
<button wire:click="loadChartData"
|
||||
class="btn btn-primary btn-sm">
|
||||
Actualizar
|
||||
{{ __('Update') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(isset($chartData['months']))
|
||||
<div class="grid gap-6 mb-8">
|
||||
{{-- Gráfico de progreso de proyectos --}}
|
||||
{{-- Project progress chart --}}
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<h3 class="text-lg font-semibold mb-4">Progreso de Proyectos (últimos 6 meses)</h3>
|
||||
<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>
|
||||
|
||||
{{-- Gráfico de inspecciones por tipo --}}
|
||||
{{-- Inspections by type chart --}}
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<h3 class="text-lg font-semibold mb-4">Inspecciones por Tipo</h3>
|
||||
<h3 class="text-lg font-semibold mb-4">{{ __('Inspections by Type') }}</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="inspectionTypesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Gráfico de proyectos por estado --}}
|
||||
{{-- Projects by status chart --}}
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<h3 class="text-lg font-semibold mb-4">Distribución de Proyectos por Estado</h3>
|
||||
<h3 class="text-lg font-semibold mb-4">{{ __('Projects by Status') }}</h3>
|
||||
<div style="position: relative; height: 300px;">
|
||||
<canvas id="projectsByStatusChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Gráfico de progreso promedio por proyecto --}}
|
||||
{{-- Average progress by project chart --}}
|
||||
<div class="bg-white rounded-lg shadow p-4">
|
||||
<h3 class="text-lg font-semibold mb-4">Progreso Promedio por Proyecto</h3>
|
||||
<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>
|
||||
|
||||
{{-- Tarjetas de métricas clave --}}
|
||||
{{-- 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 Proyectos Activos
|
||||
{{ __('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">
|
||||
Inspecciones Este Mes
|
||||
{{ __('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">
|
||||
Promedio de Progreso
|
||||
{{ __('Average Progress') }}
|
||||
</div>
|
||||
<div class="text-2xl font-bold">
|
||||
@php
|
||||
@@ -88,10 +88,10 @@
|
||||
{{ 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">
|
||||
Proyectos Completados
|
||||
{{ __('Completed Projects') }}
|
||||
</div>
|
||||
<div class="text-2xl font-bold">
|
||||
{{ \App\Models\Project::where('status', 'completed')->count() }}
|
||||
@@ -100,7 +100,7 @@
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-white rounded-lg shadow p-6 text-center">
|
||||
<p class="text-gray-500">Cargando datos...</p>
|
||||
<p class="text-gray-500">{{ __('Loading data...') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@@ -112,17 +112,17 @@
|
||||
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 => {
|
||||
@@ -162,7 +162,7 @@
|
||||
max: 100,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Progreso (%)'
|
||||
text: '{{ __("Progress") }} (%)'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +178,7 @@
|
||||
data: {
|
||||
labels: @json($chartData['inspectionTypes']['labels'] ?? []),
|
||||
datasets: [{
|
||||
label: 'Cantidad de inspecciones',
|
||||
label: '{{ __("Inspections") }}',
|
||||
data: @json($chartData['inspectionTypes']['data'] ?? []),
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.5)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
@@ -198,7 +198,7 @@
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cantidad'
|
||||
text: '{{ __("Total") }}'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,7 +214,7 @@
|
||||
data: {
|
||||
labels: @json($chartData['projectsByStatus']['labels'] ?? []),
|
||||
datasets: [{
|
||||
label: 'Proyectos por estado',
|
||||
label: '{{ __("Projects by Status") }}',
|
||||
data: @json($chartData['projectsByStatus']['data'] ?? []),
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.5)',
|
||||
@@ -255,13 +255,13 @@
|
||||
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: 'Progreso promedio (%)',
|
||||
label: '{{ __("Average Progress") }} (%)',
|
||||
data: sortedData.map(item => item.progress),
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.5)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
@@ -283,7 +283,7 @@
|
||||
max: 100,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Progreso (%)'
|
||||
text: '{{ __("Progress") }} (%)'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<div>
|
||||
<div class="bg-base-100 p-4 rounded shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">📋 Templates de inspección</h2>
|
||||
<h2 class="text-xl font-bold">📋 {{ __('Inspection templates') }}</h2>
|
||||
<div>
|
||||
<button wire:click="newTemplate" class="btn btn-primary btn-sm">
|
||||
Nuevo template
|
||||
{{ __('New template') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -21,10 +21,10 @@
|
||||
{{-- Nombre del template --}}
|
||||
<tr>
|
||||
<td class="w-1/4 py-3 pr-4 align-top">
|
||||
{{__('Nombre del template')}}
|
||||
{{ __('Template name') }}
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<input type="text" wire:model="form.name"
|
||||
<input type="text" wire:model="form.name"
|
||||
class="input w-full"
|
||||
required>
|
||||
</td>
|
||||
@@ -33,7 +33,7 @@
|
||||
{{-- Descripción --}}
|
||||
<tr>
|
||||
<td class="w-1/4 py-3 pr-4 align-top">
|
||||
{{__('Descripción')}}
|
||||
{{ __('Description') }}
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<textarea wire:model="form.description" class="textarea textarea-bordered w-full" rows="2"></textarea>
|
||||
@@ -43,11 +43,11 @@
|
||||
{{-- Fase asociada (opcional) --}}
|
||||
<tr>
|
||||
<td class="w-1/4 py-3 pr-4 align-top">
|
||||
{{__('Fase asociada (opcional)')}}
|
||||
{{ __('Associated phase (optional)') }}
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<select wire:model="form.phase_id" class="select select-bordered w-full">
|
||||
<option value="">Ninguna (global para el proyecto)</option>
|
||||
<option value="">{{ __('Global project') }}</option>
|
||||
@foreach($phases as $phase)
|
||||
<option value="{{ $phase->id }}" {{ old('form.phase_id') == $phase->id ? 'selected' : '' }}>
|
||||
{{ $phase->name }}
|
||||
@@ -61,22 +61,22 @@
|
||||
|
||||
{{-- Campos dinámicos --}}
|
||||
<div class="border-t pt-4 mt-2">
|
||||
<h3 class="font-bold mb-3">Campos del formulario</h3>
|
||||
<h3 class="font-bold mb-3">{{ __('Form fields') }}</h3>
|
||||
@foreach($form['fields'] as $index => $field)
|
||||
<div class="border p-3 rounded mb-3 bg-base-100">
|
||||
{{-- Fila: nombre interno --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<div class="font-medium">Nombre interno</div>
|
||||
<div class="font-medium">{{ __('Internal name') }}</div>
|
||||
<div><input type="text" wire:model="form.fields.{{ $index }}.name" placeholder="ej: altura_medida" class="input input-sm w-full"></div>
|
||||
</div>
|
||||
{{-- Fila: etiqueta --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<div class="font-medium">Etiqueta visible</div>
|
||||
<div class="font-medium">{{ __('Visible label') }}</div>
|
||||
<div><input type="text" wire:model="form.fields.{{ $index }}.label" placeholder="ej: Altura medida (m)" class="input input-sm w-full"></div>
|
||||
</div>
|
||||
{{-- Fila: tipo --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<div class="font-medium">Tipo de campo</div>
|
||||
<div class="font-medium">{{ __('Field type') }}</div>
|
||||
<div>
|
||||
<select wire:model="form.fields.{{ $index }}.type" class="select select-sm w-full">
|
||||
@foreach($fieldTypes as $typeValue => $typeLabel)
|
||||
@@ -87,37 +87,37 @@
|
||||
</div>
|
||||
{{-- Fila: requerido y botón eliminar --}}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<div class="font-medium">Requerido</div>
|
||||
<div class="font-medium">{{ __('Required') }}</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<input type="checkbox" wire:model="form.fields.{{ $index }}.required" class="checkbox checkbox-sm">
|
||||
<button type="button" wire:click="removeField({{ $index }})" class="btn btn-xs btn-error">Eliminar campo</button>
|
||||
<button type="button" wire:click="removeField({{ $index }})" class="btn btn-xs btn-error">{{ __('Remove field') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Campos adicionales según tipo --}}
|
||||
@if(in_array($field['type'], ['integer', 'decimal', 'percentage']))
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<div class="font-medium">Mínimo / Máximo / Paso</div>
|
||||
<div class="font-medium">{{ __('Min') }} / {{ __('Max') }} / {{ __('Step') }}</div>
|
||||
<div class="flex gap-2">
|
||||
<input type="number" wire:model="form.fields.{{ $index }}.min" placeholder="Mín" class="input input-xs w-20">
|
||||
<input type="number" wire:model="form.fields.{{ $index }}.max" placeholder="Máx" class="input input-xs w-20">
|
||||
<input type="number" step="any" wire:model="form.fields.{{ $index }}.step" placeholder="Paso" class="input input-xs w-20">
|
||||
<input type="number" wire:model="form.fields.{{ $index }}.min" placeholder="{{ __('Min') }}" class="input input-xs w-20">
|
||||
<input type="number" wire:model="form.fields.{{ $index }}.max" placeholder="{{ __('Max') }}" class="input input-xs w-20">
|
||||
<input type="number" step="any" wire:model="form.fields.{{ $index }}.step" placeholder="{{ __('Step') }}" class="input input-xs w-20">
|
||||
</div>
|
||||
</div>
|
||||
@elseif($field['type'] === 'select')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
||||
<div class="font-medium">Opciones (separadas por coma)</div>
|
||||
<div class="font-medium">{{ __('Options (comma separated)') }}</div>
|
||||
<div><input type="text" wire:model="form.fields.{{ $index }}.options" placeholder="ej: Bueno,Regular,Malo" class="input input-sm w-full"></div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
<button type="button" wire:click="addField" class="btn btn-sm btn-secondary mt-2">+ Agregar campo</button>
|
||||
<button type="button" wire:click="addField" class="btn btn-sm btn-secondary mt-2">+ {{ __('Add field') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">Guardar template</button>
|
||||
<button type="button" wire:click="cancelForm" class="btn">Cancelar</button>
|
||||
<button type="submit" class="btn btn-primary">{{ $editingTemplate ? __('Update') : __('Save template') }}</button>
|
||||
<button type="button" wire:click="cancelForm" class="btn">{{ __('Cancel') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
@@ -127,11 +127,11 @@
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Descripción</th>
|
||||
<th>Fase</th>
|
||||
<th>Campos</th>
|
||||
<th>Acciones</th>
|
||||
<th>{{ __('Name') }}</th>
|
||||
<th>{{ __('Description') }}</th>
|
||||
<th>{{ __('Phase') }}</th>
|
||||
<th>{{ __('Fields') }}</th>
|
||||
<th>{{ __('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -139,22 +139,24 @@
|
||||
<tr>
|
||||
<td>{{ $template->name }}</td>
|
||||
<td>{{ $template->description ?? '-' }}</td>
|
||||
<td>{{ $template->phase ? $template->phase->name : 'Global' }}</td>
|
||||
<td>{{ $template->phase ? $template->phase->name : __('Global project') }}</td>
|
||||
<td>{{ count($template->fields) }}</td>
|
||||
<td>
|
||||
<button wire:click="editTemplate({{ $template->id }})" class="btn btn-xs btn-warning">
|
||||
Editar
|
||||
{{ __('Edit') }}
|
||||
</button>
|
||||
<button wire:click="deleteTemplate({{ $template->id }})" class="btn btn-xs btn-error" onclick="return confirm('¿Eliminar template?')">Eliminar</button>
|
||||
<button wire:click="deleteTemplate({{ $template->id }})"
|
||||
wire:confirm="{{ __('Delete template confirmation') }}"
|
||||
class="btn btn-xs btn-error">{{ __('Delete') }}</button>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">No hay templates creados. Presiona "Nuevo template" para comenzar.</td>
|
||||
<td colspan="5" class="text-center">{{ __('No templates yet (table)') }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
<div>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('admin.users') }}" class="btn btn-ghost btn-sm px-2" wire:navigate>
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||||
</a>
|
||||
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
||||
{{ $user ? 'Editar usuario: ' . $user->name : 'Nuevo usuario' }}
|
||||
</h2>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8">
|
||||
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
|
||||
|
||||
@if(session('notify'))
|
||||
<div class="alert alert-success mb-4">
|
||||
<x-heroicon-o-check-circle class="w-5 h-5" />
|
||||
{{ session('notify') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-8">
|
||||
|
||||
<form wire:submit.prevent="save">
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-error text-sm mb-6">
|
||||
<x-heroicon-o-exclamation-circle class="w-5 h-5 shrink-0" />
|
||||
<ul class="list-disc pl-3 space-y-0.5">
|
||||
@foreach($errors->all() as $e) <li>{{ $e }}</li> @endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
1. INFORMACIÓN PERSONAL
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Información personal
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Título de cortesía
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="title" class="select select-bordered w-full max-w-xs">
|
||||
<option value="">— Sin título —</option>
|
||||
<option value="Sr.">Sr.</option>
|
||||
<option value="Sra.">Sra.</option>
|
||||
<option value="Dr.">Dr.</option>
|
||||
<option value="Dra.">Dra.</option>
|
||||
<option value="Ing.">Ing.</option>
|
||||
<option value="Arq.">Arq.</option>
|
||||
<option value="Prof.">Prof.</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Apellidos <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="lastName"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="García López" />
|
||||
@error('lastName') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Nombre <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<input type="text" wire:model="firstName"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Ana" />
|
||||
@error('firstName') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
2. VALIDACIÓN
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Validación de acceso
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Intervalo de fechas --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Válido desde / hasta
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Vacío = sin límite</p>
|
||||
</label>
|
||||
<div class="flex-1 flex items-center gap-2">
|
||||
<input type="date" wire:model="validFrom"
|
||||
class="input input-bordered flex-1" />
|
||||
<span class="text-gray-400 shrink-0">→</span>
|
||||
<input type="date" wire:model="validUntil"
|
||||
class="input input-bordered flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
@error('validFrom') <div class="flex gap-4"><div class="w-48 shrink-0"></div><p class="text-error text-xs flex-1">{{ $message }}</p></div> @enderror
|
||||
@error('validUntil') <div class="flex gap-4"><div class="w-48 shrink-0"></div><p class="text-error text-xs flex-1">{{ $message }}</p></div> @enderror
|
||||
|
||||
{{-- Contraseña con generador --}}
|
||||
<div class="flex items-start gap-4"
|
||||
x-data="{
|
||||
show: false,
|
||||
generate() {
|
||||
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
|
||||
const lower = 'abcdefghjkmnpqrstuvwxyz';
|
||||
const digits = '23456789';
|
||||
const symbols = '!@#$%&*';
|
||||
const all = upper + lower + digits + symbols;
|
||||
let pwd = upper[Math.floor(Math.random()*upper.length)]
|
||||
+ lower[Math.floor(Math.random()*lower.length)]
|
||||
+ digits[Math.floor(Math.random()*digits.length)]
|
||||
+ symbols[Math.floor(Math.random()*symbols.length)];
|
||||
for (let i = 4; i < 12; i++) {
|
||||
pwd += all[Math.floor(Math.random()*all.length)];
|
||||
}
|
||||
pwd = pwd.split('').sort(() => Math.random()-0.5).join('');
|
||||
$wire.set('formPassword', pwd);
|
||||
this.show = true;
|
||||
}
|
||||
}">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Contraseña
|
||||
@if(!$user) <span class="text-error">*</span> @endif
|
||||
@if($user)
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Vacío = no cambiar</p>
|
||||
@endif
|
||||
</label>
|
||||
<div class="flex-1 space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<label class="input input-bordered flex items-center gap-2 flex-1">
|
||||
<x-heroicon-o-lock-closed class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input wire:model="formPassword" class="grow"
|
||||
:type="show ? 'text' : 'password'"
|
||||
placeholder="{{ $user ? '••••••••' : 'Mínimo 8 caracteres' }}" />
|
||||
<button type="button" @click="show = !show"
|
||||
class="opacity-50 hover:opacity-100 transition-opacity">
|
||||
<template x-if="show">
|
||||
<x-heroicon-o-eye-slash class="w-4 h-4" />
|
||||
</template>
|
||||
<template x-if="!show">
|
||||
<x-heroicon-o-eye class="w-4 h-4" />
|
||||
</template>
|
||||
</button>
|
||||
</label>
|
||||
<button type="button" @click="generate()"
|
||||
class="btn btn-outline btn-sm gap-1 shrink-0"
|
||||
title="Generar contraseña aleatoria">
|
||||
<x-heroicon-o-arrow-path class="w-4 h-4" />
|
||||
Generar
|
||||
</button>
|
||||
</div>
|
||||
@if(!$user)
|
||||
<p class="text-xs text-gray-400">Mayúsculas, minúsculas, números y símbolo.</p>
|
||||
@endif
|
||||
@error('formPassword') <p class="text-error text-xs">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Estado --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Estado <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="userStatus" class="select select-bordered w-full max-w-xs">
|
||||
<option value="active">Activo</option>
|
||||
<option value="inactive">Inactivo</option>
|
||||
<option value="suspended">Suspendido</option>
|
||||
</select>
|
||||
@error('userStatus') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
3. CONTACTO
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Contacto
|
||||
</h3>
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Empresa --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Empresa <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model.live="companyId" class="select select-bordered w-full">
|
||||
<option value="">— Seleccionar empresa —</option>
|
||||
@foreach($companies as $company)
|
||||
<option value="{{ $company->id }}">
|
||||
{{ $company->apodo ?: $company->name }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('companyId') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Dirección --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Dirección
|
||||
</label>
|
||||
<div class="flex-1 space-y-1.5">
|
||||
<textarea wire:model="address" rows="2"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Calle, número, ciudad, CP, país"></textarea>
|
||||
@if($companyId)
|
||||
<button type="button" wire:click="copyCompanyAddress"
|
||||
class="btn btn-xs btn-outline gap-1">
|
||||
<x-heroicon-o-document-duplicate class="w-3.5 h-3.5" />
|
||||
Copiar dirección de la empresa
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Teléfono --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Teléfono
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<x-heroicon-o-phone class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input type="tel" wire:model="phone" class="grow"
|
||||
placeholder="+34 600 123 456" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Email --}}
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Email <span class="text-error">*</span>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<x-heroicon-o-envelope class="w-4 h-4 opacity-40 shrink-0" />
|
||||
<input type="email" wire:model="email" class="grow"
|
||||
placeholder="ana@empresa.com" />
|
||||
</label>
|
||||
@error('email') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
4. PERMISOS
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Permisos
|
||||
</h3>
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Rol <span class="text-error">*</span>
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Define los permisos del usuario</p>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<select wire:model="formRole" class="select select-bordered w-full max-w-xs">
|
||||
@foreach($roles as $role)
|
||||
<option value="{{ $role->name }}">{{ $role->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('formRole') <p class="text-error text-xs mt-1">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ══════════════════════════════════════════════════════════
|
||||
5. NOTAS
|
||||
══════════════════════════════════════════════════════════ --}}
|
||||
<div class="pb-6 mb-6 border-b border-base-200">
|
||||
<h3 class="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
|
||||
Notas internas
|
||||
</h3>
|
||||
<div class="flex items-start gap-4">
|
||||
<label class="w-48 shrink-0 pt-2 text-sm font-medium text-gray-700">
|
||||
Notas
|
||||
<p class="text-xs text-gray-400 font-normal mt-0.5">Solo visible para administradores</p>
|
||||
</label>
|
||||
<div class="flex-1">
|
||||
<textarea wire:model="notes" rows="4"
|
||||
class="textarea textarea-bordered w-full"
|
||||
placeholder="Observaciones, historial, información relevante…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── Botones ───────────────────────────────────────────── --}}
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<a href="{{ route('admin.users') }}" class="btn btn-outline gap-1" wire:navigate>
|
||||
<x-heroicon-o-x-mark class="w-4 h-4" />
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary gap-2"
|
||||
wire:loading.attr="disabled" wire:target="save">
|
||||
<span wire:loading wire:target="save" class="loading loading-spinner loading-sm"></span>
|
||||
<x-heroicon-o-check class="w-4 h-4" />
|
||||
{{ $user ? 'Guardar cambios' : 'Crear usuario' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,552 @@
|
||||
<div>
|
||||
<x-slot name="header">
|
||||
|
||||
{{-- ── Header del usuario ───────────────────────────────────────────── --}}
|
||||
<div class="flex items-start justify-between gap-4 flex-wrap">
|
||||
|
||||
{{-- Izquierda: avatar + datos --}}
|
||||
<div class="flex items-start gap-4">
|
||||
{{-- Avatar --}}
|
||||
<div class="w-14 h-14 rounded-full bg-primary flex items-center justify-center shrink-0 shadow">
|
||||
<span class="text-xl font-bold text-primary-content">
|
||||
{{ strtoupper(substr($user->first_name ?: $user->name, 0, 1)) }}{{ strtoupper(substr($user->last_name ?: '', 0, 1)) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- Nombre + datos de contacto --}}
|
||||
<div>
|
||||
<h2 class="font-bold text-xl leading-tight">
|
||||
@if($user->title) <span class="text-gray-500 font-normal">{{ $user->title }}</span> @endif
|
||||
{{ $user->first_name && $user->last_name
|
||||
? $user->first_name . ' ' . $user->last_name
|
||||
: $user->name }}
|
||||
</h2>
|
||||
|
||||
{{-- Empresa --}}
|
||||
@if($user->company)
|
||||
<div class="flex items-center gap-1.5 mt-0.5">
|
||||
@if($user->company->logo_path && \Illuminate\Support\Facades\Storage::disk('public')->exists($user->company->logo_path))
|
||||
<img src="{{ \Illuminate\Support\Facades\Storage::disk('public')->url($user->company->logo_path) }}"
|
||||
class="w-4 h-4 object-contain rounded" alt="" />
|
||||
@else
|
||||
<x-heroicon-o-building-office class="w-3.5 h-3.5 text-gray-400" />
|
||||
@endif
|
||||
<span class="text-sm text-gray-600">{{ $user->company->apodo ?: $user->company->name }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Contacto inline --}}
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-0.5 mt-1 text-sm text-gray-500">
|
||||
@if($user->email)
|
||||
<span class="flex items-center gap-1">
|
||||
<x-heroicon-o-envelope class="w-3.5 h-3.5 opacity-60" />
|
||||
{{ $user->email }}
|
||||
</span>
|
||||
@endif
|
||||
@if($user->phone)
|
||||
<span class="flex items-center gap-1">
|
||||
<x-heroicon-o-phone class="w-3.5 h-3.5 opacity-60" />
|
||||
{{ $user->phone }}
|
||||
</span>
|
||||
@endif
|
||||
@if($user->address)
|
||||
<span class="flex items-center gap-1 max-w-xs">
|
||||
<x-heroicon-o-map-pin class="w-3.5 h-3.5 opacity-60 shrink-0" />
|
||||
<span class="truncate">{{ $user->address }}</span>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Derecha: estado + validez + botones --}}
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
{{-- Estado --}}
|
||||
@php
|
||||
$statusBadge = match($user->status ?? 'active') {
|
||||
'active' => ['badge-success', 'Activo'],
|
||||
'inactive' => ['badge-ghost', 'Inactivo'],
|
||||
'suspended' => ['badge-error', 'Suspendido'],
|
||||
default => ['badge-ghost', ucfirst($user->status ?? '')],
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $statusBadge[0] }} badge-md">{{ $statusBadge[1] }}</span>
|
||||
|
||||
{{-- Rol principal --}}
|
||||
@foreach($user->roles->take(1) as $role)
|
||||
<span class="badge {{ $role->name === 'Admin' ? 'badge-error' : 'badge-primary' }} badge-md">
|
||||
{{ $role->name }}
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Validez --}}
|
||||
@if($user->valid_from || $user->valid_until)
|
||||
@php
|
||||
$now = now();
|
||||
$from = $user->valid_from;
|
||||
$until = $user->valid_until;
|
||||
$isExpired = $until && $until->lt($now);
|
||||
$expireSoon = !$isExpired && $until && $until->diffInDays($now) <= 30;
|
||||
$notStarted = $from && $from->gt($now);
|
||||
$validColor = $isExpired || $notStarted ? 'text-error' : ($expireSoon ? 'text-warning' : 'text-gray-400');
|
||||
@endphp
|
||||
<p class="text-xs {{ $validColor }} flex items-center gap-1">
|
||||
<x-heroicon-o-calendar-days class="w-3.5 h-3.5" />
|
||||
@if($from && $until)
|
||||
{{ $from->format('d/m/Y') }} → {{ $until->format('d/m/Y') }}
|
||||
@elseif($from)
|
||||
Desde {{ $from->format('d/m/Y') }}
|
||||
@else
|
||||
Hasta {{ $until->format('d/m/Y') }}
|
||||
@endif
|
||||
@if($isExpired) <span class="font-semibold">(Expirado)</span>
|
||||
@elseif($notStarted) <span class="font-semibold">(No activo aún)</span>
|
||||
@elseif($expireSoon) <span class="font-semibold">(Expira pronto)</span>
|
||||
@endif
|
||||
</p>
|
||||
@endif
|
||||
|
||||
{{-- Botones --}}
|
||||
<div class="flex gap-2 mt-1">
|
||||
<a href="{{ route('admin.users.edit', $user) }}"
|
||||
class="btn btn-outline btn-sm gap-1" wire:navigate>
|
||||
<x-heroicon-o-pencil class="w-4 h-4" />
|
||||
Editar
|
||||
</a>
|
||||
<a href="{{ route('admin.users') }}"
|
||||
class="btn btn-ghost btn-sm gap-1" wire:navigate>
|
||||
<x-heroicon-o-arrow-left class="w-4 h-4" />
|
||||
Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</x-slot>
|
||||
|
||||
{{-- ── Tabs ──────────────────────────────────────────────────────────────── --}}
|
||||
<div class="py-6">
|
||||
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8 space-y-4">
|
||||
|
||||
<div role="tablist" class="tabs tabs-bordered">
|
||||
<button role="tab" wire:click="setTab('permissions')"
|
||||
class="tab gap-2 {{ $activeTab === 'permissions' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-shield-check class="w-4 h-4" />
|
||||
Permisos
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('projects')"
|
||||
class="tab gap-2 {{ $activeTab === 'projects' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-folder-open class="w-4 h-4" />
|
||||
Proyectos
|
||||
<span class="badge badge-sm badge-outline">{{ $user->projects->count() }}</span>
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('activity')"
|
||||
class="tab gap-2 {{ $activeTab === 'activity' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-clock class="w-4 h-4" />
|
||||
Actividad
|
||||
</button>
|
||||
<button role="tab" wire:click="setTab('notes')"
|
||||
class="tab gap-2 {{ $activeTab === 'notes' ? 'tab-active font-semibold' : '' }}">
|
||||
<x-heroicon-o-document-text class="w-4 h-4" />
|
||||
Notas
|
||||
@if($user->notes)
|
||||
<span class="badge badge-sm badge-primary">•</span>
|
||||
@endif
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: PERMISOS
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'permissions')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
{{-- Roles --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-6">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2 mb-4">
|
||||
<x-heroicon-o-shield-check class="w-5 h-5 text-primary" />
|
||||
Roles asignados
|
||||
</h3>
|
||||
@if($user->roles->isEmpty())
|
||||
<p class="text-sm text-gray-400">Sin roles asignados.</p>
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
@foreach($user->roles as $role)
|
||||
<div class="flex items-center gap-3 p-3 bg-base-200 rounded-lg">
|
||||
<span class="badge {{ $role->name === 'Admin' ? 'badge-error' : 'badge-primary' }} badge-lg">
|
||||
{{ $role->name }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Validez y estado --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-6">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2 mb-4">
|
||||
<x-heroicon-o-calendar-days class="w-5 h-5 text-primary" />
|
||||
Validez de acceso
|
||||
</h3>
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex justify-between items-center py-2 border-b border-base-200">
|
||||
<span class="text-gray-500">Estado</span>
|
||||
<span class="badge {{ $statusBadge[0] }}">{{ $statusBadge[1] }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2 border-b border-base-200">
|
||||
<span class="text-gray-500">Válido desde</span>
|
||||
<span class="font-medium">
|
||||
{{ $user->valid_from ? $user->valid_from->format('d/m/Y') : '— (sin límite)' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2 border-b border-base-200">
|
||||
<span class="text-gray-500">Válido hasta</span>
|
||||
<span class="font-medium {{ isset($isExpired) && $isExpired ? 'text-error font-bold' : '' }}">
|
||||
{{ $user->valid_until ? $user->valid_until->format('d/m/Y') : '— (sin límite)' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center py-2">
|
||||
<span class="text-gray-500">Email verificado</span>
|
||||
@if($user->email_verified_at)
|
||||
<span class="flex items-center gap-1 text-success text-xs font-medium">
|
||||
<x-heroicon-o-check-circle class="w-4 h-4" />
|
||||
{{ $user->email_verified_at->format('d/m/Y') }}
|
||||
</span>
|
||||
@else
|
||||
<span class="text-warning text-xs flex items-center gap-1">
|
||||
<x-heroicon-o-clock class="w-4 h-4" />
|
||||
Pendiente
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Empresa --}}
|
||||
@if($user->company)
|
||||
<div class="card bg-base-100 shadow md:col-span-2">
|
||||
<div class="card-body p-6">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2 mb-3">
|
||||
<x-heroicon-o-building-office-2 class="w-5 h-5 text-primary" />
|
||||
Empresa
|
||||
</h3>
|
||||
<div class="flex items-center gap-4">
|
||||
@if($user->company->logo_path && \Illuminate\Support\Facades\Storage::disk('public')->exists($user->company->logo_path))
|
||||
<img src="{{ \Illuminate\Support\Facades\Storage::disk('public')->url($user->company->logo_path) }}"
|
||||
class="w-14 h-14 object-contain border border-base-300 rounded-lg" alt="" />
|
||||
@else
|
||||
<div class="w-14 h-14 bg-base-200 rounded-lg flex items-center justify-center">
|
||||
<x-heroicon-o-building-office class="w-7 h-7 opacity-30" />
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<p class="font-semibold">{{ $user->company->name }}</p>
|
||||
@if($user->company->apodo)
|
||||
<p class="text-sm text-gray-500">{{ $user->company->apodo }}</p>
|
||||
@endif
|
||||
@if($user->company->email)
|
||||
<p class="text-xs text-gray-400">{{ $user->company->email }}</p>
|
||||
@endif
|
||||
</div>
|
||||
@php
|
||||
$typeBadge = match($user->company->type) {
|
||||
'owner' => ['badge-success', 'Promotor'],
|
||||
'constructor' => ['badge-primary', 'Constructor'],
|
||||
'subcontractor' => ['badge-secondary','Subcontratista'],
|
||||
'consultant' => ['badge-info', 'Consultor'],
|
||||
'supplier' => ['badge-warning', 'Proveedor'],
|
||||
default => ['badge-ghost', 'Otro'],
|
||||
};
|
||||
@endphp
|
||||
<span class="badge {{ $typeBadge[0] }} ml-auto">{{ $typeBadge[1] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: PROYECTOS
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'projects')
|
||||
<div class="space-y-4">
|
||||
|
||||
{{-- Formulario asignar --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-sm mb-3 flex items-center gap-2">
|
||||
<x-heroicon-o-plus-circle class="w-4 h-4 text-primary" />
|
||||
Asignar proyecto
|
||||
</h3>
|
||||
@if($availableProjects->isEmpty())
|
||||
<p class="text-sm text-gray-400">El usuario ya está asignado a todos los proyectos disponibles.</p>
|
||||
@else
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<div class="form-control flex-1 min-w-[200px]">
|
||||
<label class="label-text text-xs mb-1">Proyecto</label>
|
||||
<select wire:model="addProjectId" class="select select-bordered select-sm w-full">
|
||||
<option value="">— Seleccionar —</option>
|
||||
@foreach($availableProjects as $p)
|
||||
<option value="{{ $p->id }}">{{ $p->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('addProjectId') <p class="text-error text-xs mt-0.5">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div class="form-control flex-1 min-w-[160px]">
|
||||
<label class="label-text text-xs mb-1">Rol en proyecto</label>
|
||||
<input type="text" wire:model="addProjectRole"
|
||||
class="input input-bordered input-sm w-full"
|
||||
placeholder="ej: Jefe de obra" />
|
||||
</div>
|
||||
<button wire:click="assignProject" class="btn btn-primary btn-sm gap-1 shrink-0">
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
Asignar
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Lista proyectos --}}
|
||||
@if($user->projects->isEmpty())
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body items-center text-center py-10 text-gray-400">
|
||||
<x-heroicon-o-folder-open class="w-10 h-10 opacity-25 mb-2" />
|
||||
<p class="text-sm">Sin proyectos asignados.</p>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="card bg-base-100 shadow overflow-x-auto">
|
||||
<table class="table table-zebra w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Proyecto</th>
|
||||
<th>Rol</th>
|
||||
<th>Estado</th>
|
||||
<th>Progreso</th>
|
||||
<th class="w-16"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($user->projects as $project)
|
||||
@php
|
||||
$avg = $project->phases->avg('progress_percent') ?? 0;
|
||||
$sCfg = match($project->status) {
|
||||
'in_progress' => ['badge-primary', 'En progreso'],
|
||||
'completed' => ['badge-success', 'Completado'],
|
||||
'paused' => ['badge-warning', 'Pausado'],
|
||||
'planning' => ['badge-ghost', 'Planificación'],
|
||||
default => ['badge-ghost', ucfirst($project->status)],
|
||||
};
|
||||
@endphp
|
||||
<tr wire:key="proj-{{ $project->id }}">
|
||||
<td>
|
||||
<a href="{{ route('projects.dashboard', $project) }}"
|
||||
class="font-medium hover:text-primary transition-colors" wire:navigate>
|
||||
{{ $project->name }}
|
||||
</a>
|
||||
@if($project->address)
|
||||
<p class="text-xs text-gray-400 truncate max-w-[200px]">{{ $project->address }}</p>
|
||||
@endif
|
||||
</td>
|
||||
<td class="text-sm text-gray-600">
|
||||
{{ $project->pivot->role_in_project ?? '—' }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-sm {{ $sCfg[0] }}">{{ $sCfg[1] }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<progress class="progress progress-primary w-20 h-1.5"
|
||||
value="{{ round($avg) }}" max="100"></progress>
|
||||
<span class="text-xs text-gray-500">{{ round($avg) }}%</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button wire:click="removeProject({{ $project->id }})"
|
||||
wire:confirm="¿Desasignar a {{ $user->name }} del proyecto '{{ $project->name }}'?"
|
||||
class="btn btn-xs btn-outline btn-error" title="Desasignar">
|
||||
<x-heroicon-o-x-mark class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: ACTIVIDAD
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'activity')
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
{{-- Inspecciones --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2 mb-3">
|
||||
<x-heroicon-o-clipboard-document-list class="w-5 h-5 text-yellow-500" />
|
||||
Últimas inspecciones
|
||||
</h3>
|
||||
@if($recentInspections->isEmpty())
|
||||
<div class="text-center py-8 text-gray-400">
|
||||
<x-heroicon-o-clipboard-document-list class="w-8 h-8 mx-auto opacity-25 mb-1" />
|
||||
<p class="text-sm">Sin inspecciones registradas</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
@foreach($recentInspections as $ins)
|
||||
@php
|
||||
$rCfg = match($ins->result ?? '') {
|
||||
'pass' => ['badge-success', 'OK'],
|
||||
'fail' => ['badge-error', 'Fallo'],
|
||||
default => ['badge-ghost', '—'],
|
||||
};
|
||||
@endphp
|
||||
<div class="p-2.5 rounded-lg bg-base-200 text-sm">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="font-medium truncate flex-1">
|
||||
{{ $ins->template?->name ?? 'Inspección' }}
|
||||
</span>
|
||||
<span class="badge badge-xs {{ $rCfg[0] }} shrink-0">{{ $rCfg[1] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-0.5 text-xs text-gray-400">
|
||||
<span class="truncate">
|
||||
@if($ins->feature?->layer?->phase?->project)
|
||||
<x-heroicon-o-folder-open class="w-3 h-3 inline" />
|
||||
{{ $ins->feature->layer->phase->project->name }}
|
||||
@endif
|
||||
</span>
|
||||
<span class="shrink-0 ml-1">{{ $ins->created_at->diffForHumans() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Issues reportados --}}
|
||||
<div class="card bg-base-100 shadow">
|
||||
<div class="card-body p-5">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2 mb-3">
|
||||
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-orange-500" />
|
||||
Issues reportados
|
||||
</h3>
|
||||
@if($recentIssues->isEmpty())
|
||||
<div class="text-center py-8 text-gray-400">
|
||||
<x-heroicon-o-check-circle class="w-8 h-8 mx-auto opacity-25 mb-1" />
|
||||
<p class="text-sm">Sin issues reportados</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
@foreach($recentIssues as $issue)
|
||||
@php
|
||||
$pCfg = match($issue->priority ?? 'medium') {
|
||||
'critical' => ['badge-error', 'Crítico'],
|
||||
'high' => ['badge-warning', 'Alto'],
|
||||
'medium' => ['badge-info', 'Medio'],
|
||||
default => ['badge-ghost', 'Bajo'],
|
||||
};
|
||||
$stCfg = match($issue->status ?? 'open') {
|
||||
'open' => 'text-orange-500',
|
||||
'closed' => 'text-green-500',
|
||||
default => 'text-gray-400',
|
||||
};
|
||||
@endphp
|
||||
<div class="p-2.5 rounded-lg bg-base-200 text-sm">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<span class="font-medium truncate flex-1">{{ $issue->title }}</span>
|
||||
<span class="badge badge-xs {{ $pCfg[0] }} shrink-0">{{ $pCfg[1] }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-0.5 text-xs text-gray-400">
|
||||
<span class="truncate">
|
||||
@if($issue->project)
|
||||
<x-heroicon-o-folder-open class="w-3 h-3 inline" />
|
||||
{{ $issue->project->name }}
|
||||
@endif
|
||||
</span>
|
||||
<span class="{{ $stCfg }} shrink-0 ml-1 font-medium">
|
||||
{{ ucfirst($issue->status ?? 'open') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ════════════════════════════════════════════════════════════════════
|
||||
TAB: NOTAS
|
||||
════════════════════════════════════════════════════════════════════ --}}
|
||||
@if($activeTab === 'notes')
|
||||
<div class="card bg-base-100 shadow max-w-2xl">
|
||||
<div class="card-body p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-semibold text-base flex items-center gap-2">
|
||||
<x-heroicon-o-document-text class="w-5 h-5 text-primary" />
|
||||
Notas internas
|
||||
</h3>
|
||||
@if(!$editingNotes)
|
||||
<button wire:click="$set('editingNotes', true)"
|
||||
class="btn btn-sm btn-outline gap-1">
|
||||
<x-heroicon-o-pencil class="w-3.5 h-3.5" />
|
||||
Editar
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if($editingNotes)
|
||||
<textarea wire:model="notes" rows="10"
|
||||
class="textarea textarea-bordered w-full text-sm"
|
||||
placeholder="Añade notas, observaciones o información relevante sobre este usuario…"
|
||||
autofocus></textarea>
|
||||
<div class="flex justify-end gap-2 mt-3">
|
||||
<button wire:click="$set('editingNotes', false)"
|
||||
class="btn btn-outline btn-sm">Cancelar</button>
|
||||
<button wire:click="saveNotes"
|
||||
class="btn btn-primary btn-sm gap-1">
|
||||
<x-heroicon-o-check class="w-4 h-4" />
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
@else
|
||||
@if($user->notes)
|
||||
<div class="prose prose-sm max-w-none text-gray-700 whitespace-pre-line bg-base-200 rounded-lg p-4 min-h-[120px]">
|
||||
{{ $user->notes }}
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-12 text-gray-400">
|
||||
<x-heroicon-o-document-text class="w-10 h-10 mx-auto opacity-25 mb-2" />
|
||||
<p class="text-sm">Sin notas.</p>
|
||||
<button wire:click="$set('editingNotes', true)"
|
||||
class="btn btn-sm btn-outline mt-3 gap-1">
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
Añadir nota
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user