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,64 @@
<?php
namespace Leantime\Domain\Sprints\Controllers;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Controller\Frontcontroller;
use Leantime\Domain\Sprints\Permissions\SprintsPermissions;
use Leantime\Domain\Sprints\Services\Sprints as SprintService;
use Symfony\Component\HttpFoundation\Response;
class DelSprint extends Controller
{
private SprintService $sprintService;
/**
* Initializes dependencies.
*/
public function init(SprintService $sprintService): void
{
$this->sprintService = $sprintService;
}
/**
* Displays the delete sprint confirmation.
*
* @param array $params Request parameters
*/
#[RequiresPermission(SprintsPermissions::VIEW)]
public function get(array $params): Response
{
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
$this->tpl->assign('id', $id);
return $this->tpl->displayPartial('sprints.delSprint');
}
/**
* Handles sprint deletion.
*
* @param array $params Request parameters
*/
#[RequiresPermission(SprintsPermissions::DELETE)]
public function post(array $params): Response
{
$id = (int) ($params['id'] ?? $_GET['id'] ?? 0);
if (isset($_POST['del']) && $id > 0) {
$this->sprintService->deleteSprint($id);
$this->tpl->setNotification($this->language->__('notifications.sprint_deleted_successfully'), 'success');
if (session()->exists('lastPage')) {
return Frontcontroller::redirect(session('lastPage'));
}
return Frontcontroller::redirect(BASE_URL.'/tickets/showKanban');
}
$this->tpl->assign('id', $id);
return $this->tpl->displayPartial('sprints.delSprint');
}
}

View File

@@ -0,0 +1,128 @@
<?php
namespace Leantime\Domain\Sprints\Controllers;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Controller\Controller;
use Leantime\Core\Exceptions\MissingParameterException;
use Leantime\Domain\Projects\Services\Projects;
use Leantime\Domain\Sprints\Permissions\SprintsPermissions;
use Leantime\Domain\Sprints\Services\Sprints as SprintService;
class EditSprint extends Controller
{
private SprintService $sprintService;
private Projects $projectService;
/**
* constructor - initialize private variables
*/
public function init(
SprintService $sprintService,
Projects $projectService,
) {
$this->sprintService = $sprintService;
$this->projectService = $projectService;
}
/**
* get - handle get requests
*/
#[RequiresPermission(SprintsPermissions::VIEW)]
public function get($params)
{
if (isset($params['id'])) {
$sprint = $this->sprintService->getSprint((int) $params['id']);
$contextProjectId = $sprint ? (int) $sprint->projectId : null;
} else {
$sprint = $this->sprintService->getNewSprint();
// Default the create context to the current project. assignProjectChoices() only
// locks the picker when that project is a program, so creating a sprint from a
// program's task-screen pageheader makes a PROGRAM sprint, while normal project
// boards keep the full project picker unchanged.
$contextProjectId = isset($params['projectId']) ? (int) $params['projectId'] : (int) session('currentProject');
}
$this->assignProjectChoices($contextProjectId);
$this->tpl->assign('sprint', $sprint);
return $this->tpl->displayPartial('sprints.sprintdialog');
}
/**
* Assign the project picker choices for the sprint dialog. When the sprint belongs to a
* program (PgmPro), offer only that program as a locked option so a program sprint can't
* be reassigned to one of its child projects.
*/
private function assignProjectChoices(?int $contextProjectId): void
{
$allAssignedprojects = $this->projectService->getProjectsAssignedToUser(
userId: session('userdata.id'),
projectStatus: 'open',
projectTypes: 'project'
);
$lockProject = false;
if ($contextProjectId) {
$project = $this->projectService->getProject($contextProjectId);
if (is_array($project) && ($project['type'] ?? '') === 'program') {
$allAssignedprojects = [[
'id' => $project['id'],
'name' => $project['name'],
]];
$lockProject = true;
}
}
$this->tpl->assign('allAssignedprojects', $allAssignedprojects);
$this->tpl->assign('lockProject', $lockProject);
}
/**
* post - handle post requests
*
* Handles both create (no id) and edit (id present), so the controller gate defers
* (entityScoped): the service's addSprint/editSprint each authorize the correct verb
* (CREATE vs EDIT) against the correct per-entity project, which is the authoritative gate.
*/
#[RequiresPermission(SprintsPermissions::EDIT, entityScoped: true)]
public function post($params)
{
// If ID is set its an update
$submittedValues = $params;
$contextProjectId = isset($params['projectId']) ? (int) $params['projectId'] : null;
try {
if (isset($_GET['id']) && $_GET['id'] > 0) {
$params['id'] = (int) $_GET['id'];
$sprintId = $params['id'];
if ($this->sprintService->editSprint($params)) {
$this->tpl->setNotification('Sprint edited successfully', 'success');
} else {
$this->tpl->setNotification('There was a problem saving the sprint', 'error');
}
} else {
if ($sprintId = $this->sprintService->addSprint($params)) {
$this->tpl->setNotification('Sprint created successfully.', 'success');
} else {
$this->tpl->setNotification('There was a problem saving the sprint', 'error');
}
}
} catch (MissingParameterException $e) {
$this->tpl->setNotification($e->getMessage(), 'error');
$this->assignProjectChoices($contextProjectId);
$this->tpl->assign('sprint', (object) $submittedValues);
return $this->tpl->displayPartial('sprints.sprintdialog');
}
$sprint = $this->sprintService->getSprint($sprintId);
$this->assignProjectChoices($sprint ? (int) $sprint->projectId : $contextProjectId);
$this->tpl->assign('sprint', $sprint);
return $this->tpl->displayPartial('sprints.sprintdialog');
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Leantime\Domain\Sprints\Models;
class Sprints
{
public $id;
public $name;
public $startDate;
public $endDate;
public $projectId;
public $modified;
/**
* Runtime flag set by plugins (PgmPro) when this sprint is inherited by the current
* project from its parent program, rather than owned by the project itself. Drives
* UI affordances (badge, hidden edit/delete). Not persisted.
*/
public bool $isInherited = false;
/**
* The program id this sprint is inherited from, when $isInherited is true. Not persisted.
*/
public ?int $ownerProgramId = null;
public function __construct() {}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Leantime\Domain\Sprints\Permissions;
use Leantime\Core\Auth\Permissions\Permission;
use Leantime\Core\Auth\Permissions\ProvidesPermissions;
/**
* The Sprints permission vocabulary — the verbs only.
*
* Sprints are PROJECT-scoped (a sprint belongs to one project), so every capability is
* evaluated against the user's role IN that project (projectScoped = true, the default). The
* standard verbs auto-grant via the central matrix (readonly = view; editor = create/edit/delete;
* manager+ = all), so no {@see \Leantime\Core\Auth\Permissions\DefaultRolePermissions} change is
* required.
*/
final class SprintsPermissions implements ProvidesPermissions
{
public const VIEW = 'sprints.view';
public const CREATE = 'sprints.create';
public const EDIT = 'sprints.edit';
public const DELETE = 'sprints.delete';
public function domain(): string
{
return 'sprints';
}
public function permissions(): array
{
return [
new Permission(self::VIEW, 'View sprints'),
new Permission(self::CREATE, 'Create sprints'),
new Permission(self::EDIT, 'Edit sprints'),
new Permission(self::DELETE, 'Delete sprints'),
];
}
}

View File

@@ -0,0 +1,229 @@
<?php
namespace Leantime\Domain\Sprints\Repositories;
use Illuminate\Database\ConnectionInterface;
use Leantime\Core\Db\Db as DbCore;
use Leantime\Domain\Sprints\Models\Sprints as SprintsModel;
class Sprints
{
private ConnectionInterface $db;
/**
* __construct - get database connection
*/
public function __construct(DbCore $db)
{
$this->db = $db->getConnection();
}
/**
* getSprint - get single sprint
*/
public function getSprint(int $id): SprintsModel|false
{
$result = $this->db->table('zp_sprints as sprint')
->select(
'sprint.id',
'sprint.name',
'sprint.projectId',
'sprint.startDate',
'sprint.endDate',
'sprint.modified'
)
->where('sprint.id', $id)
->limit(1)
->first();
if ($result === null) {
return false;
}
$sprint = new SprintsModel;
$sprint->id = $result->id;
$sprint->name = $result->name;
$sprint->projectId = $result->projectId;
$sprint->startDate = $result->startDate;
$sprint->endDate = $result->endDate;
$sprint->modified = $result->modified;
return $sprint;
}
/**
* getAllSprints - get all sprints for a project
*/
public function getAllSprints(?int $projectId = null): array
{
$query = $this->db->table('zp_sprints')
->select(
'id',
'name',
'projectId',
'startDate',
'endDate',
'modified'
);
if ($projectId !== null) {
$query->where('projectId', $projectId);
}
$results = $query->orderBy('startDate', 'desc')->get();
return $results->map(function ($row) {
$sprint = new SprintsModel;
$sprint->id = $row->id;
$sprint->name = $row->name;
$sprint->projectId = $row->projectId;
$sprint->startDate = $row->startDate;
$sprint->endDate = $row->endDate;
$sprint->modified = $row->modified;
return $sprint;
})->toArray();
}
/**
* getAllFutureSprints - get all future sprints for a project
*/
public function getAllFutureSprints(int $projectId): array
{
$results = $this->db->table('zp_sprints')
->select(
'id',
'name',
'projectId',
'startDate',
'endDate',
'modified'
)
->where('projectId', $projectId)
->where('endDate', '>', now())
->orderBy('startDate', 'desc')
->get();
return $results->map(function ($row) {
$sprint = new SprintsModel;
$sprint->id = $row->id;
$sprint->name = $row->name;
$sprint->projectId = $row->projectId;
$sprint->startDate = $row->startDate;
$sprint->endDate = $row->endDate;
$sprint->modified = $row->modified;
return $sprint;
})->toArray();
}
/**
* getCurrentSprint - get current sprint for a project
*/
public function getCurrentSprint(int $projectId): mixed
{
$result = $this->db->table('zp_sprints')
->select(
'id',
'name',
'projectId',
'startDate',
'endDate',
'modified'
)
->where('projectId', $projectId)
->where('startDate', '<', now())
->where('endDate', '>', now())
->orderBy('startDate')
->limit(1)
->first();
if ($result === null) {
return false;
}
$sprint = new SprintsModel;
$sprint->id = $result->id;
$sprint->name = $result->name;
$sprint->projectId = $result->projectId;
$sprint->startDate = $result->startDate;
$sprint->endDate = $result->endDate;
$sprint->modified = $result->modified;
return $sprint;
}
/**
* getUpcomingSprint - gets the next upcoming sprint
*/
public function getUpcomingSprint(int $projectId): SprintsModel|false
{
$result = $this->db->table('zp_sprints')
->select(
'id',
'name',
'projectId',
'startDate',
'endDate',
'modified'
)
->where('projectId', $projectId)
->where('startDate', '>', now())
->orderBy('startDate', 'asc')
->limit(1)
->first();
if ($result === null) {
return false;
}
$sprint = new SprintsModel;
$sprint->id = $result->id;
$sprint->name = $result->name;
$sprint->projectId = $result->projectId;
$sprint->startDate = $result->startDate;
$sprint->endDate = $result->endDate;
$sprint->modified = $result->modified;
return $sprint;
}
public function addSprint(SprintsModel $sprint): bool|int
{
$id = $this->db->table('zp_sprints')->insertGetId([
'name' => $sprint->name,
'projectId' => $sprint->projectId,
'startDate' => $sprint->startDate,
'endDate' => $sprint->endDate,
'modified' => now(),
]);
return $id ?: false;
}
public function editSprint(SprintsModel $sprint): bool
{
return $this->db->table('zp_sprints')
->where('id', $sprint->id)
->update([
'name' => $sprint->name,
'projectId' => $sprint->projectId,
'startDate' => $sprint->startDate,
'endDate' => $sprint->endDate,
'modified' => now(),
]) >= 0;
}
public function delSprint(int|string $id): void
{
// Clear sprint from tickets
$this->db->table('zp_tickets')
->where('sprint', $id)
->update(['sprint' => null]);
// Delete the sprint
$this->db->table('zp_sprints')
->where('id', $id)
->delete();
}
}

View File

@@ -0,0 +1,458 @@
<?php
namespace Leantime\Domain\Sprints\Services;
use DateInterval;
use DatePeriod;
use DateTime;
use Leantime\Core\Auth\Permissions\RequiresPermission;
use Leantime\Core\Domains\BaseService;
use Leantime\Core\Exceptions\MissingParameterException;
use Leantime\Domain\Reports\Repositories\Reports as ReportRepository;
use Leantime\Domain\Sprints\Models;
use Leantime\Domain\Sprints\Permissions\SprintsPermissions;
use Leantime\Domain\Sprints\Repositories\Sprints as SprintRepository;
/**
* @api
*/
class Sprints extends BaseService
{
public function __construct(
private SprintRepository $sprintRepository,
private ReportRepository $reportRepository
) {}
/**
* @api
*/
#[RequiresPermission(SprintsPermissions::VIEW, entityScoped: true)]
public function getSprint(int $id): false|Models\Sprints
{
$sprint = $this->sprintRepository->getSprint($id);
if (! $sprint) {
return false;
}
// IDOR fence: the id alone names any project's sprint. Authorize VIEW against the sprint's
// ACTUAL project — not the session project — mirroring the editSprint/deleteSprint write
// fences and the Tickets::getTicket read precedent. entityScoped makes this fire on every
// call (RPC, the EditSprint controller, and internal callers); the internal callers
// (Reports burndown, EditSprint's own session) only ever pass sprints from an
// already-accessible project, so they keep working — only the cross-project read is denied.
$this->authorize(SprintsPermissions::VIEW, (int) $sprint->projectId);
return $sprint;
}
/**
* getNewSprint - builds a blank sprint pre-populated with the default
* 13-day window (today through 13 days from now in the user's timezone).
*
* @internal Pure in-memory builder (no project, no repo) — not an RPC surface.
*/
public function getNewSprint(): Models\Sprints
{
$sprint = new Models\Sprints;
$sprint->startDate = dtHelper()->userNow();
$sprint->endDate = dtHelper()->userNow()->addDays(13);
return $sprint;
}
/**
* getCurrentSprintId returns the ID of the current sprint in the project provided
*
* @api
*/
#[RequiresPermission(SprintsPermissions::VIEW, projectIdParam: 'projectId')]
public function getCurrentSprintId(int $projectId): bool|int
{
if (session('currentSprint', '') !== '') {
return session('currentSprint');
}
session(['currentSprint' => '']);
return false;
}
/**
* @api
*/
#[RequiresPermission(SprintsPermissions::VIEW, projectIdParam: 'projectId')]
public function getUpcomingSprint(int $projectId): false|\Leantime\Domain\Sprints\Models\Sprints
{
$sprint = $this->sprintRepository->getUpcomingSprint($projectId);
if ($sprint) {
return $sprint;
}
return false;
}
/**
* @api
*/
#[RequiresPermission(SprintsPermissions::VIEW, projectIdParam: 'projectId')]
public function getAllSprints($projectId = null): array
{
$sprints = $this->sprintRepository->getAllSprints($projectId);
// Caution: Empty arrays will be false
$sprints = $sprints ?: [];
// Allow plugins (e.g. PgmPro) to append inherited sprints for this project — for
// example the sprints owned by the project's parent program. Mirrors the
// Tickets::filterTickets extension point.
$sprints = self::dispatchFilter('afterGettingAllSprints', $sprints, ['projectId' => $projectId]);
return $sprints;
}
/**
* @api
*/
#[RequiresPermission(SprintsPermissions::VIEW, projectIdParam: 'projectId')]
public function getAllFutureSprints(int $projectId): false|array
{
$sprints = $this->sprintRepository->getAllFutureSprints($projectId);
if ($sprints) {
return $sprints;
}
return false;
}
/**
* @throws MissingParameterException When the start or end date is missing.
*
* @api
*/
#[RequiresPermission(SprintsPermissions::CREATE, entityScoped: true)]
public function addSprint($params): int|false
{
// Authorize before validating, so an unauthorized caller is denied first and never learns
// about parameter requirements (matches the Tickets::addTicket authorize-then-validate order).
$projectId = (int) ($params['projectId'] ?? session('currentProject'));
$this->authorize(SprintsPermissions::CREATE, $projectId);
$this->assertSprintDates($params);
$sprint = new Models\Sprints;
foreach ($params as $key => $value) {
$sprint->$key = $value;
}
if (dtHelper()->isValidDateString($sprint->startDate ?? null)) {
$sprint->startDate = dtHelper()->parseUserDateTime($sprint->startDate)->startOfDay()->formatDateTimeForDb();
}
if (dtHelper()->isValidDateString($sprint->endDate ?? null)) {
$sprint->endDate = dtHelper()->parseUserDateTime($sprint->endDate)->endOfDay()->formatDateTimeForDb();
}
$sprint->projectId = $projectId;
$result = $this->sprintRepository->addSprint($sprint);
if ($result !== false) {
return $result;
}
return false;
}
/**
* @throws MissingParameterException When the start or end date is missing.
*
* @api
*/
#[RequiresPermission(SprintsPermissions::EDIT, entityScoped: true)]
public function editSprint($params): Models\Sprints|false
{
// IDOR fence: $params['id'] could name any project's sprint. Authorize edit against the
// EXISTING sprint's project (and, below, the target project if it's relocated) BEFORE
// validating params, so an unauthorized caller is denied first (Tickets authorize-first order).
$existing = $this->sprintRepository->getSprint((int) ($params['id'] ?? 0));
if ($existing) {
$this->authorize(SprintsPermissions::EDIT, (int) $existing->projectId);
}
$this->assertSprintDates($params);
$sprint = new Models\Sprints;
foreach ($params as $key => $value) {
$sprint->$key = $value;
}
if (dtHelper()->isValidDateString($sprint->startDate ?? null)) {
$sprint->startDate = dtHelper()->parseUserDateTime($sprint->startDate)->startOfDay()->formatDateTimeForDb();
}
if (dtHelper()->isValidDateString($sprint->endDate ?? null)) {
$sprint->endDate = dtHelper()->parseUserDateTime($sprint->endDate)->endOfDay()->formatDateTimeForDb();
}
$sprint->projectId = $params['projectId'] ?? session('currentProject');
// Relocating to another project also requires edit rights there.
if ((int) $sprint->projectId !== (int) ($existing->projectId ?? 0)) {
$this->authorize(SprintsPermissions::EDIT, (int) $sprint->projectId);
}
$result = $this->sprintRepository->editSprint($sprint);
if ($result) {
return $sprint;
}
return false;
}
/**
* deleteSprint - deletes a sprint and clears the current-sprint session value.
*
* @param int $id Sprint id to delete.
*
* @api
*/
#[RequiresPermission(SprintsPermissions::DELETE, entityScoped: true)]
public function deleteSprint(int $id): void
{
// IDOR fence: the id alone identified the row before, so any editor could delete another
// project's sprint (and detach its tickets). Authorize delete against the sprint's project.
$sprint = $this->sprintRepository->getSprint($id);
if ($sprint) {
$this->authorize(SprintsPermissions::DELETE, (int) $sprint->projectId);
}
$this->sprintRepository->delSprint($id);
session(['currentSprint' => '']);
}
/**
* assertSprintDates - ensures the start and end date are both provided.
*
* @param array $params Incoming sprint params.
*
* @throws MissingParameterException When the start or end date is missing.
*/
private function assertSprintDates(array $params): void
{
if (($params['startDate'] ?? '') == '' || ($params['endDate'] ?? '') == '') {
throw new MissingParameterException('First day and last day are required');
}
}
/**
* @throws \Exception
*
* @api
*/
#[RequiresPermission(SprintsPermissions::VIEW)]
public function getSprintBurndown(Models\Sprints $sprint): false|array
{
if (! is_object($sprint)) {
return false;
}
$sprintValues = $this->reportRepository->getSprintReport($sprint->id);
$sprintData = [];
foreach ($sprintValues as $row) {
if (is_object($row)) {
$sprintData[$row->date] = $row;
}
}
$allKeys = array_keys($sprintData);
// If the first day is set in our reports table
if (isset($allKeys[0])) {
$plannedHoursStart = $sprintData[$allKeys[0]]->sum_planned_hours;
$plannedNumStart = $sprintData[$allKeys[0]]->sum_todos;
$plannedEffortStart = $sprintData[$allKeys[0]]->sum_points;
} else {
// If the sprint started today and we don't have any data to report, planned is 0
$plannedHoursStart = 0;
$plannedNumStart = 0;
$plannedEffortStart = 0;
}
if (dtHelper()->isValidDateString($sprint->startDate)) {
$dateStart = dtHelper()->parseDbDateTime($sprint->startDate)->startOfDay();
} elseif (dtHelper()->isValidDateString($sprint->modified)) {
$dateStart = dtHelper()->parseDbDateTime($sprint->modified)->startOfDay();
} else {
$dateStart = dtHelper()->userNow()->startOfDay();
}
if (dtHelper()->isValidDateString($sprint->endDate)) {
$dateEnd = dtHelper()->parseDbDateTime($sprint->endDate)->endOfDay();
} else {
$dateEnd = dtHelper()->dbNow()->addDays(7)->endOfDay();
}
$sprintLength = $dateStart->diffInDays($dateEnd);
$period = $dateStart->daysUntil($dateEnd);
$sprintLength++; // Diff is 1 day less than actual sprint days (eg even if a sprint starts and ends today it should still be a 1 day sprint, but the diff would be 0)
$dailyHoursPlanned = $plannedHoursStart / $sprintLength;
$dailyNumPlanned = $plannedNumStart / $sprintLength;
$dailyEffortPlanned = $plannedEffortStart / $sprintLength;
$burnDown = [];
$i = 0;
foreach ($period as $key => $value) {
$burnDown[$i]['date'] = $value->format('Y-m-d');
if ($i === 0) {
$burnDown[$i]['plannedHours'] = $plannedHoursStart;
$burnDown[$i]['plannedNum'] = $plannedNumStart;
$burnDown[$i]['plannedEffort'] = $plannedEffortStart;
} else {
$burnDown[$i]['plannedHours'] = $burnDown[$i - 1]['plannedHours'] - $dailyHoursPlanned;
$burnDown[$i]['plannedNum'] = $burnDown[$i - 1]['plannedNum'] - $dailyNumPlanned;
$burnDown[$i]['plannedEffort'] = $burnDown[$i - 1]['plannedEffort'] - $dailyEffortPlanned;
}
$dateKey = $value->format('Y-m-d').' 00:00:00';
if (isset($sprintData[$dateKey])) {
$burnDown[$i]['actualHours'] = $sprintData[$dateKey]->sum_estremaining_hours;
$burnDown[$i]['actualNum'] = $sprintData[$dateKey]->sum_open_todos + $sprintData[$dateKey]->sum_progres_todos;
$burnDown[$i]['actualEffort'] = $sprintData[$dateKey]->sum_points_open + $sprintData[$dateKey]->sum_points_progress;
} elseif ($i === 0) {
$burnDown[$i]['actualHours'] = $plannedHoursStart;
$burnDown[$i]['actualNum'] = $plannedNumStart;
$burnDown[$i]['actualEffort'] = $plannedEffortStart;
} else {
// If the date is in the future. Set to 0
$today = new DateTime;
if ($value->format('Ymd') < $today->format('Ymd')) {
$burnDown[$i]['actualHours'] = $burnDown[$i - 1]['actualHours'];
$burnDown[$i]['actualNum'] = $burnDown[$i - 1]['actualNum'];
$burnDown[$i]['actualEffort'] = $burnDown[$i - 1]['actualEffort'];
} else {
$burnDown[$i]['actualHours'] = '';
$burnDown[$i]['actualNum'] = '';
$burnDown[$i]['actualEffort'] = '';
}
}
$i++;
}
return $burnDown;
}
/**
* @throws \Exception
*
* @api
*/
#[RequiresPermission(SprintsPermissions::VIEW, projectIdParam: 'project')]
public function getCummulativeReport($project): false|array
{
if (! ($project)) {
return false;
}
$sprintValues = $this->reportRepository->getFullReport($project);
$sprintData = [];
foreach ($sprintValues as $row) {
$sprintData[$row->date] = $row;
}
$allKeys = array_keys($sprintData);
$burnDown = [];
if (count($allKeys) > 0) {
$period = new DatePeriod(
new DateTime($allKeys[count($allKeys) - 1]),
new DateInterval('P1D'),
new DateTime
);
$i = 0;
foreach ($period as $key => $value) {
$burnDown[$i]['date'] = $value->format('Y-m-d');
$dateKey = $value->format('Y-m-d').' 00:00:00';
if (isset($sprintData[$dateKey])) {
$burnDown[$i]['open']['actualHours'] = $sprintData[$dateKey]->sum_estremaining_hours;
$burnDown[$i]['open']['actualNum'] = $sprintData[$dateKey]->sum_open_todos;
$burnDown[$i]['open']['actualEffort'] = $sprintData[$dateKey]->sum_points_open;
$burnDown[$i]['progress']['actualHours'] = 0;
$burnDown[$i]['progress']['actualNum'] = $sprintData[$dateKey]->sum_progres_todos;
$burnDown[$i]['progress']['actualEffort'] = $sprintData[$dateKey]->sum_points_progress;
$burnDown[$i]['done']['actualHours'] = $sprintData[$dateKey]->sum_logged_hours;
$burnDown[$i]['done']['actualNum'] = $sprintData[$dateKey]->sum_closed_todos;
$burnDown[$i]['done']['actualEffort'] = $sprintData[$dateKey]->sum_points_done;
} elseif ($i === 0) {
$burnDown[$i]['open']['actualHours'] = 0;
$burnDown[$i]['open']['actualNum'] = 0;
$burnDown[$i]['open']['actualEffort'] = 0;
$burnDown[$i]['progress']['actualHours'] = 0;
$burnDown[$i]['progress']['actualNum'] = 0;
$burnDown[$i]['progress']['actualEffort'] = 0;
$burnDown[$i]['done']['actualHours'] = 0;
$burnDown[$i]['done']['actualNum'] = 0;
$burnDown[$i]['done']['actualEffort'] = 0;
} else {
// If the date is in the future. Set to 0
$today = new DateTime;
if ($value->format('Ymd') < $today->format('Ymd')) {
$burnDown[$i]['open']['actualHours'] = $burnDown[$i - 1]['open']['actualHours'];
$burnDown[$i]['open']['actualNum'] = $burnDown[$i - 1]['open']['actualNum'];
$burnDown[$i]['open']['actualEffort'] = $burnDown[$i - 1]['open']['actualEffort'];
$burnDown[$i]['progress']['actualHours'] = $burnDown[$i - 1]['progress']['actualHours'];
$burnDown[$i]['progress']['actualNum'] = $burnDown[$i - 1]['progress']['actualNum'];
$burnDown[$i]['progress']['actualEffort'] = $burnDown[$i - 1]['progress']['actualEffort'];
$burnDown[$i]['done']['actualHours'] = $burnDown[$i - 1]['done']['actualHours'];
$burnDown[$i]['done']['actualNum'] = $burnDown[$i - 1]['done']['actualNum'];
$burnDown[$i]['done']['actualEffort'] = $burnDown[$i - 1]['done']['actualEffort'];
} else {
$burnDown[$i]['open']['actualHours'] = '';
$burnDown[$i]['open']['actualNum'] = '';
$burnDown[$i]['open']['actualEffort'] = '';
$burnDown[$i]['progress']['actualHours'] = '';
$burnDown[$i]['progress']['actualNum'] = '';
$burnDown[$i]['progress']['actualEffort'] = '';
$burnDown[$i]['done']['actualHours'] = '';
$burnDown[$i]['done']['actualNum'] = '';
$burnDown[$i]['done']['actualEffort'] = '';
}
}
$i++;
}
}
return $burnDown;
}
}

View File

@@ -0,0 +1,13 @@
@extends($layout)
@section('content')
<h4 class="widgettitle title-light">{!! __('headlines.delete_sprint') !!}</h4>
<form method="post" action="{{ BASE_URL }}/sprints/delSprint/{{ $id }}">
<p>{!! __('text.are_you_sure_delete_sprint') !!}</p><br />
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.yes_delete')" name="del" />
<x-global::forms.button tag="a" contentRole="tertiary" link="{{ session('lastPage') }}">{!! __('buttons.back') !!}</x-global::forms.button>
</form>
@endsection

View File

@@ -0,0 +1,74 @@
@extends($layout)
@section('content')
@php
$currentSprint = $sprint;
$id = '';
if (isset($currentSprint->id)) {
$id = $currentSprint->id;
}
$currentProject = session('currentProject');
@endphp
<h4 class="widgettitle title-light"><i class="fa fa-list-1-2"></i> {!! __('label.sprint') !!} {{ $currentSprint->name }}</h4>
{!! $tpl->displayNotification() !!}
<form class="formModal" method="post" action="{{ BASE_URL }}/sprints/editSprint/{{ $id }}">
<label>{!! __('label.sprint_name') !!}</label>
<x-global::forms.text-input name="name" value="{{ $currentSprint->name }}" placeholder="{{ __('label.sprint_name') }}" /><br />
<label>{!! __('label.project') !!}</label>
@if (! empty($lockProject))
{{-- Program sprint: project is fixed to the owning program and cannot be reassigned. --}}
@php $lockedProject = $allAssignedprojects[0] ?? null; @endphp
<input type="hidden" name="projectId" value="{{ $lockedProject['id'] ?? $currentProject }}" />
<select disabled>
<option selected>{{ $tpl->escape($lockedProject['name'] ?? '') }}</option>
</select><br />
@else
<select name="projectId">
@foreach ($allAssignedprojects as $project)
<option value="{{ $project['id'] }}"
@if ((isset($currentSprint) && ($currentSprint->projectId == $project['id'] || $currentProject == $project['id'])) || (! isset($currentSprint) && $currentProject == $project['id']))
selected
@endif
>{{ $tpl->escape($project['name']) }}</option>
@endforeach
</select><br />
@endif
<br /><br />
<p>{!! __('label.sprint_dates') !!}</p><br/>
<label>{!! __('label.first_day') !!}</label>
<input type="text" name="startDate" autocomplete="off" value="{{ format($currentSprint->startDate)->date() }}" placeholder="{{ __('language.dateformat') }}" id="sprintStart" /><br />
<label>{!! __('label.last_day') !!}</label>
<input type="text" name="endDate" autocomplete="off" value="{{ format($currentSprint->endDate)->date() }} " placeholder="{{ __('language.dateformat') }}" id="sprintEnd" />
<br />
<div class="row">
<div class="col-md-6">
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" />
</div>
<div class="col-md-6 align-right padding-top-sm">
@if (isset($currentSprint->id) && $currentSprint->id != '' && $login::userIsAtLeast($roles::$editor))
<a href="{{ BASE_URL }}/sprints/delSprint/{{ $currentSprint->id }}" class="delete formModal sprintModal"><i class="fa fa-trash"></i> {!! __('links.delete_sprint') !!}</a>
@endif
</div>
</div>
</form>
@once
@push('scripts')
<script>
leantime.ticketsController.initSprintDates();
</script>
@endpush
@endonce
@endsection