revert: roll back to 7d854ff (pre-security-review state)
Restores all 27 files changed by the security commit (f8a1310) and later work back to their7d854ffstate (2026-06-16 18:05), as requested. The security rewrite regressed map functionality (tabs, inspection editor, collapsing layers panel) without adding protections the7d854ffversion did not already have (XSS escaping + IDOR checks were already present). Done as a forward commit (no history rewrite / force-push) sof8a1310,a24c8a2and the merge remain in history and are fully recoverable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
<div class="py-12">
|
||||
<div class="max-w-5xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
@livewire('user-table')
|
||||
@livewire('admin-users')
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,327 @@
|
||||
<div>
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold text-gray-800 flex items-center">
|
||||
<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>
|
||||
{{ __('Company Management') }}
|
||||
</h2>
|
||||
<p class="text-gray-600 mt-2">{{ __('Manage the companies that participate in projects') }}</p>
|
||||
</div>
|
||||
|
||||
@if(session('message'))
|
||||
<div class="alert alert-success mb-4">
|
||||
<x-heroicon-o-check-circle class="w-5 h-5" />
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg text-green-800">
|
||||
{{ session('message') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-800">{{ __('Company Management') }}</h2>
|
||||
<p class="text-sm text-gray-500 mt-1">{{ __('Manage the companies that participate in projects') }}</p>
|
||||
</div>
|
||||
<a href="{{ route('companies.create') }}" class="btn btn-primary btn-sm gap-1" wire:navigate>
|
||||
<x-heroicon-o-plus class="w-4 h-4" />
|
||||
{{ __('New Company') }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
|
||||
<livewire:company-table />
|
||||
<!-- Búsqueda y Botón de Nueva Empresa -->
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div class="w-full md:w-1/2">
|
||||
<input type="text"
|
||||
wire:model.live="search"
|
||||
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">
|
||||
<button wire:click="toggleCreateForm"
|
||||
class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-6 rounded-lg flex items-center justify-center transition-colors">
|
||||
<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>
|
||||
{{ __('New Company') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de Creación/Edición -->
|
||||
<div wire:ignore.self x-cloak>
|
||||
<div x-show="@entangle('showCreateForm')" x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
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 ? __('Edit Company') : __('New Company') }}
|
||||
</h3>
|
||||
<p class="text-gray-600 mt-1">
|
||||
{{ __('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>{{ __('Validation errors') }}:</strong>
|
||||
<ul class="list-disc pl-5 mt-2 text-sm text-red-600">
|
||||
@foreach($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit.prevent="{{$editingCompanyId ? 'updateCompany' : 'createCompany'}}"
|
||||
enctype="multipart/form-data"
|
||||
class="space-y-4">
|
||||
|
||||
<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">{{ __('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">{{ __('Tax ID') }} *</label>
|
||||
<input type="text"
|
||||
wire:model="tax_id"
|
||||
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>
|
||||
</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">{{ __('Nickname') }}</label>
|
||||
<input type="text"
|
||||
wire:model="apodo"
|
||||
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">{{ __('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="">{{ __('Select a status') }}</option>
|
||||
<option value="activo">{{ __('Active') }}</option>
|
||||
<option value="inactivo">{{ __('Inactive') }}</option>
|
||||
<option value="suspendido">{{ __('Suspended') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</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">{{ __('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">{{ __('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="">{{ __('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">{{ __('Phone') }}</label>
|
||||
<input type="tel"
|
||||
wire:model="phone"
|
||||
placeholder="+34 600 123 456"
|
||||
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">{{ __('Email') }}</label>
|
||||
<input type="email"
|
||||
wire:model="email"
|
||||
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">{{ __('Website') }}</label>
|
||||
<input type="url"
|
||||
wire:model="website"
|
||||
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">{{ __('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>
|
||||
{{ __('Select file...') }}
|
||||
</label>
|
||||
<input type="file"
|
||||
wire:model="logo"
|
||||
accept="image/*"
|
||||
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="{{ __('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">
|
||||
{{ __('Remove') }}
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end pt-4 space-x-3">
|
||||
<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">
|
||||
{{ __('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 ? __('Update') : __('Create') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de Empresas -->
|
||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-800 flex items-center">
|
||||
<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>
|
||||
{{ __('Company list') }} ({{ $companies->count() }})
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@if($companies->isEmpty())
|
||||
<div class="px-6 py-8 text-center text-gray-500">
|
||||
<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 companies registered. Create your first company using the button above.') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="divide-y divide-gray-200">
|
||||
@foreach($companies as $company)
|
||||
<div class="px-6 py-4 flex flex-col md:flex-row md:items-start md:justify-between">
|
||||
<div class="flex-1 md:w-1/2">
|
||||
<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 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">
|
||||
<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="M12 2.25c-1.236 0-2.241.404-3.038 1.08a9.027 9.027 0 00-2.481 7.35c.178.404.317.845.418 1.306a4.42 4.42 0 001.266 2.05c.703.073 1.415.112 2.125.112a4.417 4.417 0 002.125-.112c.703 0 1.415-.039 2.125-.112a4.42 4.42 0 001.266-2.05a4.415 4.415 0 00.418-1.306c.797-.676 1.797-1.076 2.481-1.076A9.027 9.027 0 0018.978 9.68a11.025 11.025 0 01-4.597-.45z" />
|
||||
</svg>
|
||||
</div>
|
||||
@endif
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900">{{ $company->name }}</h4>
|
||||
<p class="text-sm text-gray-600 truncate">
|
||||
@if($company->tax_id)
|
||||
{{ $company->tax_id }}
|
||||
@else
|
||||
{{ __('No tax ID') }}
|
||||
@endif
|
||||
</p>
|
||||
@if($company->type)
|
||||
<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
|
||||
@elseif($company->type === 'consultant') bg-indigo-100 text-indigo-800
|
||||
@elseif($company->type === 'supplier') bg-yellow-100 text-yellow-800
|
||||
@else bg-gray-100 text-gray-800
|
||||
endif
|
||||
rounded">
|
||||
{{ ucfirst($company->type) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 md:mt-0 md:w-1/2 text-right space-y-2">
|
||||
<div class="text-sm text-gray-500 space-y-1">
|
||||
@if($company->address)
|
||||
<div class="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mt-0.5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.5 1.5 0 01-2.121-1.06L7 12.764l-.646.647a1 1 0 01-1.415-1.415l1.22-1.22a1.5 1.5 0 012.121-.39l3.707 3.707a1.5 1.5 0 011.06 2.12z" />
|
||||
</svg>
|
||||
<span>{{ $company->address }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($company->phone)
|
||||
<div class="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mt-0.5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.8.52l1.68-1.4a1 1 0 01.82-.52h4a2 2 0 012 2v5.5a2 2 0 01-2 2H5a2 2 0 01-2-2V5z" />
|
||||
</svg>
|
||||
<span>{{ $company->phone }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if($company->email)
|
||||
<div class="flex items-start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mt-0.5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12z" />
|
||||
</svg>
|
||||
<span>{{ $company->email }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button wire:click="editCompany({{ $company->id }})"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 font-medium flex items-center">
|
||||
<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>
|
||||
{{ __('Edit') }}
|
||||
</button>
|
||||
<button wire:click="deleteCompany({{ $company->id }})"
|
||||
class="text-sm text-red-600 hover:text-red-800 font-medium flex items-center"
|
||||
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>
|
||||
{{ __('Delete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if(!$loop->last)
|
||||
<div class="border-t border-gray-200"></div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
@@ -88,17 +88,6 @@
|
||||
let allLayersData = {}; // id -> {geojson, color}
|
||||
let visibleLayerIds = [];
|
||||
|
||||
// XSS-safe HTML escaping for user-supplied data rendered in Leaflet popups
|
||||
function escapeHtml(text) {
|
||||
if (text === null || text === undefined) return '';
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
// Inicialización del mapa
|
||||
function initMap() {
|
||||
if (map) return;
|
||||
@@ -148,9 +137,9 @@
|
||||
style: { color: data.color, weight: 3, opacity: 0.8, fillOpacity: 0.3 },
|
||||
onEachFeature: (feature, layer) => {
|
||||
const props = feature.properties;
|
||||
const content = `<b>${escapeHtml(props.name) || 'Elemento'}</b><br>
|
||||
Progreso: ${escapeHtml(props.progress) || 0}%<br>
|
||||
Responsable: ${escapeHtml(props.responsible) || '-'}`;
|
||||
const content = `<b>${props.name || 'Elemento'}</b><br>
|
||||
Progreso: ${props.progress || 0}%<br>
|
||||
Responsable: ${props.responsible || '-'}`;
|
||||
layer.bindPopup(content);
|
||||
}
|
||||
}).addTo(displayGroup);
|
||||
@@ -169,10 +158,10 @@
|
||||
onEachFeature: (f, l) => {
|
||||
l.feature = f;
|
||||
const props = f.properties;
|
||||
const content = `<b>${escapeHtml(props.name) || 'Elemento'}</b><br>
|
||||
Progreso: ${escapeHtml(props.progress) || 0}%<br>
|
||||
Responsable: ${escapeHtml(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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div>
|
||||
<div>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -25,19 +25,15 @@
|
||||
<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" />
|
||||
{{ __('Map') }}
|
||||
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.report', $project) }}" class="btn btn-outline btn-sm gap-1">
|
||||
<x-heroicon-o-document-chart-bar class="w-4 h-4" />
|
||||
{{ __('Report') }}
|
||||
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') }}
|
||||
Issues
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -307,7 +303,7 @@
|
||||
<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">{{ __('View all') }}</a>
|
||||
<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">
|
||||
@@ -350,7 +346,7 @@
|
||||
<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">{{ __('View on map') }}</a>
|
||||
<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">
|
||||
|
||||
@@ -2,40 +2,27 @@
|
||||
<div class="max-w-2xl mx-auto p-4">
|
||||
<h1 class="text-2xl font-bold mb-6">{{ $projectId ? __('Edit Project') : __('New Project') }}</h1>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-error text-sm mb-4">
|
||||
<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
|
||||
|
||||
<form wire:submit.prevent="save" class="space-y-6">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">{{ __('Name') }}</label>
|
||||
<input type="text" wire:model="name" class="input input-bordered w-full {{ $errors->has('name') ? 'input-error' : '' }}" placeholder="{{ __('Project name') }}" required>
|
||||
@error('name') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
||||
<input type="text" wire:model="name" class="input input-bordered w-full" placeholder="{{ __('Project name') }}" required>
|
||||
</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.') }}">
|
||||
@error('address') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
||||
<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>
|
||||
<input type="date" wire:model="start_date" class="input input-bordered w-full {{ $errors->has('start_date') ? 'input-error' : '' }}" required>
|
||||
@error('start_date') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
||||
<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>
|
||||
<input type="date" wire:model="end_date_estimated" class="input input-bordered w-full {{ $errors->has('end_date_estimated') ? 'input-error' : '' }}">
|
||||
@error('end_date_estimated') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
||||
<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">
|
||||
<label class="block text-sm font-medium mb-2">{{ __('Status') }}</label>
|
||||
@@ -45,14 +32,13 @@
|
||||
<option value="paused">{{ __('Paused') }}</option>
|
||||
<option value="completed">{{ __('Completed') }}</option>
|
||||
</select>
|
||||
@error('status') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
||||
</div>
|
||||
</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">
|
||||
@@ -61,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') }}
|
||||
|
||||
@@ -1,3 +1,51 @@
|
||||
<div>
|
||||
<livewire:project-table />
|
||||
<div class="flex justify-between mb-4">
|
||||
<input type="text" wire:model.live="search" placeholder="{{ __('Search') }}..." class="input input-bordered w-64" />
|
||||
<select wire:model.live="statusFilter" class="select select-bordered">
|
||||
<option value="">{{ __('All') }}</option>
|
||||
<option value="planning">{{ __('Planning') }}</option>
|
||||
<option value="in_progress">{{ __('In progress') }}</option>
|
||||
<option value="paused">{{ __('Paused') }}</option>
|
||||
<option value="completed">{{ __('Completed') }}</option>
|
||||
</select>
|
||||
@can('create projects')
|
||||
<a href="{{ route('projects.create') }}" class="btn btn-primary">+ {{ __('New Project') }}</a>
|
||||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-zebra">
|
||||
<thead>
|
||||
<tr><th>{{ __('Name') }}</th><th>{{ __('Address') }}</th><th>{{ __('Status') }}</th><th>{{ __('Progress') }}</th><th>{{ __('Actions') }}</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($projects as $project)
|
||||
<tr>
|
||||
<td>{{ $project->name }}</td>
|
||||
<td>{{ $project->address }}</td>
|
||||
<td>{{ __(ucfirst(str_replace('_', ' ', $project->status))) }}</td>
|
||||
<td>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
||||
<div class="bg-primary h-2.5 rounded-full" style="width: {{ $project->phases->avg('progress_percent') }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<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
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $projects->links() }}
|
||||
</div>
|
||||
|
||||
@@ -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' : '' }}">
|
||||
@@ -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,67 +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") }}
|
||||
</button>
|
||||
📁 {{ __('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>
|
||||
|
||||
<!-- Project navigation bar -->
|
||||
<div class="flex flex-wrap gap-1 mb-3">
|
||||
<a href="{{ route('projects.dashboard', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.dashboard') ? 'btn-primary' : 'btn-outline' }}">
|
||||
📊 {{ __('Dashboard') }}
|
||||
</a>
|
||||
<a href="{{ route('projects.map', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.map') ? 'btn-primary' : 'btn-outline' }}">
|
||||
🗺️ {{ __('Map') }}
|
||||
</a>
|
||||
<a href="{{ route('projects.gantt', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.gantt') ? 'btn-primary' : 'btn-outline' }}">
|
||||
📅 {{ __('Gantt') }}
|
||||
</a>
|
||||
<a href="{{ route('projects.report', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.report') ? 'btn-primary' : 'btn-outline' }}">
|
||||
📄 {{ __('Report') }}
|
||||
</a>
|
||||
<a href="{{ route('projects.issues', $project) }}"
|
||||
class="btn btn-xs {{ request()->routeIs('projects.issues') ? 'btn-error' : 'btn-outline' }} gap-1">
|
||||
⚠️ {{ __('Issues') }}
|
||||
@if($openIssuesCount > 0)
|
||||
<span class="badge badge-error badge-xs">{{ $openIssuesCount }}</span>
|
||||
@endif
|
||||
</a>
|
||||
</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('issues')" class="tab {{ $activeTab === 'issues' ? 'tab-active' : '' }} gap-1">
|
||||
{{ __('Issues') }}
|
||||
@if($openIssuesCount > 0)
|
||||
<span class="badge badge-error badge-xs">{{ $openIssuesCount }}</span>
|
||||
@endif
|
||||
</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 -->
|
||||
@@ -129,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>
|
||||
@@ -144,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', [
|
||||
@@ -167,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
|
||||
@@ -197,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
|
||||
@@ -211,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>
|
||||
@@ -236,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')
|
||||
@@ -255,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>
|
||||
@@ -284,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')
|
||||
@@ -294,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>
|
||||
@@ -319,16 +286,14 @@
|
||||
@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
|
||||
@elseif($activeTab === 'issues')
|
||||
<!-- Issues tab: render embedded IssueManager component -->
|
||||
@livewire('issue-manager', ['project' => $project], key('issues-tab-' . $project->id))
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('styles')
|
||||
<style>
|
||||
@@ -370,7 +335,7 @@
|
||||
// Prevent multiple initializations
|
||||
if (mapInitialized || map) return;
|
||||
mapInitialized = true;
|
||||
|
||||
|
||||
const center = [{{ $project->lat }}, {{ $project->lng }}];
|
||||
map = L.map('map').setView(center, 16);
|
||||
|
||||
@@ -408,14 +373,13 @@
|
||||
onEachFeature: function(feature, layer) {
|
||||
const props = feature.properties || {};
|
||||
const featId = props._feature_id || feature.id;
|
||||
// Escape all user-generated content for HTML context
|
||||
const safeName = escapeHtml(props.name || 'Elemento');
|
||||
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}')">✏️ Editar</button>`;
|
||||
{{ __('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); });
|
||||
}
|
||||
@@ -430,11 +394,11 @@
|
||||
|
||||
// Initialize combined bounds
|
||||
updateCombinedBounds();
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
zoomToAllFeatures();
|
||||
}, 100); // Reduced from 200ms to 100ms
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function updateCombinedBounds() {
|
||||
@@ -445,9 +409,9 @@
|
||||
const layer = layers[id];
|
||||
if (map.hasLayer(layer) && typeof layer.getBounds === 'function') {
|
||||
const b = layer.getBounds();
|
||||
if (b.isValid()) {
|
||||
combinedBounds.extend(b);
|
||||
hasBounds = true;
|
||||
if (b.isValid()) {
|
||||
combinedBounds.extend(b);
|
||||
hasBounds = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,10 +420,7 @@
|
||||
|
||||
function zoomToAllFeatures() {
|
||||
if (!map) return;
|
||||
|
||||
// Update combined bounds if needed
|
||||
updateCombinedBounds();
|
||||
|
||||
if (combinedBounds && combinedBounds.isValid()) {
|
||||
map.fitBounds(combinedBounds, { padding: [20, 20] });
|
||||
} else {
|
||||
@@ -475,32 +436,29 @@
|
||||
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();
|
||||
L.marker(latlng).addTo(map).bindPopup('{{ __('My location') }}').openPopup();
|
||||
map.setView(latlng, 16);
|
||||
}, () => alert('No se pudo obtener la ubicación'));
|
||||
}, () => alert('{{ __('No results') }}'));
|
||||
} else {
|
||||
alert('Geolocalización no soportada');
|
||||
alert('{{ __('No results') }}');
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('livewire:init', function () {
|
||||
setTimeout(initMap, 50); // Reduced from 100ms to 50ms
|
||||
setTimeout(initMap, 50);
|
||||
|
||||
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);
|
||||
// Update combined bounds when adding a layer
|
||||
updateCombinedBounds();
|
||||
}
|
||||
} else {
|
||||
if (map.hasLayer(layers[id])) {
|
||||
map.removeLayer(layers[id]);
|
||||
// Update combined bounds when removing a layer
|
||||
updateCombinedBounds();
|
||||
}
|
||||
}
|
||||
@@ -509,9 +467,8 @@
|
||||
});
|
||||
|
||||
Livewire.on('centerMap', zoomToAllFeatures);
|
||||
Livewire.on('mapResize', () => {
|
||||
Livewire.on('mapResize', () => {
|
||||
if (map) {
|
||||
// Throttle resize events to prevent excessive calls
|
||||
if (!this.resizeTimeout) {
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
map.invalidateSize();
|
||||
@@ -521,14 +478,12 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
// Update bounds when removing image markers layer
|
||||
if (imageMarkersLayer) {
|
||||
map.removeLayer(imageMarkersLayer);
|
||||
imageMarkersLayer = null;
|
||||
updateCombinedBounds();
|
||||
}
|
||||
if (s && m && m.length > 0) {
|
||||
@@ -540,10 +495,9 @@
|
||||
iconAnchor: [10, 10]
|
||||
});
|
||||
m.forEach(marker => {
|
||||
// Validate URL and sanitize name for security
|
||||
const safeUrl = isValidUrl(marker.image_url) ? marker.image_url : '';
|
||||
const safeName = escapeHtml(marker.image_name || '');
|
||||
if (safeUrl) { // Only add marker if URL is valid
|
||||
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}')" />`;
|
||||
@@ -552,21 +506,16 @@
|
||||
.addTo(imageMarkersLayer);
|
||||
}
|
||||
});
|
||||
// Update bounds when adding image markers layer
|
||||
updateCombinedBounds();
|
||||
}
|
||||
});
|
||||
|
||||
// Modal para ver imagen al hacer clic
|
||||
window.openViewer = function(url, name) {
|
||||
// Validate URL and sanitize name for security
|
||||
if (!isValidUrl(url)) {
|
||||
console.error('Invalid URL provided to openViewer:', url);
|
||||
return;
|
||||
}
|
||||
|
||||
const safeName = escapeHtml(name);
|
||||
|
||||
if (imageViewerModal) imageViewerModal.remove();
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'imageViewerModal';
|
||||
@@ -582,4 +531,4 @@
|
||||
};
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
@endpush
|
||||
|
||||
@@ -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,20 +21,19 @@
|
||||
{{-- Nombre del template --}}
|
||||
<tr>
|
||||
<td class="w-1/4 py-3 pr-4 align-top">
|
||||
{{__('Template name')}}
|
||||
{{ __('Template name') }}
|
||||
</td>
|
||||
<td class="py-3">
|
||||
<input type="text" wire:model="form.name"
|
||||
class="input w-full {{ $errors->has('form.name') ? 'input-error' : '' }}"
|
||||
class="input w-full"
|
||||
required>
|
||||
@error('form.name') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{{-- 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>
|
||||
@@ -44,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 }}
|
||||
@@ -62,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)
|
||||
@@ -88,43 +87,36 @@
|
||||
</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>
|
||||
|
||||
@if($errors->any())
|
||||
<div class="alert alert-error text-sm mt-3">
|
||||
<ul class="list-disc pl-3 space-y-0.5">
|
||||
@foreach($errors->all() as $e) <li>{{ $e }}</li> @endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">{{ __('Save template') }}</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>
|
||||
@@ -135,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>
|
||||
@@ -147,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>
|
||||
|
||||
Reference in New Issue
Block a user