OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)

This commit is contained in:
wangruiguo
2026-09-03 18:49:20 +08:00
commit d647428529
3501 changed files with 1988906 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
<div class="center padding-lg" style="max-width:1200px;">
<div class="row">
<div class="col-md-12">
<h1 style="font-size:var(--font-size-xxxl);">Create something new</h1><br />
{!! __("text.creation_hub") !!}
<br />
<br />
</div>
</div>
<div class="row">
@foreach($projectTypes as $projectType)
<div class="col-md-4 {{ $projectType["active"] !== true ? "disabled" : "" }}" >
<div class="profileBox">
<x-global::undrawSvg image="{{ $projectType['image'] }}" headline="{{ __($projectType['label']) }}" maxWidth="50%" height="150px"></x-global::undrawSvg>
<br />
{!! __($projectType["description"]) !!}
<br /><br />
@if($projectType["active"] == true )
<x-global::forms.button tag="a" link="{{ BASE_URL }}/{{ $projectType['url'] }}" contentRole="primary">{{ __($projectType['btnLabel']) }}</x-global::forms.button>
@else
<x-global::forms.button tag="a" link="#" contentRole="primary" class="disabled">Not Available in this plan</x-global::forms.button>
@endif
<div class="clearall"></div>
</div>
</div>
@endforeach
</div>
</div>

View File

@@ -0,0 +1,34 @@
@extends($layout)
@section('content')
<div class="pageheader">
<div class="pageicon"><span class="fa {{ $tpl->getModulePicture() }}"></span></div>
<div class="pagetitle">
<h5>{!! __('label.administration') !!}</h5>
<h1>{!! sprintf(__('headlines.delete_project_x'), $project['name']) !!}</h1>
</div>
</div><!--pageheader-->
<div class="maincontent">
<div class="maincontentinner">
{!! $tpl->displayNotification() !!}
<h4 class="widget widgettitle">{!! __('subtitles.delete') !!}</h4>
<div class="widgetcontent">
<form method="post">
<p>{!! __('text.confirm_project_deletion') !!}</p><br />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
<x-global::forms.button tag="a" link="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}" contentRole="tertiary">{!! __('buttons.back') !!}</x-global::forms.button>
</form>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,37 @@
<h4 class="widgettitle title-light">{!! sprintf(__('headlines.duplicate_project_x'), $project['name']) !!}</h4>
{!! $tpl->displayNotification() !!}
<form class="formModal" method="post" action="{{ BASE_URL }}/projects/duplicateProject/{{ $project['id'] }}">
<label>{!! __('label.newProjectName') !!}</label>
<x-global::forms.text-input name="projectName" value="{!! __('label.copy_of') !!} {{ $project['name'] }}" /><br />
<label>{!! __('label.planned_start_date') !!}</label>
<input type="text" name="startDate" class="projectDateFrom" value="{{ format(date('Y-m-d'))->date() }}" placeholder="{{ __('language.dateformat') }}" id="sprintStart" /><br />
<label>{!! __('label.client_product') !!}</label>
<select name="clientId" id="clientId">
@foreach ($allClients as $row)
<option value="{{ $row['id'] }}"
@if ($project['clientId'] == $row['id']) selected=selected @endif
>{{ $row['name'] }}</option>
@endforeach
</select>
<br />
<input style="float:left; margin-right:5px;"
type="checkbox" name="assignSameUsers" id="assignSameUsers"/>
<label for="assignSameUsers">{!! __('label.assignSameUsers') !!}</label>
<br />
<div class="row">
<div class="col-md-6">
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.duplicate')" />
</div>
<div class="col-md-6 align-right padding-top-sm">
</div>
</div>
</form>

View File

@@ -0,0 +1 @@
{{-- Intentionally empty stub EditAccount controller and template are not yet implemented. --}}

View File

@@ -0,0 +1 @@
{{-- Intentionally empty stub EditProject controller and template are not yet implemented. --}}

View File

@@ -0,0 +1,168 @@
@extends($layout)
@section('content')
<div class="pageheader">
<div class="pageicon"><span class="fa fa-suitcase"></span></div>
<div class="pagetitle">
<h5>{!! __('label.administration') !!}</h5>
<h1>{!! __('headline.new_project') !!}</h1>
</div>
</div><!--pageheader-->
<div class="maincontent">
<div class="maincontentinner">
{!! $tpl->displayNotification() !!}
<div class="tabbedwidget tab-primary projectTabs">
<ul>
<li><a href="#projectdetails">{!! __('tabs.projectdetails') !!}</a></li>
</ul>
<div id="projectdetails">
<form action="" method="post" class="">
<div class="row">
<div class="col-md-8">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<x-global::forms.text-input variant="headline" name="name" id="name" style="width:99%" value="{{ $project['name'] }}" placeholder="{{ __('input.placeholders.enter_title_of_project') }}" />
</div>
<input type="hidden" name="projectState" id="projectState" value="0" />
</div>
</div>
<div class="row">
<div class="col-md-12">
<br />
<p>
{!! __('label.accomplish') !!}
{!! __('label.describe_outcome') !!}
<br /><br />
</p>
<textarea name="details" id="details" class="tiptapComplex" rows="5" cols="50">{{ $project['details'] }}</textarea>
</div>
</div>
<div class="padding-top">
@if (isset($project['id']) && $project['id'] != '')
<div class="pull-right padding-top">
<x-global::forms.button tag="a" link="{{ BASE_URL }}/projects/delProject/{{ $project['id'] }}" class="delete" state="danger" variant="outline"><i class="fa fa-trash"></i> {!! __('buttons.delete') !!}</x-global::forms.button>
</div>
@endif
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" id="save" />
</div>
</div>
<div class="col-md-4">
@if ($projectTypes && count($projectTypes) > 1)
<h4 class="widgettitle title-light"><i class="fa-regular fa-rectangle-list"></i> Project Type</h4>
<p>The type of the project. This will determine which features are available.</p>
<select name="type">
@foreach ($projectTypes as $key => $type)
<option value="{{ $key }}"
@if ($project['type'] == $key) selected='selected' @endif
>{!! __($key) !!}</option>
@endforeach
</select>
<br /><br />
@endif
@dispatchEvent('beforeClientPicker', $project)
<div style="margin-bottom: 30px;">
<h4 class="widgettitle title-light tw-block"><span
class="fa fa-calendar"></span>{!! __('label.project_dates') !!}</h4>
<div>
<label>{!! __('label.project_start') !!}</label>
<div class="">
<input type="text" class="dates dateFrom" style="width:100px;" name="start" autocomplete="off"
value="{{ $project['start'] }}" placeholder="{{ __('language.dateformat') }}"/>
</div>
<label>{!! __('label.project_end') !!}</label>
<div class="">
<input type="text" class="dates dateTo" style="width:100px;" name="end" autocomplete="off"
value="{{ $project['end'] }}" placeholder="{{ __('language.dateformat') }}"/>
</div>
</div>
</div>
<div style="margin-bottom: 30px;">
<div class="">
<h4 class="widgettitle title-light"><span
class="fa fa-building"></span>{!! __('label.client_product') !!}</h4>
<select name="clientId" id="clientId">
@foreach ($clients as $row)
<option value="{{ $row['id'] }}"
@if ($project['clientId'] == $row['id']) selected=selected @endif
>{{ $row['name'] }}</option>
@endforeach
</select>
@if ($login::userIsAtLeast('manager'))
<br /><a href="{{ BASE_URL }}/clients/newClient" target="_blank">{!! __('label.client_not_listed') !!}</a>
@endif
</div>
</div>
<div style="margin-bottom: 30px;">
<div class="">
<h4 class="widgettitle title-light"><span
class="fa fa-lock-open"></span>{!! __('labels.defaultaccess') !!}</h4>
{!! __('text.who_can_access') !!}
<br /><br />
<select name="globalProjectUserAccess" style="max-width:300px;">
<option value="restricted" {{ $project['psettings'] == 'restricted' ? "selected='selected'" : '' }}>{!! __('labels.only_chose') !!}</option>
<option value="clients" {{ $project['psettings'] == 'clients' ? "selected='selected'" : '' }}>{!! __('labels.everyone_in_client') !!}</option>
<option value="all" {{ $project['psettings'] == 'all' ? "selected='selected'" : '' }}>{!! __('labels.everyone_in_org') !!}</option>
</select>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@once @push('scripts')
<script type="text/javascript">
jQuery(document).ready(function() {
jQuery("#projectdetails select").chosen();
leantime.dateController.initDateRangePicker(".dateFrom", ".dateTo", 2);
leantime.projectsController.initProjectTabs();
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initComplexEditor();
}
}
);
</script>
@endpush @endonce
@endsection

View File

@@ -0,0 +1,77 @@
@props([
'includeTitle' => true
])
@if ($includeTitle)
<h5 class="subtitle">Project Checklist <i class="fa fa-question-circle-o helperTooltip" data-tippy-content="The project checklist is list of activities you should do to ensure your projects are well defined, planned and executed."></i> </h5><br/>
@endif
<form name="progressForm" id="progressForm">
<div class="projectSteps">
<div class="progressWrapper">
<div class="progress">
<div
id="progressChecklistBar"
class="progress-bar progress-bar-success tx-transition"
role="progressbar"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
style="width: {{ $percentDone }}%"
><span class="sr-only">{{ $percentDone }}%</span></div>
</div>
@foreach ($progressSteps as $step)
<div class="step {{ $step['stepType'] }}" style="left: {{ $step['positionLeft'] }}%;">
<a href="javascript:void(0)" data-toggle="dropdown" class="dropdown-toggle" data-tippy-content="{{ __($step['description']) }}">
<span class="innerCircle"></span>
<span class="title">
@if ($step['status'] == 'done')
<i class="fa fa-circle-check"></i>
@else
<i class="fa-regular fa-circle"></i>
@endif
{{ __("text.step_".$loop->index + 1) }}: {{ __($step['title']) }}
<i class="fa fa-caret-down" aria-hidden="true"></i>
</span>
</a>
<ul class="dropdown-menu">
@foreach ($step['tasks'] as $key => $task)
<li @if ($task['status'] == 'done') class="done" @endif>
<input
type="checkbox"
name="{{ $key }}"
id="progress_{{ $key }}"
hx-patch="{{ BASE_URL }}/hx/projects/checklist/update-subtask/"
hx-target="#progressForm"
hx-swap="outerHTML"
@if ($task['status'] == 'done') checked @endif
@if (! in_array($step['stepType'], ['complete', 'current']))
disabled
@endif
/>
<label for="progress_{{ $key }}"
@if (! in_array($step['stepType'], ['complete', 'current']))
data-tippy-content="Finish the previous steps first"
@endif
>{{ __($task['title'] ?? '') }}</label>
<span class="clearall"></span>
<span class="taskDescription">
{{ __($task['description'] ?? '') }}<br />
<a href="{{ $task['link'] ?? '#' }}"><i class="fa fa-external-link"></i> Take me there</a>
</span>
</li>
@endforeach
</ul>
</div>
@endforeach
</div>
</div>
</form>

View File

@@ -0,0 +1,57 @@
@props([
'project' => [],
'type' => 'simple'
])
<div class="projectBox" id="projectBox-{{ $project['id'] }}">
<div class="row" >
<div class="col-md-12 fixed">
<div class="row tw-pb-sm">
<div class="col-md-10">
<a href="{{ BASE_URL }}/dashboard/show?projectId={{ $project['id'] }}">
<span class="projectAvatar">
@if(isset($projectTypeAvatars[$project["type"]]) && $projectTypeAvatars[$project["type"]] != "avatar")
<span class="{{ $projectTypeAvatars[$project["type"]] }}"></span>
@else
<img src='{{ BASE_URL }}/api/projects?projectAvatar={{ $project["id"] }}&v={{ format($project['modified'])->timestamp() }}' />
@endif
</span>
@if($project["clientName"] != '')
<small>{{ $project["clientName"] }}</small><br />
@else
<small>{{ __('projectType.'.$project["type"] ?? 'project') }}</small><br />
@endif
<strong>{{ $project['name'] }} <i class="fa-solid fa-up-right-from-square"></i></strong>
</a>
</div>
<div class="col-md-2 tw-text-right">
<a href="javascript:void(0);"
hx-patch="{{ BASE_URL }}/hx/projects/projectCard/toggleFavorite"
hx-vals='{"isFavorite": {{ $project['isFavorite'] }}, "projectId": {{ $project['id'] }}}'
hx-target="#projectBox-{{ $project['id'] }}"
onclick="jQuery(this).addClass('go')"
hx-swap="none"
hx-on::after-request="jQuery(this).removeClass('go')"
class="favoriteClick favoriteStar pull-right margin-right {{ $project['isFavorite'] ? 'isFavorite' : ''}} tw-mr-[5px]"
data-tippy-content="{{ __('label.favorite_tooltip') }}">
<i class="{{ $project['isFavorite'] ? 'fa-solid' : 'fa-regular' }} fa-star"></i>
</a>
</div>
</div>
@if($type != "simple")
<div id="projectProgressBox-{{ $project['id'] }}"
hx-get="{{ BASE_URL }}/hx/projects/projectCardProgress/getProgress?pId={{ $project['id'] }}"
hx-trigger="load"
hx-swap="innerHTML"
hx-target="#projectProgressBox-{{ $project['id'] }}"
hx-indicator=".htmx-indicator">
<div class="htmx-indicator">
<x-global::loadingText type="card" count="1" />
</div>
</div>
@endif
</div>
</div>
</div>

View File

@@ -0,0 +1,52 @@
@php( $percentDone = format($project['progress']['percent'])->decimal())
<div class="row">
<div class="col-md-7">
{{ __("subtitles.project_progress") }}
</div>
<div class="col-md-5" style="text-align:right">
{{ sprintf(__("text.percent_complete"), $percentDone) }}
</div>
</div>
<div class="progress">
<div class="progress-bar progress-bar-success"
role="progressbar"
aria-valuenow="{{ $percentDone }}"
aria-valuemin="0"
aria-valuemax="100"
style="width: {{ $percentDone }}%">
<span class="sr-only">{{ sprintf(__("text.percent_complete"), $percentDone) }}</span>
</div>
</div>
<div class="row">
<div class="col-md-12">
@if ($project['status'] !== null && $project['status'] != '')
<span class="label label-{{ $project['status'] }}">
{{ __("label.project_status_" . $project['status']) }}
</span><br />
@else
<span class="label label-grey">{{ __("label.no_status") }}</span><br />
@endif
</div>
</div>
<br />
<div class="row">
<div class="col-md-12">
<div class="team">
@foreach ($project['team'] as $member)
<div class="commentImage" style="margin-right:-10px;" data-tippy-content="{{ $member['firstname'] }} {{ $member['lastname'] }}">
<img
style=""
src="{{ BASE_URL }}/api/users?profileImage={{ $member['id'] }}&v={{ format($member['modified'])->timestamp() }}" data-tippy-content="{{ $member['firstname'] . ' ' . $member['lastname'] }}" />
</div>
@endforeach
</div>
<div class="clearall"></div>
</div>
</div>
<script>
// Idempotent init (see milestoneCard) — avoids re-instancing all tooltips
// every time a project progress bar renders.
window.leantime?.initTooltips?.();
</script>

View File

@@ -0,0 +1,99 @@
<div id="myProjectsHub"
hx-get="{{BASE_URL}}/projects/projectHubProjects/get"
hx-trigger="HTMX.updateProjectList from:body"
hx-target="#myProjectsHub"
hx-swap="outerHTML">
@if (count($clients) > 0)
<div class="dropdown dropdownWrapper pull-right">
<a href="javascript:void(0)" class="btn btn-default dropdown-toggle header-title-dropdown" data-toggle="dropdown">
@if ($currentClientName != '')
{{ $currentClientName }}
@else
{{ __("headline.all_clients") }}
@endif
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu">
<li><a href="{{ BASE_URL }}/projects/showMy">{{ __("headline.all_clients") }}</a></li>
@foreach ($clients as $key => $value)
@if(! empty($key))
<li>
<a href="javascript:void(0);"
hx-get="{{BASE_URL}}/projects/projectHubProjects/get?client={{ $key }}"
hx-target="#myProjectsHub"
hx-swap="outerHTML">{{ $value['name'] }}</a>
</li>
@endif
@endforeach
</ul>
</div>
@endif
@if (count($allProjects) == 0)
<br /><br />
<div class='center'>
<div style='width:70%; color:var(--main-action-color)' class='svgContainer'>
{{ __('notifications.not_assigned_to_any_project') }}
@if($login::userIsAtLeast($roles::$manager))
<br />
<x-global::forms.button tag="a" link="{{ BASE_URL }}/projects/newProject" contentRole="primary">{{ __('link.new_project') }}</x-global::forms.button>
@endif
</div>
</div>
@endif
<x-global::accordion id="myProjectsHub-favorites" class="noBackground">
<x-slot name="title">
My Favorites
</x-slot>
<x-slot name="content">
<div class="row">
@php
$hasFavorites = false;
@endphp
@foreach ($allProjects as $project)
@if($project['isFavorite'] == true)
<div class="col-md-4">
@include("projects::partials.projectCard", ["project" => $project, "type" => "detailed"])
</div>
@php
$hasFavorites = true;
@endphp
@endif
@endforeach
@if($hasFavorites === false)
<div style="color:var(--main-action-color)">
You don't have any favorites. 😿
</div>
@endif
</div>
</x-slot>
</x-global::accordion>
<x-global::accordion id="myProjectsHub-otherProjects" class="noBackground">
<x-slot name="title">
🗂️ All Assigned Projects
</x-slot>
<x-slot name="content">
<div class="row">
@foreach ($allProjects as $project)
@if($project['isFavorite'] == false)
<div class="col-md-3">
@include("projects::partials.projectCard", ["project" => $project, "type" => "detailed"])
</div>
@endif
@endforeach
</div>
</x-slot>
</x-global::accordion>
</div>

View File

@@ -0,0 +1,129 @@
@extends($layout)
@section('content')
@props([
'includeTitle' => true,
'allProjects' => []
])
<div class="maincontent" style="margin-top:0px">
<div style="padding:10px 0px">
<div class="center">
<span style="font-size:38px; color:var(--main-titles-color););">
{{ __("headline.project_hub") }}
</span><br />
<span style="font-size:18px; color:var(--main-titles-color););">
{{ __("text.project_hub_intro") }}
@if ($login::userIsAtLeast("manager"))
<br /><br /><x-global::forms.button tag="a" link="#/projects/createnew" contentRole="default">{!! __("menu.create_something_new") !!}</x-global::forms.button>
@endif
</span>
<br />
<br />
</div>
</div>
@if(is_array($allProjects) && count($allProjects) == 0)
<x-global::undrawSvg image="undraw_a_moment_to_relax_bbpa.svg" style="color:var(--main-titles-color);" maxWidth="30%">
</x-global::undrawSvg>
@endif
<div id="myProjectsHub"
hx-get="{{BASE_URL}}/projects/projectHubProjects/get"
hx-trigger="HTMX.updateProjectList from:body"
hx-target="#myProjectsHub"
hx-swap="outerHTML">
@if (count($clients) > 0)
<div class="dropdown dropdownWrapper pull-right">
<a href="javascript:void(0)" class="btn btn-default dropdown-toggle header-title-dropdown" data-toggle="dropdown">
@if ($currentClientName != '')
{{ $currentClientName }}
@else
{{ __("headline.all_clients") }}
@endif
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu">
<li><a href="{{ CURRENT_URL }}">{{ __("headline.all_clients") }}</a></li>
@foreach ($clients as $key => $value)
<li>
<a href="javascript:void(0);"
hx-get="{{BASE_URL}}/projects/projectHubProjects/get?client={{ $key }}"
hx-target="#myProjectsHub"
hx-swap="outerHTML">{{ $value['name'] }}</a>
</li>
@endforeach
</ul>
</div>
@endif
@if (count($allProjects) == 0)
<br /><br />
<div class='center'>
<div style='width:70%; color:var(--main-titles-color)' class='svgContainer'>
{{ __('notifications.not_assigned_to_any_project') }}
@if($login::userIsAtLeast($roles::$manager))
<br /><br />
<x-global::forms.button tag="a" link="{{ BASE_URL }}/projects/newProject" contentRole="primary">{{ __('link.new_project') }}</x-global::forms.button>
@endif
</div>
</div>
@endif
<x-global::accordion id="myProjectsHub-favorites" class="noBackground">
<x-slot name="title">
My Favorites
</x-slot>
<x-slot name="content">
<div class="row">
@php
$hasFavorites = false;
@endphp
@foreach ($allProjects as $project)
@if($project['isFavorite'] == true)
<div class="col-md-4">
@include("projects::partials.projectCard", ["project" => $project, "type" => "detailed"])
</div>
@php
$hasFavorites = true;
@endphp
@endif
@endforeach
@if($hasFavorites === false)
<div style="color:var(--main-titles-color)">
{{ __("text.no_favorites") }}
</div>
@endif
</div>
</x-slot>
</x-global::accordion>
<x-global::accordion id="myProjectsHub-otherProjects" class="noBackground">
<x-slot name="title">
{{ __("text.all_assigned_projects") }}
</x-slot>
<x-slot name="content">
<div class="row">
@foreach ($allProjects as $project)
@if($project['isFavorite'] == false)
<div class="col-md-3">
@include("projects::partials.projectCard", ["project" => $project, "type" => "detailed"])
</div>
@endif
@endforeach
</div>
</x-slot>
</x-global::accordion>
</div>
</div>
@endsection

View File

@@ -0,0 +1,222 @@
@extends($layout)
@section('content')
@php
$showClosedProjects = $showClosedProjects ?? false;
@endphp
<div class="pageheader">
<div class="pageicon"><span class="fa fa-suitcase"></span></div>
<div class="pagetitle">
<h5>{!! __('label.administration') !!}</h5>
<h1>{!! __('headline.all_projects') !!}</h1>
</div>
</div><!--pageheader-->
<div class="maincontent">
<div class="maincontentinner">
{!! $tpl->displayNotification() !!}
<div class="pull-right">
<form action="" method="post">
<input type="hidden" name="hideClosedProjects" value="1" />
<input type="checkbox" name="showClosedProjects" onclick="form.submit();" id="showClosed" @if ($showClosedProjects) checked='checked' @endif />&nbsp;<label for="showClosed" class="pull-right">Show Closed Projects</label>
</form>
</div>
<x-global::forms.button tag="a" link="{{ BASE_URL }}/projects/newProject" contentRole="primary"><i class='fa fa-plus'></i> {!! __('link.new_project') !!}</x-global::forms.button>
{{-- 视图切换:甘特图 / 列表 --}}
<div class="btn-group pull-right" style="margin-left:8px;">
<button type="button" class="btn btn-default active" id="btnViewGantt"><i class="fa fa-chart-gantt"></i> 甘特图</button>
<button type="button" class="btn btn-default" id="btnViewList"><i class="fa fa-list"></i> 列表</button>
</div>
<div class="clearall"></div>
{{-- 甘特图视图(默认显示,页面加载即初始化) --}}
<div id="ganttView" style="margin-top:16px;">
<div class="gantt-wrapper" style="position:relative;min-height:420px;">
<svg id="gantt" style="min-height:420px;min-width:700px;width:100%;display:block;"></svg>
</div>
<div id="ganttError" class="alert alert-danger" style="display:none;margin-top:8px;"></div>
</div>
{{-- 列表视图(默认隐藏) --}}
<div id="listView" style="display:none;">
<table class="table table-bordered" cellpadding="0" cellspacing="0" border="0" id="allProjectsTable">
<colgroup>
<col class="con1"/>
<col class="con0" />
<col class="con1"/>
<col class="con0" />
<col class="con1"/>
<col class="con0"/>
<col class="con0"/>
</colgroup>
<thead>
<tr>
<th class="head0">{!! __('label.project_name') !!}</th>
<th class="head1">{!! __('label.client_product') !!}</th>
<th class="head1">{!! __('label.project_type') !!}</th>
<th class="head0">{!! __('label.project_state') !!}</th>
<th class="head0">{!! __('label.hourly_budget') !!}</th>
<th class="head1">{!! __('label.budget_cost') !!}</th>
<th class="head1" style="width:40px;"></th>
</tr>
</thead>
<tbody>
@foreach ($allProjects as $row)
<tr class='gradeA'>
<td style="padding:6px;">
<a class="" href="{{ BASE_URL }}/projects/showProject/{{ $row['id'] }}">{{ $row['name'] }}</a>
</td>
<td>
<a class="" href="{{ BASE_URL }}/clients/showClient/{{ $row['clientId'] }}">{{ $row['clientName'] }}</a>
</td>
<td> {{ $row['type'] }} </td>
<td>
@if ($row['state'] == -1)
{!! __('label.closed') !!}
@else
{!! __('label.open') !!}
@endif
</td>
<td class="center">{{ $row['hourBudget'] }}</td>
<td class="center">{{ $row['dollarBudget'] }}</td>
<td class="center">
<button type="button" class="btn btn-xs btn-default btnToggleProject" data-project-id="{{ $row['id'] }}">
<i class="fa fa-chevron-down"></i>
</button>
</td>
</tr>
<tr class="projectDetailRow" id="projectDetail-{{ $row['id'] }}" style="display:none;">
<td colspan="7" style="padding:12px;background:#f9f9f9;">
<div style="display:flex;gap:16px;flex-wrap:wrap;">
<div style="flex:1;min-width:220px;">
<h5 style="margin-top:0;"><i class="fa fa-link"></i> 关联主数据</h5>
@if (empty($row['linkedMasters']))
<em class="text-muted">暂无关联主数据</em>
@else
<table class="table table-condensed table-bordered" style="margin-bottom:0;background:#fff;">
<thead>
<tr><th>类型</th><th>编号</th><th>名称</th><th>操作</th></tr>
</thead>
<tbody>
@foreach ($row['linkedMasters'] as $m)
<tr>
<td><span class="label label-info">{{ $m['typeName'] }}</span></td>
<td>{{ $m['bomNo'] ?? '' }}</td>
<td>{{ $m['productName'] ?? '' }}</td>
<td><a class="btn btn-xs btn-default" href="{{ BASE_URL }}/bom/refs/{{ $m['refId'] }}">查看</a></td>
</tr>
@endforeach
</tbody>
</table>
@endif
</div>
<div style="min-width:180px;">
<h5 style="margin-top:0;"><i class="fa fa-cog"></i> 快捷操作</h5>
<a class="btn btn-xs btn-primary" href="{{ BASE_URL }}/projects/showProject/{{ $row['id'] }}#masterdata">
<i class="fa fa-link"></i> 关联主数据
</a>
</div>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>{{-- /#listView --}}
</div>
</div>
@once @push('scripts')
<script type="text/javascript">
jQuery(document).ready(function() {
leantime.projectsController.initProjectTable();
// 点击展开按钮,切换项目关联主数据详情行
jQuery('.btnToggleProject').on('click', function () {
var projectId = jQuery(this).data('project-id');
var row = jQuery('#projectDetail-' + projectId);
var icon = jQuery(this).find('i');
if (row.is(':visible')) {
row.hide();
icon.removeClass('fa-chevron-up').addClass('fa-chevron-down');
} else {
row.show();
icon.removeClass('fa-chevron-down').addClass('fa-chevron-up');
}
});
// ---- 甘特图 / 列表 视图切换(甘特图默认显示,加载即初始化) ----
var ganttTasks = {!! json_encode($ganttTasks ?? [], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) !!};
function renderGantt() {
if (!ganttTasks || !ganttTasks.length) {
jQuery('#gantt').html('<div class="alert alert-info" style="margin:20px;">暂无项目数据</div>');
return;
}
try {
if (typeof Gantt === 'undefined') {
jQuery('#ganttError').text('甘特图组件未加载Gantt undefined请强制刷新 Ctrl+Shift+R').show();
return;
}
new Gantt('#gantt', ganttTasks, {
readonlyGantt: true,
resizing: false,
progress: false,
is_draggable: false,
view_modes: ['Day', 'Week', 'Month'],
view_mode: 'Month',
bar_height: 40,
header_height: 55,
column_width: 30,
step: 24,
padding: 20,
bar_corner_radius: 10,
arrow_curve: 10,
language: 'en'
});
} catch (e) {
jQuery('#ganttError').text('甘特图渲染失败:' + e.message).show();
}
}
// 甘特图组件是 defer 加载,用 load 事件确保已就绪后再渲染
if (document.readyState === 'complete') {
renderGantt();
} else {
window.addEventListener('load', renderGantt);
}
jQuery('#btnViewGantt').on('click', function () {
jQuery('#ganttView').show();
jQuery('#listView').hide();
jQuery('#btnViewGantt').addClass('active');
jQuery('#btnViewList').removeClass('active');
});
jQuery('#btnViewList').on('click', function () {
jQuery('#ganttView').hide();
jQuery('#listView').show();
jQuery('#btnViewList').addClass('active');
jQuery('#btnViewGantt').removeClass('active');
});
});
</script>
@endpush @endonce
@endsection

View File

@@ -0,0 +1,584 @@
@extends($layout)
@section('content')
@php
$state = $state ?? null;
@endphp
<div class="pageheader">
<div class="pageicon"><span class="fa fa-suitcase"></span></div>
<div class="pagetitle">
<h5>{!! __('label.administration') !!}</h5>
<h1>{!! sprintf(__('headline.project'), $tpl->escape($project['name'])) !!}
</h1>
</div>
</div><!--pageheader-->
<div class="maincontent">
<div class="maincontentinner">
{!! $tpl->displayNotification() !!}
<div class="inlineDropDownContainer" style="float:right; z-index:9; padding-top:2px;">
<x-global::forms.button tag="a" link="{{ BASE_URL }}/projects/duplicateProject/{{ $project['id'] }}" contentRole="default" class="duplicateProjectModal" data-tippy-content="{{ __('link.duplicate_project') }}"><i class="fa-regular fa-copy"></i> Copy</x-global::forms.button>
<x-global::forms.button tag="a" state="danger" variant="outline" class="delete" link="{{ BASE_URL }}/projects/delProject/{{ $project['id'] }}" data-tippy-content="{{ __('link.delete_project') }}"><i class="fa fa-trash"></i> Delete</x-global::forms.button>
</div>
<div class="tabbedwidget tab-primary projectTabs">
<ul>
<li><a href="#projectdetails"><span class="fa fa-leaf"></span> {!! __('tabs.projectdetails') !!}</a></li>
<li><a href="#team"><span class="fa fa-group"></span> {!! __('tabs.team') !!}</a></li>
<li><a href="#integrations"> <span class="fa fa-asterisk"></span> {!! __('tabs.Integrations') !!}</a></li>
<li><a href="#todosettings"><span class="fa fa-list-ul"></span> {!! __('tabs.todosettings') !!}</a></li>
<li><a href="#masterdata"><span class="fa fa-link"></span> 主数据</a></li>
@dispatchEvent('projectTabsList')
</ul>
<div id="projectdetails">
@include('projects::submodules.projectDetails')
</div>
<div id="team">
<form method="post" action="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}#team">
<input type="hidden" name="saveUsers" value="1" />
<div class="row-fluid">
<div class="span12">
<div class="form-group">
<br />{!! __('text.choose_access_for_users') !!}<br />
<br />
<div class="row">
<div class="col-md-12">
<h4 class="widgettitle title-light">
<span class="fa fa-users"></span>{!! __('headlines.team_member') !!}
</h4>
</div>
</div>
<div class="row">
@foreach ($project['assignedUsers'] as $userId => $assignedUser)
<div class="col-md-4">
<div class="userBox">
<input type='checkbox' name='editorId[]' id="user-{{ $assignedUser['id'] }}" value='{{ $assignedUser['id'] }}'
checked="checked"
/>
<div class="commentImage">
<img src="{{ BASE_URL }}/api/users?profileImage={{ $assignedUser['id'] }}&v={{ format($assignedUser['modified'])->timestamp() }}"/>
</div>
<label for="user-{{ $assignedUser['id'] }}" >{!! sprintf(__('text.full_name'), $tpl->escape($assignedUser['firstname']), $tpl->escape($assignedUser['lastname'])) !!}
@if ($assignedUser['jobTitle'] != '')
<small>
{{ $assignedUser['jobTitle'] }}
</small>
<br/>
@endif
@if ($assignedUser['source'] == 'api')
<small>
API Access
</small>
<br/>
@endif
@if ($assignedUser['status'] == 'i')
<small>{!! __('label.invited') !!}</small>
@endif
</label>
@php
if (($roles::getRoles()[$assignedUser['role']] == $roles::$admin || $roles::getRoles()[$assignedUser['role']] == $roles::$owner)) {
@endphp
<x-global::forms.text-input readonly disabled value="{{ __('label.roles.'.$roles::getRoles()[$assignedUser['role']]) }}" />
@php
} else {
@endphp
<select name="userProjectRole-{{ $assignedUser['id'] }}">
<option value="inherit">Inherit</option>
<option value="{{ array_search($roles::$readonly, $roles::getRoles()) }}"
@if ($assignedUser['projectRole'] == array_search($roles::$readonly, $roles::getRoles())) selected='selected' @endif
>{!! __('label.roles.'.$roles::$readonly) !!}</option>
<option value="{{ array_search($roles::$commenter, $roles::getRoles()) }}"
@if ($assignedUser['projectRole'] == array_search($roles::$commenter, $roles::getRoles())) selected='selected' @endif
>{!! __('label.roles.'.$roles::$commenter) !!}</option>
<option value="{{ array_search($roles::$editor, $roles::getRoles()) }}"
@if ($assignedUser['projectRole'] == array_search($roles::$editor, $roles::getRoles())) selected='selected' @endif
>{!! __('label.roles.'.$roles::$editor) !!}</option>
<option value="{{ array_search($roles::$manager, $roles::getRoles()) }}"
@if ($assignedUser['projectRole'] == array_search($roles::$manager, $roles::getRoles())) selected='selected' @endif
>{!! __('label.roles.'.$roles::$manager) !!}</option>
</select>
@php } @endphp
<div class="clearall"></div>
</div>
</div>
@endforeach
</div>
` <div class="row">
<div class="col-md-12">
<h4 class="widgettitle title-light">
<span class="fa fa-user-friends "></span>{!! __('headlines.assign_users_to_project') !!}
</h4>
</div>
</div>
<div class="row">
@foreach ($availableUsers as $row)
@if (collect($project['assignedUsers'])->where('id', $row['id'])->isEmpty())
<div class="col-md-4">
<div class="userBox">
<input type='checkbox' name='editorId[]' id="user-{{ $row['id'] }}" value='{{ $row['id'] }}' />
<div class="commentImage">
<img src="{{ BASE_URL }}/api/users?profileImage={{ $row['id'] }}&v={{ format($row['modified'])->timestamp() }}"/>
</div>
<label for="user-{{ $row['id'] }}" >{!! sprintf(__('text.full_name'), $tpl->escape($row['firstname']), $tpl->escape($row['lastname'])) !!}</label>
@if ($roles::getRoles()[$row['role']] == $roles::$admin || $roles::getRoles()[$row['role']] == $roles::$owner)
<x-global::forms.text-input readonly disabled value="{{ __('label.roles.'.$roles::getRoles()[$row['role']]) }}" />
@else
@php $assignedUserMatch = collect($project['assignedUsers'])->where('id', $row['id'])->first(); @endphp
<select name="userProjectRole-{{ $row['id'] }}">
<option value="inherit">Inherit</option>
<option value="{{ array_search($roles::$readonly, $roles::getRoles()) }}"
@if ($assignedUserMatch && $assignedUserMatch['projectRole'] == array_search($roles::$readonly, $roles::getRoles())) selected='selected' @endif
>{!! __('label.roles.'.$roles::$readonly) !!}</option>
<option value="{{ array_search($roles::$commenter, $roles::getRoles()) }}"
@if ($assignedUserMatch && $assignedUserMatch['projectRole'] == array_search($roles::$commenter, $roles::getRoles())) selected='selected' @endif
>{!! __('label.roles.'.$roles::$commenter) !!}</option>
<option value="{{ array_search($roles::$editor, $roles::getRoles()) }}"
@if ($assignedUserMatch && $assignedUserMatch['projectRole'] == array_search($roles::$editor, $roles::getRoles())) selected='selected' @endif
>{!! __('label.roles.'.$roles::$editor) !!}</option>
<option value="{{ array_search($roles::$manager, $roles::getRoles()) }}"
@if ($assignedUserMatch && $assignedUserMatch['projectRole'] == array_search($roles::$manager, $roles::getRoles())) selected='selected' @endif
>{!! __('label.roles.'.$roles::$manager) !!}</option>
</select>
@endif
<div class="clearall"></div>
</div>
</div>
@endif
@endforeach
@if ($login::userIsAtLeast($roles::$manager))
<div class="col-md-4">
<div class="userBox">
<a class="userEditModal" href="{{ BASE_URL }}/users/newUser?preSelectProjectId={{ $project['id'] }}" style="font-size:var(--font-size-l); line-height:61px"><span class="fa fa-user-plus"></span> {!! __('links.create_user') !!}</a>
<div class="clearall"></div>
</div>
</div>
@endif
</div>
<div class="row">
<div class="col-md-12">
</div>
</div>
</div>
</div>
</div>
<br/>
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="saveUsers" id="save" />
</form>
</div>
<div id="integrations">
@if ($projectMuteCount > 0)
<div class="alert alert-info" style="margin-bottom: 20px;">
<i class="fa fa-bell-slash"></i>
{!! sprintf(__('label.project_mute_count'), $projectMuteCount) !!}
</div>
@endif
<h4 class="widgettitle title-light"><span class="fa fa-leaf"></span>Mattermost</h4>
<div class="row">
<div class="col-md-3">
<img src="{{ BASE_URL }}/dist/images/mattermost-logoHorizontal.png" width="200" />
</div>
<div class="col-md-5">
{!! __('text.mattermost_instructions') !!}
</div>
<div class="col-md-4">
<strong>{!! __('label.webhook_url') !!}</strong><br />
<form action="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}#integrations" method="post">
<x-global::forms.text-input name="mattermostWebhookURL" id="mattermostWebhookURL" value="{{ e($mattermostWebhookURL) }}" />
<br />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="mattermostSave" />
</form>
</div>
</div>
<br />
<h4 class="widgettitle title-light"><span class="fa fa-leaf"></span>Slack</h4>
<div class="row">
<div class="col-md-3">
<img src="https://cdn.cdnlogo.com/logos/s/52/slack.svg" width="200"/>
</div>
<div class="col-md-5">
{!! __('text.slack_instructions') !!}
</div>
<div class="col-md-4">
<strong>{!! __('label.webhook_url') !!}</strong><br />
<form action="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}#integrations" method="post">
<x-global::forms.text-input name="slackWebhookURL" id="slackWebhookURL" value="{{ e($slackWebhookURL) }}" />
<br />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="slackSave" />
</form>
</div>
</div>
<h4 class="widgettitle title-light"><span class="fa fa-leaf"></span>Zulip</h4>
<div class="row">
<div class="col-md-3">
<img src="{{ BASE_URL }}/dist/images/zulip-org-logo.png" width="200"/>
</div>
<div class="col-md-5">
{!! __('text.zulip_instructions') !!}
</div>
<div class="col-md-4">
<form action="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}#integrations" method="post">
<strong>{!! __('label.base_url') !!}</strong><br />
<x-global::forms.text-input name="zulipURL" id="zulipURL" placeholder="{{ __('input.placeholders.zulip_url') }}" value="{{ $zulipHook['zulipURL'] }}" />
<br />
<strong>{!! __('label.bot_email') !!}</strong><br />
<x-global::forms.text-input name="zulipEmail" id="zulipEmail" placeholder="" value="{{ $tpl->escape($zulipHook['zulipEmail']) }}" />
<br />
<strong>{!! __('label.botkey') !!}</strong><br />
<x-global::forms.text-input name="zulipBotKey" id="zulipBotKey" placeholder="" value="{{ $tpl->escape($zulipHook['zulipBotKey']) }}" />
<br />
<strong>{!! __('label.stream') !!}</strong><br />
<x-global::forms.text-input name="zulipStream" id="zulipStream" placeholder="" value="{{ $tpl->escape($zulipHook['zulipStream']) }}" />
<br />
<strong>{!! __('label.topic') !!}</strong><br />
<x-global::forms.text-input name="zulipTopic" id="zulipTopic" placeholder="" value="{{ $tpl->escape($zulipHook['zulipTopic']) }}" />
<br />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="zulipSave" />
</form>
</div>
</div>
<h4 class="widgettitle title-light"><span class="fa fa-leaf"></span>Telegram</h4>
<div class="row">
<div class="col-md-3">
<img src="{{ BASE_URL }}/dist/images/telegram-logo.png" width="130" alt="Telegram logo" />
</div>
<div class="col-md-5">
{!! __('text.telegram_instructions') !!}
</div>
<div class="col-md-4">
<form action="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}#integrations" method="post">
<strong>{!! __('label.botkey') !!}</strong><br />
<x-global::forms.text-input name="telegramBotToken" id="telegramBotToken" placeholder="" value="{{ $tpl->escape($telegramHook['telegramBotToken']) }}" />
<br />
<strong>{!! __('label.telegram_chat_id') !!}</strong><br />
<x-global::forms.text-input name="telegramChatId" id="telegramChatId" placeholder="{{ __('input.placeholders.telegram_chat_id') }}" value="{{ $tpl->escape($telegramHook['telegramChatId']) }}" />
<br />
<strong>{!! __('label.telegram_topic_id') !!}</strong><br />
<x-global::forms.text-input name="telegramTopicId" id="telegramTopicId" placeholder="{{ __('input.placeholders.telegram_topic_id') }}" value="{{ $tpl->escape($telegramHook['telegramTopicId']) }}" />
<br />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="telegramSave" />
</form>
</div>
</div>
{{-- Slack webhook --}}
<h4 class='widgettitle title-light'><span class='fa fa-leaf'></span>Discord</h4>
<div class='row'>
<div class='col-md-3'>
<img src='{{ BASE_URL }}/dist/images/discord-logo.png' width='200'/>
</div>
<div class='col-md-5'>
{!! __('text.discord_instructions') !!}
</div>
<div class="col-md-4">
<strong>{!! __('label.webhook_url') !!}</strong><br/>
<form action="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}#integrations" method="post">
@for ($i = 1; $i <= 3; $i++)
@php $discordVarName = 'discordWebhookURL'.$i; $discordVarVal = $$discordVarName ?? ''; @endphp
<input type="text" name="discordWebhookURL{{ $i }}" id="discordWebhookURL{{ $i }}" placeholder="{{ __('input.placeholders.discord_url') }}" value="{{ e($discordVarVal) }}"/><br/>
@endfor
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="discordSave" />
</form>
</div>
</div>
</div>
<div id="todosettings">
<form action="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}#todosettings" method="post">
<ul class="sortableTicketList" id="todoStatusList">
@foreach ($todoStatus as $key => $ticketStatus)
<li>
<div class="ticketBox">
<div class="row statusList" id="todostatus-{{ $key }}">
<input type="hidden" name="labelKeys[]" id="labelKey-{{ $key }}" class='labelKey' value="{{ $key }}"/>
<div class="sortHandle">
<br />
<span class="fa fa-sort"></span>
</div>
<div class="col-md-1">
<label>{!! __('label.sortindex') !!}</label>
<input type="text" name="labelSort-{{ $key }}" class="sorter" id="labelSort-{{ $key }}" value="{{ $ticketStatus['sortKey'] }}" style="width:50px;"/>
</div>
<div class="col-md-2">
<label>{!! __('label.label') !!}</label>
<input type="text" name="label-{{ $key }}" {{ $key == -1 ? 'readonly' : '' }} id="label-{{ $key }}" value="{{ $ticketStatus['name'] }}" />
</div>
<div class="col-md-2">
<label>{!! __('label.color') !!}</label>
<select name="labelClass-{{ $key }}" id="labelClass-{{ $key }}" class="colorChosen">
<option value="label-purple" class="label-purple" {{ $ticketStatus['class'] == 'label-purple' ? 'selected="selected"' : '' }}><span class="label-purple">{!! __('label.purple') !!}</span></option>
<option value="label-pink" class="label-pink" {{ $ticketStatus['class'] == 'label-pink' ? 'selected="selected"' : '' }}><span class="label-pink">{!! __('label.pink') !!}</span></option>
<option value="label-darker-blue" class="label-darker-blue" {{ $ticketStatus['class'] == 'label-darker-blue' ? 'selected="selected"' : '' }}><span class="label-darker-blue">{!! __('label.darker-blue') !!}</span></option>
<option value="label-info" class="label-info" {{ $ticketStatus['class'] == 'label-info' ? 'selected="selected"' : '' }}><span class="label-info">{!! __('label.dark-blue') !!}</span></option>
<option value="label-blue" class="label-blue" {{ $ticketStatus['class'] == 'label-blue' ? 'selected="selected"' : '' }}><span class="label-blue">{!! __('label.blue') !!}</span></option>
<option value="label-dark-green" class="label-dark-green" {{ $ticketStatus['class'] == 'label-dark-green' ? 'selected="selected"' : '' }}><span class="label-dark-green">{!! __('label.dark-green') !!}</span></option>
<option value="label-success" class="label-success" {{ $ticketStatus['class'] == 'label-success' ? 'selected="selected"' : '' }}><span class="label-success">{!! __('label.green') !!}</span></option>
<option value="label-warning" class="label-warning" {{ $ticketStatus['class'] == 'label-warning' ? 'selected="selected"' : '' }}><span class="label-warning">{!! __('label.yellow') !!}</span></option>
<option value="label-brown" class="label-brown" {{ $ticketStatus['class'] == 'label-brown' ? 'selected="selected"' : '' }}><span class="label-brown">{!! __('label.brown') !!}</span></option>
<option value="label-danger" class="label-danger" {{ $ticketStatus['class'] == 'label-danger' ? 'selected="selected"' : '' }}><span class="label-danger">{!! __('label.dark-red') !!}</span></option>
<option value="label-important" class="label-important" {{ $ticketStatus['class'] == 'label-important' ? 'selected="selected"' : '' }}><span class="label-important">{!! __('label.red') !!}</span></option>
<option value="label-default" class="label-default" {{ $ticketStatus['class'] == 'label-default' ? 'selected="selected"' : '' }}><span class="label-default">{!! __('label.grey') !!}</span></option>
</select>
</div>
<div class="col-md-2">
<label>{!! __('label.reportType') !!}</label>
<select name="labelType-{{ $key }}" id="labelType-{{ $key }}">
<option value="NEW" {{ ($ticketStatus['statusType'] == 'NEW') ? 'selected="selected"' : '' }}>{!! __('status.new') !!}</option>
<option value="INPROGRESS" {{ ($ticketStatus['statusType'] == 'INPROGRESS') ? 'selected="selected"' : '' }}>{!! __('status.in_progress') !!}</option>
<option value="DONE" {{ ($ticketStatus['statusType'] == 'DONE') ? 'selected="selected"' : '' }}>{!! __('status.done') !!}</option>
<option value="NONE" {{ ($ticketStatus['statusType'] == 'NONE') ? 'selected="selected"' : '' }}>{!! __('status.dont_report') !!}</option>
</select>
</div>
<div class="col-md-2">
<label for="">{!! __('label.showInKanban') !!}</label>
<input type="checkbox" name="labelKanbanCol-{{ $key }}" id="labelKanbanCol-{{ $key }}" {{ $ticketStatus['kanbanCol'] ? 'checked="checked"' : '' }}/>
</div>
<div class="remove">
<br />
@if ($key != -1)
<a href="javascript:void(0);" onclick="leantime.projectsController.removeStatus({{ $key }})" class="delete"><span class="fa fa-trash"></span></a>
@endif
</div>
</div>
@if ($key == -1)
<em>* the archive status is protected cannot be renamed or removed.</em>
@endif
</div>
</li>
@endforeach
</ul>
<a href="javascript:void(0);" onclick="leantime.projectsController.addToDoStatus();" class="quickAddLink" style="text-align:left;">{!! __('links.add_status') !!}</a>
<br />
<x-global::forms.button tag="input" inputType="submit" :labelText="__('buttons.save')" name="submitSettings" contentRole="primary"/>
</form>
</div>
<div id="masterdata">
<h4 class="widgettitle title-light"><span class="fa fa-link"></span> 关联全局主数据</h4>
<p class="text-muted">关联 BOM / 工艺文件 / 工具清单后,可在项目里追加项目级列(日期、库存、采购数量等),不影响全局主数据。</p>
@if (empty($linkedMasters))
<div class="alert alert-info">本项目尚未关联任何主数据。</div>
@else
<div class="table-responsive" style="margin-bottom:20px;">
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>类型</th>
<th>编号</th>
<th>名称</th>
<th>版本</th>
<th style="width:180px;">操作</th>
</tr>
</thead>
<tbody>
@foreach ($linkedMasters as $ref)
@php $m = $ref['master'] ?? []; $typeNames = ['bom'=>'BOM','process'=>'工艺文件','tooling'=>'工具清单']; @endphp
<tr>
<td><span class="label label-info">{{ $typeNames[$m['type'] ?? 'bom'] ?? 'BOM' }}</span></td>
<td>{{ $m['bomNo'] ?? '' }}</td>
<td>{{ $m['productName'] ?? '' }}</td>
<td>{{ $m['version'] ?? '' }}</td>
<td>
<a class="btn btn-xs btn-default" href="{{ BASE_URL }}/bom/refs/{{ $ref['id'] }}">打开</a>
<button class="btn btn-xs btn-danger btnUnlinkMaster" data-ref-id="{{ $ref['id'] }}">取消关联</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@if (! empty($availableMasters))
<form method="post" action="{{ BASE_URL }}/projects/showProject/{{ $project['id'] }}#masterdata">
<input type="hidden" name="linkMaster" value="1" />
<div class="row">
<div class="col-md-5">
<select name="masterId" class="form-control">
@foreach ($availableMasters as $m)
<option value="{{ $m['id'] }}">{{ $m['typeName'] }} {{ $m['bomNo'] ?? '' }} {{ $m['productName'] ?? '' }}</option>
@endforeach
</select>
</div>
<div class="col-md-2">
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" labelText="关联主数据" name="linkMasterSubmit" />
</div>
</div>
</form>
@else
<div class="alert alert-warning">没有可关联的全局主数据。请先到「BOM / 工艺文件 / 工具清单」创建主数据。</div>
@endif
</div>
@dispatchEvent('projectTabsContent')
</div>
</div>
</div>
<!-- New Status Template -->
<div class="newStatusTpl" style="display:none;">
<div class="ticketBox">
<div class="row statusList" id="todostatus-XXNEWKEYXX">
<input type="hidden" name="labelKeys[]" id="labelKey-XXNEWKEYXX" class='labelKey' value="XXNEWKEYXX"/>
<div class="sortHandle">
<br />
<span class="fa fa-sort"></span>
</div>
<div class="col-md-1">
<label>{!! __('label.sortindex') !!}</label>
<input type="text" name="labelSort-XXNEWKEYXX" class="sorter" id="labelSort-XXNEWKEYXX" value="" style="width:50px;"/>
</div>
<div class="col-md-2">
<label>{!! __('label.label') !!}</label>
<input type="text" name="label-XXNEWKEYXX" id="label-XXNEWKEYXX" value="" />
</div>
<div class="col-md-2">
<label>{!! __('label.color') !!}</label>
<select name="labelClass-XXNEWKEYXX" id="labelClass-XXNEWKEYXX" class="colorChosen">
<option value="label-blue" class="label-blue"><span class="label-blue">{!! __('label.blue') !!}</span></option>
<option value="label-info" class="label-info"><span class="label-info">{!! __('label.dark-blue') !!}</span></option>
<option value="label-darker-blue" class="label-darker-blue"><span class="label-darker-blue">{!! __('label.darker-blue') !!}</span></option>
<option value="label-warning" class="label-warning"><span class="label-warning">{!! __('label.yellow') !!}</span></option>
<option value="label-success" class="label-success"><span class="label-success">{!! __('label.green') !!}</span></option>
<option value="label-dark-green" class="label-dark-green"><span class="label-dark-green">{!! __('label.dark-green') !!}</span></option>
<option value="label-important" class="label-important"><span class="label-important">{!! __('label.red') !!}</span></option>
<option value="label-danger" class="label-danger"><span class="label-danger">{!! __('label.dark-red') !!}</span></option>
<option value="label-pink" class="label-pink"><span class="label-pink">{!! __('label.pink') !!}</span></option>
<option value="label-purple" class="label-purple"><span class="label-purple">{!! __('label.purple') !!}</span></option>
<option value="label-brown" class="label-brown"><span class="label-brown">{!! __('label.brown') !!}</span></option>
<option value="label-default" class="label-default"><span class="label-default">{!! __('label.grey') !!}</span></option>
</select>
</div>
<div class="col-md-2">
<label>{!! __('label.reportType') !!}</label>
<select name="labelType-XXNEWKEYXX" id="labelType-XXNEWKEYXX">
<option value="NEW">{!! __('status.new') !!}</option>
<option value="INPROGRESS">{!! __('status.in_progress') !!}</option>
<option value="DONE">{!! __('status.done') !!}</option>
<option value="NONE">{!! __('status.dont_report') !!}</option>
</select>
</div>
<div class="col-md-2">
<label for="">{!! __('label.showInKanban') !!}</label>
<input type="checkbox" name="labelKanbanCol-XXNEWKEYXX" id="labelKanbanCol-XXNEWKEYXX"/>
</div>
<div class="remove">
<br />
<a href="javascript:void(0);" onclick="leantime.projectsController.removeStatus('XXNEWKEYXX')" class="delete"><span class="fa fa-trash"></span></a>
</div>
</div>
</div>
</div>
@once @push('scripts')
<script type='text/javascript'>
jQuery(document).ready(function() {
jQuery("#projectdetails select").chosen();
@if (isset($_GET['integrationSuccess']))
window.history.pushState({},document.title, '{{ BASE_URL }}/projects/showProject/{{ (int) $project['id'] }}');
@endif
jQuery(".dates").datepicker(
{
dateFormat: leantime.dateHelper.getFormatFromSettings("dateformat", "jquery"),
dayNames: leantime.i18n.__("language.dayNames").split(","),
dayNamesMin: leantime.i18n.__("language.dayNamesMin").split(","),
dayNamesShort: leantime.i18n.__("language.dayNamesShort").split(","),
monthNames: leantime.i18n.__("language.monthNames").split(","),
currentText: leantime.i18n.__("language.currentText"),
closeText: leantime.i18n.__("language.closeText"),
buttonText: leantime.i18n.__("language.buttonText"),
isRTL: leantime.i18n.__("language.isRTL") === "true" ? 1 : 0,
nextText: leantime.i18n.__("language.nextText"),
prevText: leantime.i18n.__("language.prevText"),
weekHeader: leantime.i18n.__("language.weekHeader"),
firstDay: leantime.i18n.__("language.firstDayOfWeek"),
}
);
leantime.projectsController.initProjectTabs();
leantime.projectsController.initDuplicateProjectModal();
leantime.projectsController.initTodoStatusSortable("#todoStatusList");
leantime.projectsController.initSelectFields();
leantime.usersController.initUserEditModal();
// 取消关联主数据
jQuery('.btnUnlinkMaster').on('click', function () {
var refId = jQuery(this).data('ref-id');
if (!confirm('确定取消关联该主数据?项目级追加的列和数据将被删除,不影响全局主数据。')) { return; }
jQuery.ajax({
url: '{{ BASE_URL }}/bom/api/ref/' + refId,
method: 'DELETE',
headers: { 'X-CSRF-TOKEN': jQuery('meta[name="csrf-token"]').attr('content') || '' },
success: function () { location.reload(); },
error: function () { alert('取消失败'); }
});
});
if (window.leantime && window.leantime.tiptapController) {
leantime.tiptapController.initComplexEditor();
}
});
</script>
@endpush @endonce
@endsection

View File

@@ -0,0 +1,221 @@
@php
use Leantime\Domain\Menu\Repositories\Menu;
@endphp
<form action="" method="post" class="stdform">
<div class="row">
<div class="col-md-8">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<x-global::forms.text-input variant="headline" name="name" id="name" style="width:99%" value="{{ $project['name'] }}" placeholder="{{ __('input.placeholders.enter_title_of_project') }}" />
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<p>
{!! __('label.accomplish') !!}
<br /><br />
</p>
<textarea name="details" id="details" class="tiptapComplex" rows="5" cols="50">{{ $project['details'] }}</textarea>
</div>
</div>
<div class="row padding-top">
<div class="col-md-12">
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" id="save" />
</div>
</div>
</div>
<div class="col-md-4">
<div class="row marginBottom">
@if ($projectTypes && count($projectTypes) > 1)
<div class="col-md-12 center">
<h4 class="widgettitle title-light"><i class="fa-regular fa-rectangle-list"></i> Project Type</h4>
<p>The type of the project. This will determine which features are available.</p>
<select name="type">
@foreach ($projectTypes as $key => $type)
<option value="{{ $tpl->escape($key) }}"
@if ($project['type'] == $key)
selected='selected'
@endif
>{!! __($tpl->escape($type)) !!}</option>
@endforeach
</select>
<br /><br />
</div>
@endif
</div>
<div class="row marginBottom">
<div class="col-md-12 center">
<h4 class="widgettitle title-light"><span
class="fa fa-picture-o"></span>{!! __('label.project_avatar') !!}</h4>
<img src='{{ BASE_URL }}/api/projects?projectAvatar={{ $project['id'] }}&v={{ format($project['modified'])->timestamp() }}' class='profileImg' alt='Profile Picture' id="previousImage"/>
<div id="projectAvatar">
</div>
<div class="par">
<div class='fileupload fileupload-new' data-provides='fileupload'>
<input type="hidden"/>
<div class="input-append">
<div class="uneditable-input span3">
<i class="fa-file fileupload-exists"></i>
<span class="fileupload-preview"></span>
</div>
<span class="btn btn-file">
<span class="fileupload-new">{!! __('buttons.select_file') !!}</span>
<span class='fileupload-exists'>{!! __('buttons.change') !!}</span>
<input type='file' name='file' onchange="leantime.projectsController.readURL(this)" accept=".jpg,.png,.gif,.webp"/>
</span>
<a href='#' class='btn fileupload-exists' data-dismiss='fileupload' onclick="leantime.projectsController.clearCroppie()">{!! __('buttons.remove') !!}</a>
</div>
<span id="save-picture" class="btn btn-primary fileupload-exists ld-ext-right">
<span onclick="leantime.projectsController.saveCroppie()">{!! __('buttons.save') !!}</span>
<span class="ld ld-ring ld-spin"></span>
</span>
<input type="hidden" name="profileImage" value="1" />
<input id="picSubmit" type="submit" name="savePic" class="hidden"
value="{{ __('buttons.upload') }}"/>
</div>
</div>
</div>
</div>
@dispatchEvent('afterProjectAvatar', $project)
<div class="row marginBottom" style="margin-bottom: 30px;">
<div class="col-md-12">
<h4 class="widgettitle title-light"><span
class="fa fa-calendar"></span>{!! __('label.project_dates') !!}</h4>
<label class="control-label">{!! __('label.project_start') !!}</label>
<div class="">
<input type="text" class="dates" style="width:100px;" name="start" autocomplete="off"
value="{{ format($project['start'])->date() }}" placeholder="{{ __('language.dateformat') }}"/>
</div>
<label class="control-label">{!! __('label.project_end') !!}</label>
<div class="">
<input type="text" class="dates" style="width:100px;" name="end" autocomplete="off"
value="{{ format($project['end'])->date() }}" placeholder="{{ __('language.dateformat') }}"/>
</div>
</div>
</div>
<div class="row" style="margin-bottom: 30px;">
<div class="col-md-12 " style="margin-bottom: 30px;">
<h4 class="widgettitle title-light"><span
class="fa fa-building"></span>{!! __('label.client_product') !!}</h4>
<select name="clientId" id="clientId">
@foreach ($clients as $row)
<option value="{{ $row['id'] }}"
@if ($project['clientId'] == $row['id'])
selected=selected
@endif
>{{ $row['name'] }}</option>
@endforeach
</select>
@if ($login::userIsAtLeast('manager'))
<br /><a href="{{ BASE_URL }}/clients/newClient" target="_blank">{!! __('label.client_not_listed') !!}</a>
@endif
</div>
</div>
<div class="row marginBottom" style="margin-bottom: 30px;">
<div class="col-md-12">
<h4 class="widgettitle title-light"><span
class="fa fa-wrench"></span>{!! __('label.settings') !!}</h4>
<input type="hidden" name="menuType" id="menuType"
value="{{ Menu::DEFAULT_MENU }}">
<div class="form-group">
<label class="col-md-4 control-label" for="projectState">{!! __('label.project_state') !!}</label>
<div class="col-md-6">
<select name="projectState" id="projectState">
<option value="0" @if ($project['state'] == 0) selected=selected @endif>{!! __('label.open') !!}</option>
<option value="-1" @if ($project['state'] == -1) selected=selected @endif>{!! __('label.closed') !!}</option>
</select>
</div>
</div>
</div>
</div>
<div class="row marginBottom" style="margin-bottom: 30px;">
<div class="col-md-12 ">
<h4 class="widgettitle title-light"><span
class="fa fa-lock-open"></span>{!! __('labels.defaultaccess') !!}</h4>
{!! __('text.who_can_access') !!}
<br /><br />
<select name="globalProjectUserAccess" style="max-width:300px;">
<option value="restricted" {{ $project['psettings'] == 'restricted' ? "selected='selected'" : '' }}>{!! __('labels.only_chose') !!}</option>
<option value="clients" {{ $project['psettings'] == 'clients' ? "selected='selected'" : '' }}>{!! __('labels.everyone_in_client') !!}</option>
<option value="all" {{ $project['psettings'] == 'all' ? "selected='selected'" : '' }}>{!! __('labels.everyone_in_org') !!}</option>
</select>
</div>
</div>
<div class="row" style="margin-bottom: 30px;">
<div class="col-md-12 ">
<h4 class="widgettitle title-light"><span
class="fa fa-money-bill-alt"></span>{!! __('label.budgets') !!}</h4>
<div class="form-group">
<label class="col-md-4 control-label"for="hourBudget">{!! __('label.hourly_budget') !!}</label>
<div class="col-md-6">
<x-global::forms.text-input variant="large" name="hourBudget" id="hourBudget" value="{{ $project['hourBudget'] }}" />
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label" for="dollarBudget">{!! __('label.budget_cost') !!}</label>
<div class="col-md-6">
<x-global::forms.text-input variant="large" name="dollarBudget" id="dollarBudget" value="{{ $project['dollarBudget'] }}" />
</div>
</div>
</div>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,83 @@
<script type="text/javascript">
var ganttData = [
@php
$jsContent = [];
foreach ($projectTickets as $ticket) {
if ($ticket['editFrom'] != '0000-00-00 00:00:00' && $ticket['editFrom'] != '1969-12-31 00:00:00') {
$plannedFromDate = new DateTime($ticket['editFrom']);
$plannedToDate = new DateTime($ticket['editTo']);
} else {
$plannedFromDate = new DateTime;
$plannedToDate = new DateTime;
$plannedToDate->add(new DateInterval('P1D'));
}
$author = str_replace("'", '', str_replace('"', '', json_encode($ticket['firstname'] . ' ' . $ticket['lastname'])));
$jsContent[] = '{
id: ' . $ticket['id'] . ", name: '<a href=\"/tickets/showTicket/" . $ticket['id'] . "'>" . str_replace("'", '', str_replace('"', '', json_encode($ticket['headline']))) . "</a>', series: [
{name: '" . $author . "', start: new Date(" . $plannedFromDate->format('Y') . ', ' . ($plannedFromDate->format('m') - 1) . ', ' . $plannedFromDate->format('d') . '), end: new Date(' . $plannedToDate->format('Y') . ', ' . ($plannedToDate->format('m') - 1) . ', ' . $plannedToDate->format('d') . ') }
]
}';
}
echo implode(',', $jsContent);
@endphp
];
jQuery(function () {
var width = jQuery(".maincontentinner").width() - 500;
jQuery("#ganttChart").ganttView({
data: ganttData,
slideWidth: width,
behavior: {
onClick: function (data) {
},
onResize: function (data) {
var msg = "You edited the To-Do to start: " + data.start.toString("M/d/yyyy") + ", end: " + data.end.toString("M/d/yyyy") + " }";
jQuery("#eventMessage").text(msg);
jQuery.ajax({
type: 'POST',
url: leantime.appUrl+'/tickets/editTicket?raw=true&changeDate=true',
data:
{
id : data.id,
dateFrom:data.start.toString("yyyy-M-d"),
dateTo:data.end.toString("yyyy-M-d")
}
});
jQuery("#eventMessage").show();
},
onDrag: function (data) {
var msg = "You dragged the To-Do to start: " + data.start.toString("M/d/yyyy") + ", end: " + data.end.toString("M/d/yyyy") + " ";
jQuery("#eventMessage").text(msg);
jQuery.ajax({
type: 'POST',
url: leantime.appUrl+'/tickets/editTicket?raw=true&changeDate=true',
data:
{
id : data.id,
dateFrom:data.start.toString("yyyy-M-d"),
dateTo:data.end.toString("yyyy-M-d")
}
});
jQuery("#eventMessage").show();
}
}
});
// $("#ganttChart").ganttView("setSlideWidth", 600);
});
</script>
{!! $tpl->displayLink('tickets.newTicket', "<i class='fa fa-plus'></i> " . __('NEW_TICKET'), null, ['class' => 'btn btn-primary btn-rounded']) !!}
<div id="eventMessage" class="alert alert-success" style="display:none;"></div>
<div id="ganttChart"></div>