Files
construprogress/resources/views/livewire/tasks/task-calendar.blade.php
T

229 lines
11 KiB
PHP
Raw Normal View History

<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