229 lines
11 KiB
PHP
229 lines
11 KiB
PHP
<div class="space-y-6">
|
|||
|
|
<!-- Header -->
|
||
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||
|
|
<div>
|
||
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||
|
|
{{ $project ? 'Calendario: ' . $project->name : 'Calendario - Todas las tareas' }}
|
||
|
|
</h1>
|
||
|
|
<p class="text-gray-500 dark:text-gray-400 mt-1">
|
||
|
|
Vista de calendario de tareas
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<div class="flex flex-wrap gap-2">
|
||
|
|
@can('create tasks', $project ?? \App\Models\Project::first())
|
||
|
|
<a href="{{ $project ? route('projects.tasks.create', $project) : route('tasks.create') }}"
|
||
|
|
class="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||
|
|
</svg>
|
||
|
|
Nueva Tarea
|
||
|
|
</a>
|
||
|
|
@endcan
|
||
|
|
<a href="{{ $project ? route('projects.tasks', $project) : route('tasks.index') }}"
|
||
|
|
class="inline-flex items-center px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
|
||
|
|
</svg>
|
||
|
|
Vista Tabla
|
||
|
|
</a>
|
||
|
|
<a href="{{ $project ? route('projects.tasks.kanban', $project) : route('tasks.kanban') }}"
|
||
|
|
class="inline-flex items-center px-4 py-2 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors">
|
||
|
|
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/>
|
||
|
|
</svg>
|
||
|
|
Kanban
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Filters -->
|
||
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||
|
|
<div class="flex flex-col lg:flex-row gap-4">
|
||
|
|
<div class="flex-1 min-w-[250px]">
|
||
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Buscar</label>
|
||
|
|
<input type="text" wire:model.debounce.300ms="search" placeholder="Título, descripción..."
|
||
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="min-w-[160px]">
|
||
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Vista</label>
|
||
|
|
<select wire:model="viewMode" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||
|
|
<option value="dayGridMonth">Mes</option>
|
||
|
|
<option value="timeGridWeek">Semana</option>
|
||
|
|
<option value="timeGridDay">Día</option>
|
||
|
|
<option value="listWeek">Lista</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="min-w-[180px]">
|
||
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Asignado</label>
|
||
|
|
<select wire:model="assigneeFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||
|
|
<option value="">Todos</option>
|
||
|
|
@foreach($assignees as $assignee)
|
||
|
|
<option value="{{ $assignee->id }}">{{ $assignee->name }}</option>
|
||
|
|
@endforeach
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if($project)
|
||
|
|
<div class="min-w-[180px]">
|
||
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Fase</label>
|
||
|
|
<select wire:model="phaseFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||
|
|
<option value="">Todas</option>
|
||
|
|
@foreach($phases as $phase)
|
||
|
|
<option value="{{ $phase->id }}">{{ $phase->name }}</option>
|
||
|
|
@endforeach
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
<div class="min-w-[160px]">
|
||
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Prioridad</label>
|
||
|
|
<select wire:model="priorityFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||
|
|
<option value="">Todas</option>
|
||
|
|
@foreach($priorityOptions as $value => $label)
|
||
|
|
<option value="{{ $value }}">{{ $label }}</option>
|
||
|
|
@endforeach
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="min-w-[160px]">
|
||
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Estado</label>
|
||
|
|
<select wire:model="statusFilter" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||
|
|
<option value="">Todos</option>
|
||
|
|
@foreach($statusOptions as $value => $label)
|
||
|
|
<option value="{{ $value }}">{{ $label }}</option>
|
||
|
|
@endforeach
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Calendar Container -->
|
||
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||
|
|
<div id="calendar" class="min-h-[600px]"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Legend -->
|
||
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||
|
|
<h4 class="font-medium text-gray-900 dark:text-white mb-3">Leyenda</h4>
|
||
|
|
<div class="flex flex-wrap gap-4 text-sm">
|
||
|
|
@foreach(['low' => '#6b7280', 'medium' => '#f59e0b', 'high' => '#ef4444', 'critical' => '#7c3aed'] as $key => $color)
|
||
|
|
<span class="flex items-center gap-2" style="color: {{ $color }}">
|
||
|
|
<span class="w-3 h-3 rounded" style="background-color: {{ $color }}"></span>
|
||
|
|
{{ \App\Models\Task::priorityOptions()[$key] ?? ucfirst($key) }}
|
||
|
|
</span>
|
||
|
|
@endforeach
|
||
|
|
<span class="flex items-center gap-2">
|
||
|
|
<span class="w-3 h-3 rounded border-2 border-dashed border-gray-400"></span>
|
||
|
|
Tareas vencidas
|
||
|
|
</span>
|
||
|
|
<span class="flex items-center gap-2">
|
||
|
|
<span class="w-3 h-3 rounded border-2 border-amber-500 bg-amber-500/20"></span>
|
||
|
|
Vence hoy
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@push('scripts')
|
||
|
|
<script>
|
||
|
|
// FullCalendar initialization
|
||
|
|
document.addEventListener('livewire:load', () => {
|
||
|
|
// Load FullCalendar dynamically if not already loaded
|
||
|
|
if (typeof FullCalendar === 'undefined') {
|
||
|
|
// Load CSS
|
||
|
|
const css = document.createElement('link');
|
||
|
|
css.rel = 'stylesheet';
|
||
|
|
css.href = 'https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.11/index.global.min.css';
|
||
|
|
document.head.appendChild(css);
|
||
|
|
|
||
|
|
// Load JS
|
||
|
|
const js = document.createElement('script');
|
||
|
|
js.src = 'https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.11/index.global.min.js';
|
||
|
|
js.onload = () => {
|
||
|
|
initCalendar();
|
||
|
|
};
|
||
|
|
document.head.appendChild(js);
|
||
|
|
} else {
|
||
|
|
initCalendar();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
let calendarInstance = null;
|
||
|
|
|
||
|
|
function initCalendar() {
|
||
|
|
const events = @json($events);
|
||
|
|
const initialView = '{{ $viewMode }}';
|
||
|
|
|
||
|
|
const calendarEl = document.getElementById('calendar');
|
||
|
|
if (!calendarEl) return;
|
||
|
|
|
||
|
|
// Destroy existing instance
|
||
|
|
if (calendarInstance) {
|
||
|
|
calendarInstance.destroy();
|
||
|
|
}
|
||
|
|
|
||
|
|
calendarInstance = new FullCalendar.Calendar(calendarEl, {
|
||
|
|
initialView: initialView,
|
||
|
|
locale: '{{ app()->getLocale() }}',
|
||
|
|
headerToolbar: {
|
||
|
|
left: 'prev,next today',
|
||
|
|
center: 'title',
|
||
|
|
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
||
|
|
},
|
||
|
|
events: events,
|
||
|
|
eventTimeFormat: {
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit',
|
||
|
|
meridiem: false
|
||
|
|
},
|
||
|
|
eventDidMount: function(info) {
|
||
|
|
// Add tooltip
|
||
|
|
const props = info.event.extendedProps;
|
||
|
|
let tooltip = `<strong>${info.event.title}</strong>`;
|
||
|
|
if (props.project) tooltip += `<br>Proyecto: ${props.project}`;
|
||
|
|
if (props.phase) tooltip += `<br>Fase: ${props.phase}`;
|
||
|
|
if (props.assignee) tooltip += `<br>Asignado: ${props.assignee}`;
|
||
|
|
if (props.dueDate) tooltip += `<br>Vence: ${props.dueDate}`;
|
||
|
|
if (props.statusLabel) tooltip += `<br>Estado: ${props.statusLabel}`;
|
||
|
|
if (props.priorityLabel) tooltip += `<br>Prioridad: ${props.priorityLabel}`;
|
||
|
|
if (props.subtasksCount > 0) tooltip += `<br>Subtareas: ${props.completedSubtasksCount}/${props.subtasksCount}`;
|
||
|
|
|
||
|
|
tippy(info.el, {
|
||
|
|
content: tooltip,
|
||
|
|
allowHTML: true,
|
||
|
|
theme: 'light',
|
||
|
|
placement: 'top'
|
||
|
|
});
|
||
|
|
},
|
||
|
|
eventClick: function(info) {
|
||
|
|
// Navigate to task detail
|
||
|
|
if (info.event.url) {
|
||
|
|
window.location.href = info.event.url;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
datesSet: function(info) {
|
||
|
|
// Optional: notify Livewire of view change
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
calendarInstance.render();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Listen for Livewire updates
|
||
|
|
document.addEventListener('livewire:render', () => {
|
||
|
|
// Re-initialize calendar when data changes
|
||
|
|
setTimeout(() => {
|
||
|
|
if (calendarInstance) {
|
||
|
|
calendarInstance.destroy();
|
||
|
|
}
|
||
|
|
initCalendar();
|
||
|
|
}, 100);
|
||
|
|
});
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<!-- Load Tippy.js for tooltips -->
|
||
|
|
<script src="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy-bundle.umd.min.js"></script>
|
||
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/dist/tippy.css">
|
||
|
|
@endpush
|