ultimatepos/Modules/ProfessionalProject/Resources/views/gantt/chart.blade.php

785 lines
25 KiB
PHP

@extends('layouts.app')
@section('title', __('professionalproject::lang.gantt') . ' - ' . $project->name)
@section('content')
@include('professionalproject::layouts.nav')
<section class="content-header">
<h1>@lang('professionalproject::lang.gantt'): {{ $project->name }}</h1>
</section>
<section class="content pp-gantt-page">
<div class="box box-solid pp-gantt-box">
<div class="box-header with-border pp-gantt-toolbar">
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\ProjectController::class, 'show'], $project->id) }}" class="btn btn-default btn-sm">
<i class="fa fa-arrow-right"></i> @lang('messages.go_back')
</a>
</div>
<div class="box-body pp-gantt-body">
<div id="gantt_here"></div>
</div>
</div>
</section>
<div class="modal fade pp-gantt-modal-wrap" id="pp_gantt_task_modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-md" role="document">
<div class="modal-content pp-gantt-modal">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="@lang('messages.close')">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="pp_gantt_modal_title"></h4>
<p class="pp-gantt-modal-dates" id="pp_gantt_modal_dates"></p>
</div>
<div class="modal-body">
<div class="form-group">
<label for="pp_gantt_task_text">@lang('lang_v1.description')</label>
<textarea id="pp_gantt_task_text" class="form-control pp-gantt-textarea" rows="3" placeholder="@lang('lang_v1.description')"></textarea>
</div>
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label for="pp_gantt_start_date">@lang('business.start_date')</label>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input type="text" id="pp_gantt_start_date" class="form-control pp-gantt-datepicker" autocomplete="off" readonly>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label for="pp_gantt_duration">@lang('professionalproject::lang.duration_days')</label>
<div class="pp-gantt-duration-wrap">
<button type="button" class="btn btn-default pp-gantt-duration-minus" aria-label="-">
<i class="fa fa-minus"></i>
</button>
<input type="number" id="pp_gantt_duration" class="form-control pp-gantt-duration-input" min="1" step="1" value="1">
<button type="button" class="btn btn-default pp-gantt-duration-plus" aria-label="+">
<i class="fa fa-plus"></i>
</button>
</div>
</div>
</div>
</div>
<div class="pp-gantt-end-preview" id="pp_gantt_end_preview"></div>
</div>
<div class="modal-footer pp-gantt-modal-footer">
<button type="button" class="btn btn-danger pp-gantt-btn-delete">
<i class="fa fa-trash"></i> @lang('messages.delete')
</button>
<div class="pp-gantt-footer-actions">
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fa fa-times"></i> @lang('messages.cancel')
</button>
<button type="button" class="btn btn-primary pp-gantt-btn-save">
<i class="fa fa-check"></i> @lang('messages.save')
</button>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('css')
<link rel="stylesheet" href="{{ asset('modules/professionalproject/vendor/dhtmlxgantt/dhtmlxgantt.css?v=' . $asset_v) }}">
<style>
.pp-gantt-page .pp-gantt-box {
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 12px rgba(15, 23, 42, 0.08);
}
.pp-gantt-toolbar {
background: #f8fafc;
border-bottom: 1px solid #e5e7eb;
padding: 10px 15px;
}
.pp-gantt-body {
padding: 0;
background: #fff;
}
#gantt_here {
width: 100%;
height: 650px;
min-height: 500px;
background: #fff;
}
.gantt_grid_head_cell,
.gantt_scale_cell {
font-weight: 600;
color: #334155;
background: #f8fafc;
}
.gantt_grid_data .gantt_cell,
.gantt_tree_content {
color: #1e293b;
font-size: 13px;
}
.gantt_task_line {
border-radius: 4px;
border: none;
background-color: #0ea5e9;
}
.gantt_task_progress {
background-color: #0284c7;
}
.gantt_row.odd,
.gantt_task_row.odd {
background-color: #fafafa;
}
/* Hide default dhtmlx lightbox */
.gantt_cal_light,
.gantt_cal_cover {
display: none !important;
}
/* Custom task modal */
.pp-gantt-modal-wrap .modal-dialog {
margin: 60px auto;
}
.pp-gantt-modal {
border: none;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.2);
}
.pp-gantt-modal .modal-header {
background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%);
border-bottom: 1px solid #e2e8f0;
padding: 18px 20px 14px;
}
.pp-gantt-modal .modal-header .close {
margin-top: 2px;
opacity: 0.55;
}
.pp-gantt-modal .modal-title {
font-size: 16px;
font-weight: 700;
color: #0f172a;
line-height: 1.5;
padding-left: 24px;
}
.pp-gantt-modal-dates {
margin: 8px 0 0;
font-size: 13px;
color: #64748b;
}
.pp-gantt-modal .modal-body {
padding: 20px;
}
.pp-gantt-modal .form-group label {
font-weight: 600;
color: #334155;
margin-bottom: 8px;
}
.pp-gantt-textarea {
border-radius: 8px;
border-color: #d1d5db;
resize: vertical;
min-height: 88px;
}
.pp-gantt-textarea:focus,
.pp-gantt-datepicker:focus,
.pp-gantt-duration-input:focus {
border-color: #0ea5e9;
box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.15);
}
.pp-gantt-duration-wrap {
display: flex;
align-items: stretch;
gap: 8px;
}
.pp-gantt-duration-wrap .btn {
width: 42px;
border-radius: 8px;
border-color: #d1d5db;
flex-shrink: 0;
}
.pp-gantt-duration-input {
text-align: center;
border-radius: 8px;
border-color: #d1d5db;
font-weight: 600;
-moz-appearance: textfield;
}
.pp-gantt-duration-input::-webkit-outer-spin-button,
.pp-gantt-duration-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.pp-gantt-end-preview {
margin-top: 4px;
padding: 12px 14px;
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
color: #0369a1;
font-size: 13px;
}
.pp-gantt-end-preview i {
margin-left: 6px;
}
.pp-gantt-modal-footer {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
padding: 14px 20px;
background: #f8fafc;
border-top: 1px solid #e2e8f0;
}
.pp-gantt-footer-actions {
display: flex;
gap: 8px;
margin-right: auto;
}
.pp-gantt-modal-footer .btn {
border-radius: 8px;
min-width: 96px;
}
.pp-gantt-modal-footer .btn-primary {
background: #0ea5e9;
border-color: #0284c7;
}
.pp-gantt-modal-footer .btn-primary:hover {
background: #0284c7;
border-color: #0369a1;
}
.pp-gantt-modal-footer .btn-danger {
background: #fff;
color: #dc2626;
border-color: #fca5a5;
}
.pp-gantt-modal-footer .btn-danger:hover {
background: #fef2f2;
color: #b91c1c;
border-color: #f87171;
}
</style>
@endsection
@section('javascript')
@if(!is_persian_lang())
<script src="https://cdn.jsdelivr.net/npm/moment-jalaali@0.9.2/build/moment-jalaali.js"></script>
@endif
<script src="{{ asset('modules/professionalproject/vendor/dhtmlxgantt/dhtmlxgantt.js?v=' . $asset_v) }}"></script>
<script>
(function () {
var labels = {
save: @json(__('messages.save')),
cancel: @json(__('messages.cancel')),
delete: @json(__('messages.delete')),
description: @json(__('lang_v1.description')),
time_period: @json(__('professionalproject::lang.time_period')),
days: @json(__('professionalproject::lang.days')),
end_date: @json(__('professionalproject::lang.end_date')),
new_task: @json(__('professionalproject::lang.new_task')),
confirm_delete: @json('آیا از حذف این وظیفه مطمئن هستید؟')
};
var $modal = $('#pp_gantt_task_modal');
var modalTaskId = null;
var modalCancelled = false;
var usePersianPicker = typeof is_persian_lang !== 'undefined' && is_persian_lang;
function toPersianDigits(value) {
if (typeof persian_number === 'function') {
return persian_number(String(value));
}
return String(value);
}
function formatJalaliDate(date, includeYear) {
if (!date) {
return '';
}
try {
if (typeof moment !== 'undefined' && typeof moment.jDate === 'function') {
var m = moment(date);
if (!m.isValid()) {
return '';
}
var fmt = includeYear ? 'jD jMMMM jYYYY' : 'jD jMMMM';
return toPersianDigits(m.format(fmt));
}
return toPersianDigits(new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
numberingSystem: 'latn',
day: 'numeric',
month: 'long',
year: includeYear ? 'numeric' : undefined
}).format(date));
} catch (e) {
return '';
}
}
function formatJalaliString(value) {
if (!value) {
return '';
}
if (value instanceof Date) {
if (typeof gantt !== 'undefined' && gantt.config && gantt.config.jalali && gantt.date) {
return formatJalaliString(gantt.date.date_to_str('%Y-%m-%d')(value));
}
return formatJalaliDate(value, true);
}
var raw = String(value).trim();
if (/^1[34]\d{2}-\d{2}-\d{2}$/.test(raw) && typeof moment !== 'undefined' && typeof moment.jDate === 'function') {
var parts = raw.split('-');
var jalaliMoment = moment.jDate(parts[0], parts[1], parts[2]);
if (jalaliMoment.isValid()) {
return toPersianDigits(jalaliMoment.format('jD jMMMM jYYYY'));
}
}
return formatJalaliDate(raw, true);
}
function toGregorianDateString(value) {
if (!value) {
return '';
}
if (value instanceof Date) {
return gantt.date.date_to_str('%Y-%m-%d')(value);
}
var raw = String(value).trim();
if (/^1[34]\d{2}-\d{2}-\d{2}$/.test(raw) && typeof moment !== 'undefined' && typeof moment.jDate === 'function') {
var parts = raw.split('-');
var jalaliMoment = moment.jDate(parts[0], parts[1], parts[2]);
if (jalaliMoment.isValid()) {
return jalaliMoment.format('YYYY-MM-DD');
}
}
return raw;
}
function normalizeDigits(value) {
if (typeof english_number === 'function') {
return english_number(String(value));
}
return String(value);
}
function momentFromPicker(value) {
if (!value || typeof moment === 'undefined') {
return null;
}
var raw = normalizeDigits(value).trim();
if (usePersianPicker && typeof moment.jDate === 'function') {
return moment(raw, 'jYYYY/jMM/jDD');
}
return moment(raw, typeof moment_date_format !== 'undefined' ? moment_date_format.toUpperCase() : 'YYYY-MM-DD');
}
function pickerValueToGanttDate(value) {
if (!value) {
return '';
}
var m = momentFromPicker(value);
if (m && m.isValid()) {
if (gantt.config.jalali && typeof moment.jDate === 'function') {
return m.format('jYYYY-jMM-jDD');
}
return m.format('YYYY-MM-DD');
}
return normalizeDigits(value).replace(/\//g, '-');
}
function ganttDateToPickerValue(value) {
if (!value) {
return '';
}
if (value instanceof Date) {
if (usePersianPicker && typeof moment.jDate === 'function') {
return moment(value).format('jYYYY/jMM/jDD');
}
return moment(value).format('YYYY-MM-DD');
}
var raw = String(value).trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) {
return raw.replace(/-/g, '/');
}
return raw;
}
function parseGanttDate(value) {
if (value instanceof Date) {
return value;
}
if (typeof value === 'string' && value !== '') {
return gantt.date.str_to_date(value);
}
return null;
}
function formatGanttInternalDate(date) {
if (!date) {
return '';
}
if (gantt.config.jalali && gantt.date && gantt.date.date_to_str) {
return formatJalaliString(gantt.date.date_to_str('%Y-%m-%d')(date));
}
return formatJalaliDate(date, true);
}
function formatScaleMonth(date) {
return formatGanttInternalDate(date);
}
function formatScaleDay(date) {
if (!date) {
return '';
}
if (gantt.config.jalali && gantt.date && gantt.date.date_to_str) {
var jalali = gantt.date.date_to_str('%Y-%m-%d')(date);
var parts = jalali.split('-');
if (parts.length === 3 && typeof moment !== 'undefined' && typeof moment.jDate === 'function') {
return toPersianDigits(moment.jDate(parts[0], parts[1], parts[2]).format('jMM/jDD'));
}
}
return formatJalaliDate(date, false);
}
function updateModalPreview() {
var startVal = $('#pp_gantt_start_date').val();
var duration = Math.max(1, parseInt($('#pp_gantt_duration').val(), 10) || 1);
var text = $('#pp_gantt_task_text').val().trim();
$('#pp_gantt_modal_title').text(text || labels.new_task);
if (!startVal) {
$('#pp_gantt_modal_dates').text('');
$('#pp_gantt_end_preview').html('<i class="fa fa-info-circle"></i> ' + labels.time_period);
return;
}
var ganttStart = pickerValueToGanttDate(startVal);
var startDate = parseGanttDate(ganttStart);
if (!startDate) {
return;
}
var endDate = gantt.calculateEndDate(startDate, duration);
var startLabel = formatGanttInternalDate(startDate);
var endLabel = formatGanttInternalDate(endDate);
$('#pp_gantt_modal_dates').text(startLabel + ' — ' + endLabel);
$('#pp_gantt_end_preview').html(
'<i class="fa fa-calendar"></i> <strong>' + labels.end_date + ':</strong> ' +
endLabel + ' <span class="text-muted">(' + toPersianDigits(duration) + ' ' + labels.days + ')</span>'
);
}
function initModalDatepicker() {
var $input = $('#pp_gantt_start_date');
if (usePersianPicker && typeof initPersianDatepicker === 'function') {
initPersianDatepicker('#pp_gantt_start_date', {
autoClose: true,
format: typeof datepicker_date_format !== 'undefined' ? datepicker_date_format : 'YYYY/MM/DD'
});
} else if ($.fn.datepicker) {
try {
$input.datepicker('destroy');
} catch (e) {}
$input.datepicker({
autoclose: true,
format: typeof datepicker_date_format !== 'undefined' ? datepicker_date_format : 'yyyy-mm-dd'
});
}
$input.off('change.ppGantt').on('change.ppGantt', updateModalPreview);
}
function openTaskModal(id) {
var task = gantt.getTask(id);
modalTaskId = id;
modalCancelled = true;
$('#pp_gantt_task_text').val(task.text || '');
$('#pp_gantt_duration').val(Math.max(1, parseInt(task.duration, 10) || 1));
$('#pp_gantt_start_date').val(ganttDateToPickerValue(task.start_date));
if (task.$new) {
$('.pp-gantt-btn-delete').hide();
} else {
$('.pp-gantt-btn-delete').show();
}
$modal.modal('show');
}
function saveTaskModal() {
if (!modalTaskId) {
return;
}
var text = $('#pp_gantt_task_text').val().trim();
var duration = Math.max(1, parseInt($('#pp_gantt_duration').val(), 10) || 1);
var startDate = pickerValueToGanttDate($('#pp_gantt_start_date').val());
if (!text) {
$('#pp_gantt_task_text').focus();
return;
}
if (!startDate) {
$('#pp_gantt_start_date').focus();
return;
}
modalCancelled = false;
gantt.updateTask(modalTaskId, {
text: text,
start_date: startDate,
duration: duration
});
$modal.modal('hide');
}
function deleteTaskModal() {
if (!modalTaskId || !confirm(labels.confirm_delete)) {
return;
}
modalCancelled = false;
gantt.deleteTask(modalTaskId);
$modal.modal('hide');
}
gantt.config.date_format = '%Y-%m-%d';
gantt.config.xml_date = '%Y-%m-%d';
gantt.config.order_branch = true;
gantt.config.order_branch_free = true;
gantt.config.rtl = true;
gantt.config.jalali = true;
gantt.config.grid_width = 450;
gantt.config.row_height = 36;
gantt.config.bar_height = 22;
gantt.config.fit_tasks = true;
gantt.config.scroll_on_click = false;
gantt.config.open_tree_initially = true;
gantt.config.show_errors = false;
gantt.i18n.setLocale('fa');
gantt.i18n.setLocale({
locale: 'fa',
labels: {
gantt_save_btn: labels.save,
gantt_cancel_btn: labels.cancel,
gantt_delete_btn: labels.delete,
section_description: labels.description,
section_time: labels.time_period,
column_text: @json(__('messages.name')),
column_start_date: @json(__('business.start_date')),
column_duration: @json(__('professionalproject::lang.duration_days')),
confirm_closing: '',
confirm_deleting: labels.confirm_delete,
days: labels.days
}
});
gantt.config.columns = [
{ name: 'text', label: @json(__('messages.name')), tree: true, width: 240, resize: true },
{
name: 'start_date',
label: @json(__('business.start_date')),
align: 'center',
width: 130,
resize: true,
template: function (task) {
return formatJalaliString(task.start_date);
}
},
{
name: 'duration',
label: @json(__('professionalproject::lang.duration_days')),
align: 'center',
width: 80,
resize: true,
template: function (task) {
return toPersianDigits(task.duration || 0);
}
}
];
gantt.templates.grid_date = function (date) {
return formatGanttInternalDate(date);
};
gantt.config.scale_height = 54;
gantt.config.scales = [
{
unit: 'month',
step: 1,
format: formatScaleMonth
},
{
unit: 'day',
step: 1,
format: formatScaleDay
}
];
gantt.templates.task_text = function (start, end, task) {
var text = task.text || '';
if (text.length > 36) {
return text.substring(0, 33) + '...';
}
return text;
};
gantt.templates.tooltip_text = function (start, end, task) {
return '<b>' + @json(__('messages.name')) + ':</b> ' + (task.text || '') +
'<br/><b>' + @json(__('business.start_date')) + ':</b> ' + formatGanttInternalDate(start) +
'<br/><b>' + @json(__('professionalproject::lang.end_date')) + ':</b> ' + formatGanttInternalDate(end);
};
gantt.attachEvent('onBeforeLightbox', function (id) {
openTaskModal(id);
return false;
});
$modal.on('shown.bs.modal', function () {
initModalDatepicker();
updateModalPreview();
$('#pp_gantt_task_text').focus();
});
$modal.on('hidden.bs.modal', function () {
if (modalCancelled && modalTaskId && gantt.isTaskExists(modalTaskId)) {
var task = gantt.getTask(modalTaskId);
if (task.$new) {
gantt.deleteTask(modalTaskId);
}
}
modalTaskId = null;
});
$('.pp-gantt-btn-save').on('click', saveTaskModal);
$('.pp-gantt-btn-delete').on('click', deleteTaskModal);
$modal.find('[data-dismiss="modal"]').on('click', function () {
modalCancelled = true;
});
$('.pp-gantt-duration-minus').on('click', function () {
var $input = $('#pp_gantt_duration');
var val = Math.max(1, (parseInt($input.val(), 10) || 1) - 1);
$input.val(val).trigger('change');
updateModalPreview();
});
$('.pp-gantt-duration-plus').on('click', function () {
var $input = $('#pp_gantt_duration');
var val = (parseInt($input.val(), 10) || 1) + 1;
$input.val(val).trigger('change');
updateModalPreview();
});
$('#pp_gantt_duration').on('input change', updateModalPreview);
$('#pp_gantt_task_text').on('input', updateModalPreview);
gantt.init('gantt_here');
var tasks = { data: @json($tasks), links: @json($links) };
gantt.parse(tasks);
gantt.render();
$(window).on('resize.ppGantt', function () {
gantt.render();
});
gantt.createDataProcessor({
task: {
create: function (data) {
return $.post('{{ action([\Modules\ProfessionalProject\Http\Controllers\GanttApiController::class, 'storeTask']) }}', {
_token: '{{ csrf_token() }}',
project_id: {{ $project->id }},
text: data.text,
start_date: toGregorianDateString(data.start_date),
duration: data.duration,
progress: data.progress,
parent: data.parent,
type: data.type || 'task'
});
},
update: function (data, id) {
return $.ajax({
method: 'PUT',
url: '/professional-project/gantt/task/' + id,
data: {
_token: '{{ csrf_token() }}',
text: data.text,
start_date: toGregorianDateString(data.start_date),
duration: data.duration,
progress: data.progress,
parent: data.parent,
sortorder: data.sortorder
}
});
},
delete: function (id) {
return $.ajax({
method: 'DELETE',
url: '/professional-project/gantt/task/' + id,
data: { _token: '{{ csrf_token() }}' }
});
}
},
link: {
create: function (data) {
return $.post('{{ action([\Modules\ProfessionalProject\Http\Controllers\GanttApiController::class, 'storeLink']) }}', {
_token: '{{ csrf_token() }}',
project_id: {{ $project->id }},
source: data.source,
target: data.target,
type: data.type
});
},
update: function (data, id) {
return $.ajax({
method: 'PUT',
url: '/professional-project/gantt/link/' + id,
data: {
_token: '{{ csrf_token() }}',
source: data.source,
target: data.target,
type: data.type
}
});
},
delete: function (id) {
return $.ajax({
method: 'DELETE',
url: '/professional-project/gantt/link/' + id,
data: { _token: '{{ csrf_token() }}' }
});
}
}
});
})();
</script>
@endsection