vernova/resources/views/tasks/kanban.blade.php
2026-07-26 19:58:27 +03:30

154 lines
9.0 KiB
PHP

@extends('layouts.app')
@section('content')
<div class="space-y-6">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-4">
<h1 class="text-2xl font-bold text-gray-900">{{ __('task.kanban') }}</h1>
<a href="{{ route('tasks.index') }}" class="text-sm text-teal-600 hover:text-teal-700">{{ __('task.listView') }}</a>
</div>
<div class="flex gap-2">
<select id="projectFilter" onchange="filterByProject(this.value)"
class="px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-teal-500">
<option value="">{{ __('common.all') }} {{ __('nav.projects') }}</option>
@foreach($projects as $project)
<option value="{{ $project->id }}" {{ $selectedProject == $project->id ? 'selected' : '' }}>{{ $project->name }}</option>
@endforeach
</select>
<a href="{{ route('tasks.create') }}" class="inline-flex items-center gap-2 px-4 py-2 bg-teal-600 text-white text-sm font-medium rounded-lg hover:bg-teal-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
{{ __('task.create') }}
</a>
</div>
</div>
<!-- Kanban Board -->
<div class="flex gap-4 overflow-x-auto pb-4" style="min-height: calc(100vh - 280px);">
@foreach([
'pending' => ['color' => 'gray', 'icon' => 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z'],
'in_progress' => ['color' => 'blue', 'icon' => 'M13 10V3L4 14h7v7l9-11h-7z'],
'review' => ['color' => 'amber', 'icon' => 'M15 12a3 3 0 11-6 0 3 3 0 016 0z'],
'done' => ['color' => 'emerald', 'icon' => 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z']
] as $status => $config)
<div class="flex-shrink-0 w-72"
x-data="{ tasks: [] }"
x-init="tasks = []">
<!-- Column Header -->
<div class="flex items-center gap-2 mb-3 px-1">
<div class="w-7 h-7 bg-{{ $config['color'] }}-100 text-{{ $config['color'] }}-600 rounded-lg flex items-center justify-center">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{{ $config['icon'] }}"/>
</svg>
</div>
<h3 class="text-sm font-bold text-gray-900">{{ __('task.status_' . $status) }}</h3>
<span class="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded-full">{{ $kanbanColumns[$status]->count() }}</span>
</div>
<!-- Column Body -->
<div class="bg-gray-100 rounded-xl p-2 min-h-[200px] space-y-2">
@forelse($kanbanColumns[$status] as $task)
<div class="bg-white rounded-lg border border-gray-200 p-3 shadow-sm hover:shadow-md transition-shadow cursor-pointer group">
<div class="flex items-start justify-between gap-2 mb-2">
<a href="{{ route('tasks.show', $task) }}" class="text-sm font-medium text-gray-900 hover:text-teal-600 line-clamp-2">
{{ $task->title }}
</a>
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-bold flex-shrink-0
{{ $task->priority === 'urgent' ? 'bg-red-100 text-red-700' :
($task->priority === 'high' ? 'bg-orange-100 text-orange-700' :
($task->priority === 'medium' ? 'bg-amber-100 text-amber-700' : 'bg-gray-100 text-gray-600')) }}">
{{ __('task.' . $task->priority) }}
</span>
</div>
@if($task->project)
<p class="text-xs text-gray-500 mb-2">{{ $task->project->name }}</p>
@endif
<div class="flex items-center justify-between">
<div class="flex items-center gap-1">
@if($task->due_date)
<span class="text-xs {{ $task->due_date->isPast() && $status !== 'done' ? 'text-red-600 font-medium' : 'text-gray-400' }}">
<svg class="w-3 h-3 inline-block me-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
{{ calendar_date($task->due_date) }}
</span>
@endif
</div>
<div class="flex items-center gap-1">
@if($task->assignee)
<div class="w-6 h-6 bg-teal-100 text-teal-700 rounded-full flex items-center justify-center text-[10px] font-bold" title="{{ $task->assignee->name }}">
{{ mb_strtoupper(mb_substr($task->assignee->name, 0, 1)) }}
</div>
@endif
{{-- Quick status change buttons --}}
<div class="opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-0.5">
@if($status !== 'pending')
<button onclick="moveTask({{ $task->id }}, 'pending')" class="p-0.5 text-gray-400 hover:text-gray-600" title="{{ __('task.status_pending') }}">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</button>
@endif
@if($status !== 'in_progress')
<button onclick="moveTask({{ $task->id }}, 'in_progress')" class="p-0.5 text-blue-400 hover:text-blue-600" title="{{ __('task.status_in_progress') }}">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
</button>
@endif
@if($status !== 'review')
<button onclick="moveTask({{ $task->id }}, 'review')" class="p-0.5 text-amber-400 hover:text-amber-600" title="{{ __('task.status_review') }}">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</button>
@endif
@if($status !== 'done')
<button onclick="moveTask({{ $task->id }}, 'done')" class="p-0.5 text-emerald-400 hover:text-emerald-600" title="{{ __('task.status_done') }}">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</button>
@endif
</div>
</div>
</div>
</div>
@empty
<div class="text-center py-8 text-gray-400 text-xs">
{{ __('task.no_tasks') }}
</div>
@endforelse
</div>
</div>
@endforeach
</div>
</div>
@push('scripts')
<script>
function filterByProject(projectId) {
const url = new URL(window.location.href);
if (projectId) {
url.searchParams.set('project_id', projectId);
} else {
url.searchParams.delete('project_id');
}
window.location.href = url.toString();
}
async function moveTask(taskId, newStatus) {
try {
const res = await fetch(`/tasks/${taskId}/status`, {
method: 'PUT',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ status: newStatus })
});
const data = await res.json();
if (data.success) {
location.reload();
}
} catch(e) {
console.error(e);
}
}
</script>
@endpush
@endsection