548 lines
25 KiB
PHP
548 lines
25 KiB
PHP
@extends('layouts.app')
|
|
@section('title', $project->name)
|
|
|
|
@section('content')
|
|
@include('professionalproject::layouts.nav')
|
|
@php
|
|
$projectStatuses = \Modules\ProfessionalProject\Entities\PpProject::statusDropdown();
|
|
$priorityLabels = \Modules\ProfessionalProject\Entities\PpTask::priorityDropdown();
|
|
$priorityColors = \Modules\ProfessionalProject\Entities\PpTask::priorityColors();
|
|
$tasks = $project->tasks ?? collect();
|
|
$totalTasks = $tasks->count();
|
|
$doneTaskIds = $task_statuses->where('is_completed', 1)->pluck('id')->toArray();
|
|
$completedTasks = $tasks->whereIn('status_id', $doneTaskIds)->count();
|
|
$overdueTasks = $tasks->filter(function ($task) use ($doneTaskIds) {
|
|
return !empty($task->due_date) && !in_array($task->status_id, $doneTaskIds) && \Carbon\Carbon::parse($task->due_date)->isPast();
|
|
})->count();
|
|
$upcomingTasks = $tasks->filter(function ($task) use ($doneTaskIds) {
|
|
if (empty($task->due_date) || in_array($task->status_id, $doneTaskIds)) {
|
|
return false;
|
|
}
|
|
$due = \Carbon\Carbon::parse($task->due_date);
|
|
return $due->isToday() || $due->isFuture();
|
|
})->count();
|
|
$timelineItems = collect();
|
|
foreach ($project->tasks as $task) {
|
|
$timelineItems->push([
|
|
'type' => 'task',
|
|
'icon' => 'fa-check-square-o',
|
|
'color' => '#00a65a',
|
|
'title' => $task->title,
|
|
'meta' => $task->status->title ?? '-',
|
|
'date' => $task->updated_at,
|
|
]);
|
|
}
|
|
foreach ($milestones as $milestone) {
|
|
$timelineItems->push([
|
|
'type' => 'milestone',
|
|
'icon' => 'fa-flag',
|
|
'color' => '#f39c12',
|
|
'title' => $milestone->title,
|
|
'meta' => $milestone->tasks_count . ' وظیفه',
|
|
'date' => $milestone->updated_at,
|
|
]);
|
|
}
|
|
$timelineItems = $timelineItems->sortByDesc('date')->take(10)->values();
|
|
@endphp
|
|
<section class="content-header">
|
|
<h1>{{ $project->name }}</h1>
|
|
</section>
|
|
<section class="content pp-show-page">
|
|
<div class="box box-solid pp-topbar">
|
|
<div class="box-body">
|
|
<div class="row">
|
|
<div class="col-md-7">
|
|
<span class="pp-topbar-title">{{ $project->name }}</span>
|
|
<span class="label pp-status-pill">{{ $projectStatuses[$project->status] ?? $project->status }}</span>
|
|
<span class="pp-meta-chip"><i class="fa fa-calendar"></i> شروع: {{ $project->start_date ? format_date($project->start_date) : '-' }}</span>
|
|
<span class="pp-meta-chip"><i class="fa fa-calendar-check-o"></i> پایان: {{ $project->end_date ? format_date($project->end_date) : '-' }}</span>
|
|
</div>
|
|
<div class="col-md-5 text-right">
|
|
@can('professionalproject.edit_project')
|
|
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\ProjectController::class, 'edit'], $project->id) }}" class="btn btn-primary btn-sm">
|
|
<i class="fa fa-edit"></i> @lang('professionalproject::lang.edit_project')
|
|
</a>
|
|
@endcan
|
|
@can('professionalproject.create_task')
|
|
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\TaskController::class, 'create'], ['project_id' => $project->id]) }}" class="btn btn-success btn-sm">
|
|
<i class="fa fa-plus"></i> @lang('professionalproject::lang.create_task')
|
|
</a>
|
|
@endcan
|
|
@can('professionalproject.view_gantt')
|
|
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\GanttController::class, 'index'], $project->id) }}" class="btn btn-warning btn-sm">
|
|
<i class="fa fa-sitemap"></i> @lang('professionalproject::lang.gantt')
|
|
</a>
|
|
@endcan
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-3 col-sm-6">
|
|
<div class="small-box bg-aqua">
|
|
<div class="inner">
|
|
<h3>{{ $project->progress }}%</h3>
|
|
<p>@lang('professionalproject::lang.progress')</p>
|
|
</div>
|
|
<div class="icon"><i class="fa fa-line-chart"></i></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-sm-6">
|
|
<div class="small-box bg-green">
|
|
<div class="inner">
|
|
<h3>{{ $completedTasks }}/{{ $totalTasks }}</h3>
|
|
<p>وظایف تکمیل شده</p>
|
|
</div>
|
|
<div class="icon"><i class="fa fa-check-circle"></i></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-sm-6">
|
|
<div class="small-box bg-red">
|
|
<div class="inner">
|
|
<h3>{{ $overdueTasks }}</h3>
|
|
<p>وظایف عقبافتاده</p>
|
|
</div>
|
|
<div class="icon"><i class="fa fa-exclamation-triangle"></i></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3 col-sm-6">
|
|
<div class="small-box bg-yellow">
|
|
<div class="inner">
|
|
<h3>{{ $upcomingTasks }}</h3>
|
|
<p>وظایف باز آینده</p>
|
|
</div>
|
|
<div class="icon"><i class="fa fa-calendar"></i></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<div class="nav-tabs-custom">
|
|
<ul class="nav nav-tabs">
|
|
<li @if($tab == 'overview') class="active" @endif>
|
|
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\ProjectController::class, 'show'], $project->id) }}?tab=overview">@lang('professionalproject::lang.overview')</a>
|
|
</li>
|
|
<li @if($tab == 'tasks') class="active" @endif>
|
|
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\ProjectController::class, 'show'], $project->id) }}?tab=tasks">@lang('professionalproject::lang.tasks')</a>
|
|
</li>
|
|
<li @if($tab == 'milestones') class="active" @endif>
|
|
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\ProjectController::class, 'show'], $project->id) }}?tab=milestones">@lang('professionalproject::lang.milestones')</a>
|
|
</li>
|
|
@can('professionalproject.view_gantt')
|
|
<li>
|
|
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\GanttController::class, 'index'], $project->id) }}">@lang('professionalproject::lang.gantt')</a>
|
|
</li>
|
|
@endcan
|
|
</ul>
|
|
|
|
<div class="tab-content" style="padding: 15px;">
|
|
@if($tab == 'overview')
|
|
<div class="tab-pane active">
|
|
<div class="row">
|
|
<div class="col-md-7">
|
|
<div class="box box-solid">
|
|
<div class="box-header with-border">
|
|
<h3 class="box-title">توضیحات پروژه</h3>
|
|
</div>
|
|
<div class="box-body">
|
|
<p class="text-muted" style="line-height: 1.9;">{{ $project->description ?: 'برای این پروژه توضیحی ثبت نشده است.' }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="box box-solid">
|
|
<div class="box-header with-border">
|
|
<h3 class="box-title">شاخصهای کلیدی پروژه</h3>
|
|
</div>
|
|
<div class="box-body">
|
|
<table class="table table-striped table-bordered">
|
|
<tr>
|
|
<th style="width: 180px;">وضعیت پروژه</th>
|
|
<td><span class="label label-primary">{{ $projectStatuses[$project->status] ?? $project->status }}</span></td>
|
|
</tr>
|
|
<tr>
|
|
<th>@lang('professionalproject::lang.lead')</th>
|
|
<td>{{ $project->lead->user_full_name ?? '-' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>@lang('role.customer')</th>
|
|
<td>{{ $project->customer->name ?? '-' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>@lang('business.start_date')</th>
|
|
<td>{{ $project->start_date ? format_date($project->start_date) : '-' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>@lang('professionalproject::lang.end_date')</th>
|
|
<td>{{ $project->end_date ? format_date($project->end_date) : '-' }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>تعداد مایلستون</th>
|
|
<td>{{ $milestones->count() }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>تعداد کل وظایف</th>
|
|
<td>{{ $totalTasks }}</td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-5">
|
|
<div class="box box-solid">
|
|
<div class="box-header with-border">
|
|
<h3 class="box-title">@lang('professionalproject::lang.members')</h3>
|
|
</div>
|
|
<div class="box-body">
|
|
@if($project->members->count())
|
|
@foreach($project->members as $member)
|
|
<span class="label label-default" style="display:inline-block; margin: 3px 2px; padding: 7px 9px;">
|
|
<i class="fa fa-user"></i> {{ $member->user_full_name }}
|
|
</span>
|
|
@endforeach
|
|
@else
|
|
<p class="text-muted">عضوی برای پروژه ثبت نشده است.</p>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<div class="box box-solid">
|
|
<div class="box-header with-border">
|
|
<h3 class="box-title">تایملاین فعالیت پروژه</h3>
|
|
</div>
|
|
<div class="box-body">
|
|
@forelse($timelineItems as $item)
|
|
<div class="pp-timeline-item">
|
|
<span class="pp-timeline-icon" style="background: {{ $item['color'] }};">
|
|
<i class="fa {{ $item['icon'] }}"></i>
|
|
</span>
|
|
<div class="pp-timeline-content">
|
|
<div class="pp-timeline-title">{{ $item['title'] }}</div>
|
|
<div class="pp-timeline-meta">{{ $item['meta'] }}</div>
|
|
<div class="pp-timeline-date">{{ !empty($item['date']) ? format_datetime($item['date']) : '-' }}</div>
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<p class="text-muted">فعلاً رویدادی برای نمایش وجود ندارد.</p>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@elseif($tab == 'tasks')
|
|
<div class="tab-pane active">
|
|
@php
|
|
$taskBoards = [];
|
|
foreach ($task_statuses as $status) {
|
|
$taskBoards[$status->id] = $project->tasks->where('status_id', $status->id)->sortBy('position');
|
|
}
|
|
@endphp
|
|
<div class="pp-project-task-board">
|
|
@foreach($task_statuses as $status)
|
|
<div class="pp-project-task-col">
|
|
<div class="pp-project-task-col-head" style="border-top-color: {{ $status->color }};">
|
|
<strong>{{ $status->title }}</strong>
|
|
<span class="badge">{{ count($taskBoards[$status->id]) }}</span>
|
|
</div>
|
|
<div class="pp-project-task-col-body pp-project-task-dropzone" data-status-id="{{ $status->id }}">
|
|
@forelse($taskBoards[$status->id] as $task)
|
|
<div class="pp-project-task-card pp-project-task-item" data-task-id="{{ $task->id }}" data-position="{{ (float) $task->position }}">
|
|
<div class="pp-task-title">{{ $task->title }}</div>
|
|
<div class="pp-task-meta">
|
|
<span class="label {{ $priorityColors[$task->priority] ?? 'label-default' }}">{{ $priorityLabels[$task->priority] ?? $task->priority }}</span>
|
|
<span class="pp-task-date"><i class="fa fa-calendar"></i> {{ $task->due_date ? format_date($task->due_date) : '-' }}</span>
|
|
</div>
|
|
<div class="pp-task-meta">
|
|
<small><i class="fa fa-flag"></i> {{ $task->milestone->title ?? '-' }}</small>
|
|
</div>
|
|
<div class="pp-task-meta">
|
|
<small><i class="fa fa-user"></i> {{ $task->assignees->pluck('user_full_name')->implode('، ') ?: '-' }}</small>
|
|
</div>
|
|
<div class="text-left m-t-5">
|
|
@can('professionalproject.edit_task')
|
|
<a href="{{ action([\Modules\ProfessionalProject\Http\Controllers\TaskController::class, 'edit'], $task->id) }}" class="btn btn-xs btn-default"><i class="fa fa-edit"></i></a>
|
|
@endcan
|
|
</div>
|
|
</div>
|
|
@empty
|
|
<div class="text-muted small">آیتمی وجود ندارد</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
@elseif($tab == 'milestones')
|
|
<div class="tab-pane active">
|
|
<div class="clearfix">
|
|
@can('professionalproject.manage_milestones')
|
|
<button class="btn btn-primary btn-sm pull-left" id="add_milestone_btn">
|
|
<i class="fa fa-plus"></i> @lang('professionalproject::lang.add_milestone')
|
|
</button>
|
|
@endcan
|
|
</div>
|
|
|
|
<table class="table table-bordered table-hover" style="margin-top:15px;">
|
|
<thead>
|
|
<tr>
|
|
<th>@lang('professionalproject::lang.milestone_title')</th>
|
|
<th>@lang('professionalproject::lang.task_count')</th>
|
|
<th>@lang('messages.action')</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@forelse($milestones as $ms)
|
|
<tr>
|
|
<td>{{ $ms->title }}</td>
|
|
<td>{{ $ms->tasks_count }}</td>
|
|
<td>
|
|
@if($ms->milestone_type !== 'uncategorised' && auth()->user()->can('professionalproject.manage_milestones'))
|
|
<button class="btn btn-xs btn-danger delete_milestone" data-id="{{ $ms->id }}"><i class="fa fa-trash"></i></button>
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@empty
|
|
<tr>
|
|
<td colspan="3" class="text-center text-muted">مایلستونی برای این پروژه ثبت نشده است.</td>
|
|
</tr>
|
|
@endforelse
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
@endsection
|
|
|
|
@section('css')
|
|
@include('professionalproject::layouts.professional_style')
|
|
<style>
|
|
.pp-show-page .small-box {
|
|
border-radius: 10px;
|
|
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.08);
|
|
}
|
|
.pp-project-task-board {
|
|
display: flex;
|
|
gap: 12px;
|
|
overflow-x: auto;
|
|
padding-bottom: 6px;
|
|
}
|
|
.pp-project-task-col {
|
|
min-width: 295px;
|
|
max-width: 295px;
|
|
background: #f8fafc;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 10px;
|
|
}
|
|
.pp-project-task-col-head {
|
|
padding: 10px 12px;
|
|
border-top: 3px solid #3b82f6;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.pp-project-task-col-body {
|
|
min-height: 360px;
|
|
max-height: 620px;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
}
|
|
.pp-dropzone-hover {
|
|
background: #eef6ff;
|
|
border-radius: 8px;
|
|
}
|
|
.pp-project-task-card {
|
|
background: #fff;
|
|
border: 1px solid #e5e7eb;
|
|
border-right: 4px solid #60a5fa;
|
|
border-radius: 8px;
|
|
padding: 9px;
|
|
margin-bottom: 8px;
|
|
cursor: grab;
|
|
}
|
|
.pp-task-title {
|
|
font-weight: 700;
|
|
margin-bottom: 4px;
|
|
}
|
|
.pp-task-meta {
|
|
margin-bottom: 4px;
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
}
|
|
.pp-task-date {
|
|
margin-right: 6px;
|
|
}
|
|
.pp-project-task-placeholder {
|
|
border: 2px dashed #93c5fd;
|
|
background: #eff6ff;
|
|
border-radius: 8px;
|
|
height: 74px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.pp-is-dragging {
|
|
opacity: 0.8;
|
|
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.12);
|
|
}
|
|
.pp-show-page .nav-tabs-custom {
|
|
border-radius: 10px;
|
|
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06);
|
|
}
|
|
.pp-show-page .box.box-solid {
|
|
border-radius: 8px;
|
|
border: 1px solid #edf0f4;
|
|
}
|
|
.pp-show-page .table > tbody > tr > td,
|
|
.pp-show-page .table > tbody > tr > th {
|
|
vertical-align: middle;
|
|
}
|
|
.pp-show-page .pp-topbar {
|
|
border-radius: 10px;
|
|
margin-bottom: 15px;
|
|
border: 1px solid #e5e7eb;
|
|
}
|
|
.pp-show-page .pp-topbar-title {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
margin-left: 8px;
|
|
}
|
|
.pp-show-page .pp-status-pill {
|
|
background: #1f78d1;
|
|
display: inline-block;
|
|
margin-left: 6px;
|
|
font-size: 11px;
|
|
}
|
|
.pp-show-page .pp-meta-chip {
|
|
display: inline-block;
|
|
background: #f3f4f6;
|
|
border-radius: 999px;
|
|
padding: 4px 10px;
|
|
margin: 4px 3px;
|
|
font-size: 12px;
|
|
}
|
|
.pp-timeline-item {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
padding: 8px 0;
|
|
border-bottom: 1px dashed #e5e7eb;
|
|
}
|
|
.pp-timeline-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.pp-timeline-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
line-height: 28px;
|
|
text-align: center;
|
|
border-radius: 50%;
|
|
color: #fff;
|
|
margin-left: 10px;
|
|
flex: 0 0 28px;
|
|
}
|
|
.pp-timeline-title {
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
}
|
|
.pp-timeline-meta {
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
}
|
|
.pp-timeline-date {
|
|
color: #9ca3af;
|
|
font-size: 11px;
|
|
margin-top: 2px;
|
|
}
|
|
</style>
|
|
@endsection
|
|
|
|
@section('javascript')
|
|
<script>
|
|
$(document).ready(function() {
|
|
function initProjectTaskKanbanDrag() {
|
|
$('.pp-project-task-dropzone').sortable({
|
|
connectWith: '.pp-project-task-dropzone',
|
|
items: '> .pp-project-task-item',
|
|
placeholder: 'pp-project-task-placeholder',
|
|
tolerance: 'pointer',
|
|
cursor: 'move',
|
|
start: function(event, ui) {
|
|
ui.item.addClass('pp-is-dragging');
|
|
},
|
|
stop: function(event, ui) {
|
|
ui.item.removeClass('pp-is-dragging');
|
|
},
|
|
update: function(event, ui) {
|
|
// Only process once on the receiving list.
|
|
if (this !== ui.item.parent()[0]) {
|
|
return;
|
|
}
|
|
|
|
var $item = ui.item;
|
|
var taskId = $item.data('task-id');
|
|
var statusId = $item.closest('.pp-project-task-dropzone').data('status-id');
|
|
|
|
var prevPos = parseFloat($item.prev('.pp-project-task-item').data('position'));
|
|
var nextPos = parseFloat($item.next('.pp-project-task-item').data('position'));
|
|
var position;
|
|
|
|
if (!isNaN(prevPos) && !isNaN(nextPos)) {
|
|
position = (prevPos + nextPos) / 2;
|
|
} else if (!isNaN(prevPos)) {
|
|
position = prevPos + 16384;
|
|
} else if (!isNaN(nextPos)) {
|
|
position = nextPos / 2;
|
|
} else {
|
|
position = 16384;
|
|
}
|
|
|
|
$.post('{{ action([\Modules\ProfessionalProject\Http\Controllers\TaskController::class, "updateKanbanPosition"]) }}', {
|
|
_token: '{{ csrf_token() }}',
|
|
task_id: taskId,
|
|
status_id: statusId,
|
|
position: position
|
|
}, function(r) {
|
|
if (r && r.success) {
|
|
$item.attr('data-position', position).data('position', position);
|
|
} else {
|
|
toastr.error((r && r.msg) ? r.msg : 'خطا در جابهجایی وظیفه');
|
|
$('.pp-project-task-dropzone').sortable('cancel');
|
|
}
|
|
}).fail(function() {
|
|
toastr.error('خطا در ارتباط با سرور');
|
|
$('.pp-project-task-dropzone').sortable('cancel');
|
|
});
|
|
}
|
|
}).disableSelection();
|
|
}
|
|
@if($tab == 'tasks')
|
|
initProjectTaskKanbanDrag();
|
|
@endif
|
|
|
|
$('#add_milestone_btn').click(function() {
|
|
var title = prompt('@lang("professionalproject::lang.milestone_title")');
|
|
if (title) {
|
|
$.post('{{ action([\Modules\ProfessionalProject\Http\Controllers\MilestoneController::class, "store"]) }}', {
|
|
_token: '{{ csrf_token() }}',
|
|
project_id: {{ $project->id }},
|
|
title: title
|
|
}, function(r) { if (r.success) location.reload(); else toastr.error(r.msg); });
|
|
}
|
|
});
|
|
$('.delete_milestone').click(function() {
|
|
var id = $(this).data('id');
|
|
$.ajax({ method: 'DELETE', url: '/professional-project/milestones/' + id, data: { _token: '{{ csrf_token() }}' }, success: function(r) {
|
|
if (r.success) location.reload(); else toastr.error(r.msg);
|
|
}});
|
|
});
|
|
});
|
|
</script>
|
|
@endsection
|