OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
64
app/Domain/Sprints/Controllers/DelSprint.php
Normal file
64
app/Domain/Sprints/Controllers/DelSprint.php
Normal 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');
|
||||
}
|
||||
}
|
||||
128
app/Domain/Sprints/Controllers/EditSprint.php
Normal file
128
app/Domain/Sprints/Controllers/EditSprint.php
Normal 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');
|
||||
}
|
||||
}
|
||||
32
app/Domain/Sprints/Models/Sprints.php
Normal file
32
app/Domain/Sprints/Models/Sprints.php
Normal 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() {}
|
||||
}
|
||||
41
app/Domain/Sprints/Permissions/SprintsPermissions.php
Normal file
41
app/Domain/Sprints/Permissions/SprintsPermissions.php
Normal 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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
229
app/Domain/Sprints/Repositories/Sprints.php
Normal file
229
app/Domain/Sprints/Repositories/Sprints.php
Normal 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();
|
||||
}
|
||||
}
|
||||
458
app/Domain/Sprints/Services/Sprints.php
Normal file
458
app/Domain/Sprints/Services/Sprints.php
Normal 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;
|
||||
}
|
||||
}
|
||||
13
app/Domain/Sprints/Templates/delSprint.blade.php
Normal file
13
app/Domain/Sprints/Templates/delSprint.blade.php
Normal 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
|
||||
74
app/Domain/Sprints/Templates/sprintdialog.blade.php
Normal file
74
app/Domain/Sprints/Templates/sprintdialog.blade.php
Normal 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
|
||||
Reference in New Issue
Block a user