Добавлена выгрузка в csv в разделе LTD
parent
a83822befa
commit
0793c34e21
|
|
@ -51,6 +51,26 @@ export function buildTeamTaskQuery(page: number, filters: TeamTaskFilters): Reco
|
|||
return query
|
||||
}
|
||||
|
||||
export function buildTeamTaskExportUrl(teamId: number, filters: TeamTaskFilters): string {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
for (const taskType of filters.taskTypes) {
|
||||
params.append('task_type[]', taskType)
|
||||
}
|
||||
|
||||
for (const assignee of filters.assignees) {
|
||||
params.append('assignee[]', assignee)
|
||||
}
|
||||
|
||||
for (const sprintId of filters.sprintIds) {
|
||||
params.append('sprint_id[]', sprintId)
|
||||
}
|
||||
|
||||
const query = params.toString()
|
||||
|
||||
return `/api/teams/${teamId}/tasks/export${query ? `?${query}` : ''}`
|
||||
}
|
||||
|
||||
function parseStringList(value: unknown): string[] {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import SprintTasksTable from '../components/SprintTasksTable.vue'
|
|||
import TeamLtdFooter from '../components/TeamLtdFooter.vue'
|
||||
import type { SelectOption, TeamTaskFilters } from '../utils/teamLtdFilters'
|
||||
import {
|
||||
buildTeamTaskExportUrl,
|
||||
buildTeamTaskQuery,
|
||||
emptyTeamTaskFilters,
|
||||
hasActiveTeamTaskFilters,
|
||||
|
|
@ -70,6 +71,8 @@ const sprintOptions = computed<SelectOption[]>(() =>
|
|||
|
||||
const hasFilters = computed(() => hasActiveTeamTaskFilters(filters.value))
|
||||
|
||||
const exportUrl = computed(() => buildTeamTaskExportUrl(teamId.value, filters.value))
|
||||
|
||||
function currentPageFromRoute(): number {
|
||||
const value = Number(route.query.page ?? 1)
|
||||
return Number.isFinite(value) && value > 0 ? value : 1
|
||||
|
|
@ -234,6 +237,13 @@ watch(
|
|||
>
|
||||
Сбросить фильтры
|
||||
</button>
|
||||
<a
|
||||
:href="exportUrl"
|
||||
class="team-ltd__export"
|
||||
download
|
||||
>
|
||||
Выгрузить в CSV
|
||||
</a>
|
||||
</div>
|
||||
<p v-if="filterOptionsError" class="team-ltd__filters-error">{{ filterOptionsError }}</p>
|
||||
|
||||
|
|
@ -317,6 +327,23 @@ watch(
|
|||
background: rgba(41, 165, 255, 0.08);
|
||||
}
|
||||
|
||||
.team-ltd__export {
|
||||
margin-left: auto;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid rgba(41, 165, 255, 0.45);
|
||||
border-radius: 8px;
|
||||
color: #29a5ff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.12s ease, border-color 0.12s ease;
|
||||
}
|
||||
|
||||
.team-ltd__export:hover {
|
||||
background: rgba(41, 165, 255, 0.12);
|
||||
border-color: rgba(41, 165, 255, 0.65);
|
||||
}
|
||||
|
||||
.team-ltd__state {
|
||||
padding: 48px 0;
|
||||
text-align: center;
|
||||
|
|
|
|||
|
|
@ -122,6 +122,70 @@ try {
|
|||
exit;
|
||||
}
|
||||
|
||||
if ($method === 'GET' && preg_match('#^/api/teams/(\d+)/tasks/export$#', $path, $matches)) {
|
||||
$result = $api->getAllTasksByTeamId(
|
||||
(int) $matches[1],
|
||||
parseStringListFromQuery('task_type'),
|
||||
parseStringListFromQuery('assignee'),
|
||||
parseIntListFromQuery('sprint_id')
|
||||
);
|
||||
if ($result === null) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => 'Team not found'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$fileName = sprintf('ltd_%s_%s.csv', $result['team_id'], date('Y-m-d'));
|
||||
|
||||
header('Content-Type: text/csv; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . $fileName . '"');
|
||||
|
||||
$output = fopen('php://output', 'wb');
|
||||
// BOM для корректного распознавания UTF-8 в Excel.
|
||||
fwrite($output, "\xEF\xBB\xBF");
|
||||
|
||||
fputcsv($output, [
|
||||
'Задача',
|
||||
'Название',
|
||||
'Спринт',
|
||||
'Тип',
|
||||
'Исполнитель',
|
||||
'Роль',
|
||||
'Оценка (сек)',
|
||||
'Факт (сек)',
|
||||
'Lead Time (дни)',
|
||||
'Переработка (%)',
|
||||
'Flow Efficiency (%)',
|
||||
'Ревью',
|
||||
'Тест',
|
||||
'Взято в работу',
|
||||
'Закрыта',
|
||||
], ';', '"', '');
|
||||
|
||||
foreach ($result['items'] as $item) {
|
||||
fputcsv($output, [
|
||||
$item['task_key'],
|
||||
$item['task_title'],
|
||||
$item['sprint_name'],
|
||||
$item['task_type'],
|
||||
$item['employee_name'] ?? '',
|
||||
$item['employee_role'] ?? '',
|
||||
$item['estimate_seconds'],
|
||||
$item['total_time_seconds'],
|
||||
$item['lead_time_days'] ?? '',
|
||||
$item['recast_percentage'] ?? '',
|
||||
$item['efficiency_percentage'] ?? '',
|
||||
$item['review_count'],
|
||||
$item['test_count'],
|
||||
$item['take_in_work_datetime'] ?? '',
|
||||
$item['resolution_datetime'] ?? '',
|
||||
], ';', '"', '');
|
||||
}
|
||||
|
||||
fclose($output);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($method === 'GET' && preg_match('#^/api/teams/(\d+)/tasks$#', $path, $matches)) {
|
||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
||||
$perPage = max(1, min(100, (int) ($_GET['per_page'] ?? 50)));
|
||||
|
|
|
|||
|
|
@ -274,6 +274,76 @@ final class ReportsApi {
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $taskTypes
|
||||
* @param array<int, string> $assignees
|
||||
* @param array<int, int> $sprintIds
|
||||
* @return array{
|
||||
* team_id: int,
|
||||
* team_name: string,
|
||||
* items: array<int, array<string, mixed>>
|
||||
* }|null
|
||||
*/
|
||||
public function getAllTasksByTeamId(
|
||||
int $teamId,
|
||||
array $taskTypes = [],
|
||||
array $assignees = [],
|
||||
array $sprintIds = []
|
||||
): ?array {
|
||||
$teamRows = $this->db->getRowset(
|
||||
'SELECT id, TRIM(team_name) AS team_name FROM teams WHERE id = :team_id',
|
||||
['team_id' => $teamId]
|
||||
);
|
||||
|
||||
if ($teamRows === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
[$filterSql, $filterParams] = $this->buildTeamTasksFilterSql($taskTypes, $assignees, $sprintIds);
|
||||
|
||||
$baseFrom = 'FROM tasks t
|
||||
JOIN sprints s ON s.id = t.sprint_id
|
||||
LEFT JOIN employees e ON e.id = t.employee_id
|
||||
WHERE s.team_id = :team_id' . $filterSql;
|
||||
|
||||
$rows = $this->db->getRowset(
|
||||
'SELECT t.id,
|
||||
t.task_type,
|
||||
t.task_key,
|
||||
t.task_title,
|
||||
e.employee_name,
|
||||
e.employee_role::text AS employee_role,
|
||||
t.estimate_seconds,
|
||||
t.total_time_seconds,
|
||||
t.take_in_work_datetime,
|
||||
t.resolution_datetime,
|
||||
t.lead_time_days,
|
||||
t.recast_percentage,
|
||||
t.efficiency_percentage,
|
||||
t.review_count,
|
||||
t.test_count,
|
||||
s.id AS sprint_id,
|
||||
s.sprint_name ' . $baseFrom . '
|
||||
ORDER BY t.resolution_datetime DESC NULLS LAST, t.task_key ASC',
|
||||
array_merge(['team_id' => $teamId], $filterParams)
|
||||
);
|
||||
|
||||
return [
|
||||
'team_id' => (int) $teamRows[0]['id'],
|
||||
'team_name' => trim((string) $teamRows[0]['team_name']),
|
||||
'items' => array_map(
|
||||
fn (array $row): array => array_merge(
|
||||
$this->mapTaskRow($row),
|
||||
[
|
||||
'sprint_id' => (int) $row['sprint_id'],
|
||||
'sprint_name' => $row['sprint_name'],
|
||||
]
|
||||
),
|
||||
$rows
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
* @return array{
|
||||
|
|
|
|||
Loading…
Reference in New Issue