OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
361
app/Domain/Reports/Repositories/ReportEngine.php
Normal file
361
app/Domain/Reports/Repositories/ReportEngine.php
Normal file
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Leantime\Domain\Reports\Repositories;
|
||||
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Leantime\Core\Db\DatabaseHelper;
|
||||
use Leantime\Core\Db\Db as DbCore;
|
||||
|
||||
/**
|
||||
* Bulk, session-free queries backing the report engine.
|
||||
*
|
||||
* Every method takes an explicit set of project ids plus the requesting user's context
|
||||
* (user id, client id) so the engine can serve project, plan, strategy and — later —
|
||||
* company-level reports without depending on session('currentProject'). The access
|
||||
* predicate mirrors Tickets\Repositories\Tickets::getAllMilestones: assigned via
|
||||
* zp_relationuserproject, open to all, open to the user's client, or requestor role >= 40.
|
||||
*/
|
||||
class ReportEngine
|
||||
{
|
||||
private ConnectionInterface $connection;
|
||||
|
||||
private DatabaseHelper $dbHelper;
|
||||
|
||||
public function __construct(DbCore $db, DatabaseHelper $dbHelper)
|
||||
{
|
||||
$this->connection = $db->getConnection();
|
||||
$this->dbHelper = $dbHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* All milestones of the given projects the requesting user may see. No date filtering —
|
||||
* the service buckets rows into completed/in-progress/overdue/upcoming.
|
||||
*
|
||||
* @param int[] $projectIds
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function getMilestonesForProjects(array $projectIds, int $userId, int $clientId): array
|
||||
{
|
||||
if ($projectIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = $this->connection->table('zp_tickets')
|
||||
->select([
|
||||
'zp_tickets.id',
|
||||
'zp_tickets.headline',
|
||||
'zp_tickets.description',
|
||||
'zp_tickets.outcomeImpact',
|
||||
'zp_tickets.date',
|
||||
'zp_tickets.projectId',
|
||||
'zp_tickets.status',
|
||||
'zp_tickets.editFrom',
|
||||
'zp_tickets.editTo',
|
||||
'zp_tickets.modified',
|
||||
'zp_projects.name as projectName',
|
||||
])
|
||||
->selectRaw("'milestone' AS ".$this->dbHelper->wrapColumn('type'))
|
||||
->selectRaw("CASE WHEN (zp_tickets.tags IS NULL OR zp_tickets.tags = '') THEN 'var(--grey)' ELSE zp_tickets.tags END AS tags")
|
||||
->leftJoin('zp_projects', 'zp_tickets.projectId', '=', 'zp_projects.id')
|
||||
->where('zp_tickets.type', '=', 'milestone')
|
||||
->whereIn('zp_tickets.projectId', $projectIds)
|
||||
->orderBy('zp_tickets.editTo');
|
||||
|
||||
$this->applyAccessPredicate($query, $userId, $clientId);
|
||||
|
||||
return $query->get()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Status-change history rows for the given tickets, oldest first. The service derives
|
||||
* completion dates from the latest transition into a DONE-type status.
|
||||
*
|
||||
* @param int[] $ticketIds
|
||||
* @return array<int, object> Rows with ticketId, changeValue (new status id), dateModified
|
||||
*/
|
||||
public function getStatusHistoryForTickets(array $ticketIds): array
|
||||
{
|
||||
if ($ticketIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->connection->table('zp_tickethistory')
|
||||
->select(['ticketId', 'changeValue', 'dateModified'])
|
||||
->whereIn('ticketId', $ticketIds)
|
||||
->where('changeType', '=', 'status')
|
||||
->orderBy('dateModified')
|
||||
->get()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Due-date change history ("toDate") for the given tickets within a window, oldest first.
|
||||
* Feeds the commitment-integrity view (milestones pushed out of the period).
|
||||
*
|
||||
* @param int[] $ticketIds
|
||||
* @return array<int, object> Rows with ticketId, changeValue (new due date), dateModified
|
||||
*/
|
||||
public function getDueDateChangesForTickets(array $ticketIds, string $fromDb, string $toDb): array
|
||||
{
|
||||
if ($ticketIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->connection->table('zp_tickethistory')
|
||||
->select(['ticketId', 'changeValue', 'dateModified'])
|
||||
->whereIn('ticketId', $ticketIds)
|
||||
->where('changeType', '=', 'toDate')
|
||||
->whereBetween('dateModified', [$fromDb, $toDb])
|
||||
->orderBy('dateModified')
|
||||
->get()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-milestone tickets belonging to the given milestones (for key-task lists and
|
||||
* milestone progress computation).
|
||||
*
|
||||
* @param int[] $milestoneIds
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function getTasksForMilestones(array $milestoneIds): array
|
||||
{
|
||||
if ($milestoneIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->connection->table('zp_tickets')
|
||||
->select([
|
||||
'id',
|
||||
'headline',
|
||||
'status',
|
||||
'projectId',
|
||||
'milestoneid',
|
||||
'storypoints',
|
||||
'priority',
|
||||
'editTo',
|
||||
'dateToFinish',
|
||||
])
|
||||
->whereIn('milestoneid', $milestoneIds)
|
||||
->where('type', '<>', 'milestone')
|
||||
->orderBy('milestoneid')
|
||||
->get()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Project status updates (comments on module "project" carrying a green/yellow/red status)
|
||||
* within the given window, newest first.
|
||||
*
|
||||
* @param int[] $projectIds
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function getStatusUpdatesForProjects(array $projectIds, string $fromDb, string $toDb, int $userId, int $clientId): array
|
||||
{
|
||||
if ($projectIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = $this->connection->table('zp_comment')
|
||||
->select([
|
||||
'zp_comment.id',
|
||||
'zp_comment.moduleId as projectId',
|
||||
'zp_comment.text',
|
||||
'zp_comment.date',
|
||||
'zp_comment.status',
|
||||
'zp_user.firstname as authorFirstname',
|
||||
'zp_user.lastname as authorLastname',
|
||||
'zp_user.profileId as authorProfileId',
|
||||
])
|
||||
->leftJoin('zp_user', 'zp_comment.userId', '=', 'zp_user.id')
|
||||
->leftJoin('zp_projects', 'zp_comment.moduleId', '=', 'zp_projects.id')
|
||||
->where('zp_comment.module', '=', 'project')
|
||||
->whereIn('zp_comment.moduleId', $projectIds)
|
||||
->whereBetween('zp_comment.date', [$fromDb, $toDb])
|
||||
->orderByDesc('zp_comment.date');
|
||||
|
||||
$this->applyAccessPredicate($query, $userId, $clientId);
|
||||
|
||||
return $query->get()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* The most recent status update per project regardless of period (feeds the status pill).
|
||||
*
|
||||
* @param int[] $projectIds
|
||||
* @return array<int, object> Keyed by projectId
|
||||
*/
|
||||
public function getLatestStatusUpdateForProjects(array $projectIds): array
|
||||
{
|
||||
if ($projectIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$latest = $this->connection->table('zp_comment')
|
||||
->selectRaw('moduleId, MAX(date) as maxDate')
|
||||
->where('module', '=', 'project')
|
||||
->whereIn('moduleId', $projectIds)
|
||||
->groupBy('moduleId');
|
||||
|
||||
$rows = $this->connection->table('zp_comment')
|
||||
->select([
|
||||
'zp_comment.moduleId as projectId',
|
||||
'zp_comment.text',
|
||||
'zp_comment.date',
|
||||
'zp_comment.status',
|
||||
'zp_user.firstname as authorFirstname',
|
||||
'zp_user.lastname as authorLastname',
|
||||
])
|
||||
->joinSub($latest, 'latest', function ($join) {
|
||||
$join->on('zp_comment.moduleId', '=', 'latest.moduleId')
|
||||
->on('zp_comment.date', '=', 'latest.maxDate');
|
||||
})
|
||||
->leftJoin('zp_user', 'zp_comment.userId', '=', 'zp_user.id')
|
||||
->where('zp_comment.module', '=', 'project')
|
||||
->get()
|
||||
->all();
|
||||
|
||||
$byProject = [];
|
||||
foreach ($rows as $row) {
|
||||
$byProject[(int) $row->projectId] = $row;
|
||||
}
|
||||
|
||||
return $byProject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Goals (goal-board canvas items) of the given projects.
|
||||
*
|
||||
* @param int[] $projectIds
|
||||
* @return array<int, object>
|
||||
*/
|
||||
public function getGoalsForProjects(array $projectIds, int $userId, int $clientId): array
|
||||
{
|
||||
if ($projectIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = $this->connection->table('zp_canvas_items')
|
||||
->select([
|
||||
'zp_canvas_items.id',
|
||||
'zp_canvas_items.title',
|
||||
'zp_canvas_items.description',
|
||||
'zp_canvas_items.status',
|
||||
'zp_canvas_items.metricType',
|
||||
'zp_canvas_items.startValue',
|
||||
'zp_canvas_items.currentValue',
|
||||
'zp_canvas_items.endValue',
|
||||
'zp_canvas_items.setting',
|
||||
'zp_canvas_items.kpi',
|
||||
'zp_canvas_items.startDate',
|
||||
'zp_canvas_items.endDate',
|
||||
'zp_canvas_items.canvasId',
|
||||
'zp_canvas.projectId',
|
||||
'zp_canvas.title as boardTitle',
|
||||
])
|
||||
->join('zp_canvas', 'zp_canvas_items.canvasId', '=', 'zp_canvas.id')
|
||||
->leftJoin('zp_projects', 'zp_canvas.projectId', '=', 'zp_projects.id')
|
||||
->where('zp_canvas.type', '=', 'goalcanvas')
|
||||
->where('zp_canvas_items.box', '=', 'goal')
|
||||
->whereIn('zp_canvas.projectId', $projectIds)
|
||||
->orderBy('zp_canvas.projectId')
|
||||
->orderBy('zp_canvas_items.sortindex');
|
||||
|
||||
$this->applyAccessPredicate($query, $userId, $clientId);
|
||||
|
||||
return $query->get()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hours logged against the given projects within a window, grouped by project and
|
||||
* milestone (tasks roll up to their milestone; unassigned work groups under 0).
|
||||
*
|
||||
* @param int[] $projectIds
|
||||
* @return array<int, object> Rows with projectId, milestoneId, loggedHours
|
||||
*/
|
||||
public function getHoursLoggedForProjects(array $projectIds, string $fromDb, string $toDb): array
|
||||
{
|
||||
if ($projectIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->connection->table('zp_timesheets')
|
||||
->selectRaw('zp_tickets.projectId AS '.$this->dbHelper->wrapColumn('projectId'))
|
||||
->selectRaw('COALESCE(zp_tickets.milestoneid, 0) AS '.$this->dbHelper->wrapColumn('milestoneId'))
|
||||
->selectRaw('SUM(zp_timesheets.hours) AS '.$this->dbHelper->wrapColumn('loggedHours'))
|
||||
->join('zp_tickets', 'zp_timesheets.ticketId', '=', 'zp_tickets.id')
|
||||
->whereIn('zp_tickets.projectId', $projectIds)
|
||||
->whereBetween('zp_timesheets.workDate', [$fromDb, $toDb])
|
||||
->groupBy('zp_tickets.projectId')
|
||||
->groupByRaw('COALESCE(zp_tickets.milestoneid, 0)')
|
||||
->get()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Core metadata of the given projects the requesting user may see.
|
||||
*
|
||||
* @param int[] $projectIds
|
||||
* @return array<int, object> Keyed by project id
|
||||
*/
|
||||
public function getProjectsMeta(array $projectIds, int $userId, int $clientId): array
|
||||
{
|
||||
if ($projectIds === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = $this->connection->table('zp_projects')
|
||||
->select([
|
||||
'zp_projects.id',
|
||||
'zp_projects.name',
|
||||
'zp_projects.details',
|
||||
'zp_projects.clientId',
|
||||
'zp_projects.state',
|
||||
'zp_projects.start',
|
||||
'zp_projects.end',
|
||||
'zp_projects.type',
|
||||
'zp_projects.parent',
|
||||
'zp_clients.name as clientName',
|
||||
])
|
||||
->leftJoin('zp_clients', 'zp_projects.clientId', '=', 'zp_clients.id')
|
||||
->whereIn('zp_projects.id', $projectIds);
|
||||
|
||||
$this->applyAccessPredicate($query, $userId, $clientId);
|
||||
|
||||
$byId = [];
|
||||
foreach ($query->get()->all() as $row) {
|
||||
$byId[(int) $row->id] = $row;
|
||||
}
|
||||
|
||||
return $byId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the standard project access predicate: project assigned to the user, open to
|
||||
* everyone, open to the user's client, or the requesting user is admin/owner (role >= 40).
|
||||
* The query must already join zp_projects.
|
||||
*/
|
||||
private function applyAccessPredicate(Builder $query, int $userId, int $clientId): void
|
||||
{
|
||||
$query->leftJoin('zp_user as requestor', function ($join) use ($userId) {
|
||||
$join->on('requestor.id', '=', $this->connection->raw((string) $userId));
|
||||
});
|
||||
|
||||
$query->where(function ($q) use ($userId, $clientId) {
|
||||
$q->whereIn('zp_projects.id', function ($subquery) use ($userId) {
|
||||
$subquery->select('projectId')
|
||||
->from('zp_relationuserproject')
|
||||
->where('zp_relationuserproject.userId', $userId);
|
||||
})
|
||||
->orWhere('zp_projects.psettings', 'all')
|
||||
->orWhere(function ($q2) use ($clientId) {
|
||||
$q2->where('zp_projects.psettings', 'clients')
|
||||
->where('zp_projects.clientId', $clientId);
|
||||
})
|
||||
->orWhere('requestor.role', '>=', 40);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user