ultimatepos/Modules/ProfessionalProject/Resources/views/project/show.blade.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