154 lines
9.0 KiB
PHP
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
|