151 lines
6.9 KiB
PHP
151 lines
6.9 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('content')
|
|
<div class="space-y-4">
|
|
<!-- Page Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<h1 class="text-2xl font-bold text-gray-900">{{ __('task.gantt') }}</h1>
|
|
<div class="flex items-center gap-2">
|
|
<select id="project-filter" onchange="filterProject(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') }} {{ __('project.projects') }}</option>
|
|
@foreach($projects as $project)
|
|
<option value="{{ $project->id }}" {{ request('project_id') == $project->id ? 'selected' : '' }}>{{ $project->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
@if(empty($ganttData))
|
|
<div class="bg-white rounded-xl border border-gray-200 p-12 text-center">
|
|
<svg class="w-12 h-12 mx-auto text-gray-300" 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>
|
|
<p class="mt-3 text-gray-500">{{ __('task.gantt_no_tasks') }}</p>
|
|
</div>
|
|
@else
|
|
<!-- Gantt Chart -->
|
|
<div class="bg-white rounded-xl border border-gray-200 overflow-hidden">
|
|
<div id="gantt_container" style="height: 600px;"></div>
|
|
</div>
|
|
|
|
<!-- Legend -->
|
|
<div class="bg-white rounded-xl border border-gray-200 p-4">
|
|
<h3 class="text-xs font-bold text-gray-700 mb-3">{{ __('task.gantt_legend') }}</h3>
|
|
<div class="flex flex-wrap gap-4">
|
|
<span class="flex items-center gap-1.5 text-xs text-gray-600"><span class="w-3 h-3 rounded" style="background:#14b8a6"></span> {{ __('task.status_pending') }}</span>
|
|
<span class="flex items-center gap-1.5 text-xs text-gray-600"><span class="w-3 h-3 rounded" style="background:#3b82f6"></span> {{ __('task.status_in_progress') }}</span>
|
|
<span class="flex items-center gap-1.5 text-xs text-gray-600"><span class="w-3 h-3 rounded" style="background:#f59e0b"></span> {{ __('task.status_review') }}</span>
|
|
<span class="flex items-center gap-1.5 text-xs text-gray-600"><span class="w-3 h-3 rounded" style="background:#10b981"></span> {{ __('task.status_done') }}</span>
|
|
<span class="flex items-center gap-1.5 text-xs text-gray-600"><span class="w-3 h-3 rounded" style="background:#ef4444"></span> {{ __('task.status_cancelled') }}</span>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
@if(!empty($ganttData))
|
|
@push('styles')
|
|
<link rel="stylesheet" href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css">
|
|
<style>
|
|
.gantt_grid_head_row { direction: ltr; }
|
|
.gantt_task_line { border-radius: 4px !important; }
|
|
.gantt_task_progress { border-radius: 4px 0 0 4px !important; opacity: 0.3; }
|
|
.gantt_row.project .gantt_cell { font-weight: bold; }
|
|
.today-marker { background: #ef4444; }
|
|
</style>
|
|
@endpush
|
|
|
|
@push('scripts')
|
|
<script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script>
|
|
<script src="https://cdn.dhtmlx.com/gantt/edge/ext/dhtmlxgantt_marker.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Configure Gantt
|
|
gantt.config.readonly = true;
|
|
gantt.config.drag_move = false;
|
|
gantt.config.drag_progress = false;
|
|
gantt.config.drag_resize = false;
|
|
gantt.config.drag_links = false;
|
|
gantt.config.show_links = false;
|
|
gantt.config.row_height = 36;
|
|
gantt.config.bar_height = 22;
|
|
gantt.config.date_format = '%d-%m-%Y';
|
|
gantt.config.scale_unit = 'week';
|
|
gantt.config.subscales = [{unit: 'day', step: 1, date: '%d'}];
|
|
gantt.config.min_column_width = 30;
|
|
gantt.config.fit_tasks = true;
|
|
gantt.config.auto_types = true;
|
|
|
|
// Columns
|
|
gantt.config.columns = [
|
|
{name: 'text', label: '{{ __("task.task_name") }}', tree: true, width: 200},
|
|
{name: 'start_date', label: '{{ __("task.start_date") }}', align: 'center', width: 80},
|
|
{name: 'duration', label: '{{ __("task.gantt_duration") }}', align: 'center', width: 50, template: function(task) { return task.duration + ' {{ __("task.gantt_day") }}'; }},
|
|
];
|
|
|
|
// Tooltip
|
|
gantt.templates.tooltip_text = function(start, end, task) {
|
|
let statusLabels = {
|
|
'pending': '{{ __("task.status_pending") }}',
|
|
'in_progress': '{{ __("task.status_in_progress") }}',
|
|
'review': '{{ __("task.status_review") }}',
|
|
'done': '{{ __("task.status_done") }}',
|
|
'cancelled': '{{ __("task.status_cancelled") }}'
|
|
};
|
|
let html = '<b>' + task.text + '</b><br>';
|
|
html += '{{ __("task.start_date") }}: ' + gantt.templates.tooltip_date_format(start) + '<br>';
|
|
html += '{{ __("task.gantt_duration") }}: ' + task.duration + ' {{ __("task.gantt_day") }}<br>';
|
|
if (task.assignee) html += '{{ __("task.assignee") }}: ' + task.assignee + '<br>';
|
|
if (task.status && statusLabels[task.status]) html += '{{ __("task.status") }}: ' + statusLabels[task.status];
|
|
return html;
|
|
};
|
|
|
|
// Today marker
|
|
gantt.plugins({ marker: true });
|
|
gantt.addMarker({
|
|
start_date: new Date(),
|
|
css: 'today-marker',
|
|
text: '{{ __("task.gantt_today") }}'
|
|
});
|
|
|
|
// Init
|
|
gantt.init('gantt_container');
|
|
|
|
var ganttData = @json($ganttData);
|
|
gantt.parse({ data: ganttData });
|
|
|
|
// Ctrl+Scroll zoom
|
|
var currentZoom = 'week';
|
|
var zoomLevels = {
|
|
'day': { scale_unit: 'day', subscales: [], min_column_width: 30 },
|
|
'week': { scale_unit: 'week', subscales: [{unit: 'day', step: 1, date: '%d'}], min_column_width: 30 },
|
|
'month': { scale_unit: 'month', subscales: [{unit: 'week', step: 1, date: '%w'}], min_column_width: 40 }
|
|
};
|
|
|
|
document.getElementById('gantt_container').addEventListener('wheel', function(e) {
|
|
if (e.ctrlKey) {
|
|
e.preventDefault();
|
|
var levels = ['day', 'week', 'month'];
|
|
var idx = levels.indexOf(currentZoom);
|
|
if (e.deltaY < 0 && idx > 0) {
|
|
currentZoom = levels[idx - 1];
|
|
} else if (e.deltaY > 0 && idx < levels.length - 1) {
|
|
currentZoom = levels[idx + 1];
|
|
}
|
|
var cfg = zoomLevels[currentZoom];
|
|
gantt.config.scale_unit = cfg.scale_unit;
|
|
gantt.config.subscales = cfg.subscales;
|
|
gantt.config.min_column_width = cfg.min_column_width;
|
|
gantt.render();
|
|
}
|
|
}, { passive: false });
|
|
});
|
|
|
|
function filterProject(projectId) {
|
|
var url = '{{ route("tasks.gantt") }}';
|
|
if (projectId) url += '?project_id=' + projectId;
|
|
window.location.href = url;
|
|
}
|
|
</script>
|
|
@endpush
|
|
@endif
|
|
@endsection
|